Introduction
The release of Vue 3 brought a transformational change, moving from the Options API to the Composition API. At the heart of this transition lies the concept of “composables” — modular functions that leverage Vue’s reactive features. This change enhanced the framework’s flexibility and code reusability. The inconsistent implementation of composables across projects often leads to convoluted and hard-to-maintain codebases.
This style guide harmonizes coding practices around composables, focusing on producing clean, maintainable, and testable code. While composables represent a new pattern, they remain functions at their core. The guide bases its recommendations on time-tested principles of good software design.
This guide serves as a comprehensive resource for both newcomers to Vue 3 and experienced developers aiming to standardize their team’s coding style.
Table of Contents
Open Table of Contents
File Naming
Rule 1.1: Prefix with use
and Follow PascalCase
// Good
useCounter.ts;
useApiRequest.ts;
// Bad
counter.ts;
APIrequest.ts;
Composable Naming
Rule 2.1: Use Descriptive Names
// Good
export function function useUserData(): void
useUserData() {}
// Bad
export function function useData(): void
useData() {}
Folder Structure
Rule 3.1: Place in composables Directory
src/
└── composables/
├── useCounter.ts
└── useUserData.ts
Argument Passing
Rule 4.1: Use Object Arguments for Four or More Parameters
// Good: For Multiple Parameters
useUserData({ id: number
id: 1, fetchOnMount: boolean
fetchOnMount: true, token: string
token: "abc", locale: string
locale: "en" });
// Also Good: For Fewer Parameters
useCounter(1, true, "session");
// Bad
useUserData(1, true, "abc", "en");
Error Handling
Rule 5.1: Expose Error State
// Good
const const error: any
error = ref(null);
try {
// Do something
} catch (var err: unknown
err) {
const error: any
error.value = var err: unknown
err;
}
return { error: any
error };
// Bad
try {
// Do something
} catch (var err: unknown
err) {
var console: Console
The `console` module provides a simple debugging console that is similar to the
JavaScript console mechanism provided by web browsers.
The module exports two specific components:
* A `Console` class with methods such as `console.log()`, `console.error()` and `console.warn()` that can be used to write to any Node.js stream.
* A global `console` instance configured to write to [`process.stdout`](https://nodejs.org/docs/latest-v22.x/api/process.html#processstdout) and
[`process.stderr`](https://nodejs.org/docs/latest-v22.x/api/process.html#processstderr). The global `console` can be used without importing the `node:console` module.
_**Warning**_: The global console object's methods are neither consistently
synchronous like the browser APIs they resemble, nor are they consistently
asynchronous like all other Node.js streams. See the [`note on process I/O`](https://nodejs.org/docs/latest-v22.x/api/process.html#a-note-on-process-io) for
more information.
Example using the global `console`:
```js
console.log('hello world');
// Prints: hello world, to stdout
console.log('hello %s', 'world');
// Prints: hello world, to stdout
console.error(new Error('Whoops, something bad happened'));
// Prints error message and stack trace to stderr:
// Error: Whoops, something bad happened
// at [eval]:5:15
// at Script.runInThisContext (node:vm:132:18)
// at Object.runInThisContext (node:vm:309:38)
// at node:internal/process/execution:77:19
// at [eval]-wrapper:6:22
// at evalScript (node:internal/process/execution:76:60)
// at node:internal/main/eval_string:23:3
const name = 'Will Robinson';
console.warn(`Danger ${name}! Danger!`);
// Prints: Danger Will Robinson! Danger!, to stderr
```
Example using the `Console` class:
```js
const out = getStreamSomehow();
const err = getStreamSomehow();
const myConsole = new console.Console(out, err);
myConsole.log('hello world');
// Prints: hello world, to out
myConsole.log('hello %s', 'world');
// Prints: hello world, to out
myConsole.error(new Error('Whoops, something bad happened'));
// Prints: [Error: Whoops, something bad happened], to err
const name = 'Will Robinson';
myConsole.warn(`Danger ${name}! Danger!`);
// Prints: Danger Will Robinson! Danger!, to err
```console.Console.error(message?: any, ...optionalParams: any[]): void (+1 overload)
Prints to `stderr` with newline. Multiple arguments can be passed, with the
first used as the primary message and all additional used as substitution
values similar to [`printf(3)`](http://man7.org/linux/man-pages/man3/printf.3.html)
(the arguments are all passed to [`util.format()`](https://nodejs.org/docs/latest-v22.x/api/util.html#utilformatformat-args)).
```js
const code = 5;
console.error('error #%d', code);
// Prints: error #5, to stderr
console.error('error', code);
// Prints: error 5, to stderr
```
If formatting elements (e.g. `%d`) are not found in the first string then
[`util.inspect()`](https://nodejs.org/docs/latest-v22.x/api/util.html#utilinspectobject-options) is called on each argument and the
resulting string values are concatenated. See [`util.format()`](https://nodejs.org/docs/latest-v22.x/api/util.html#utilformatformat-args)
for more information.error("An error occurred:", var err: unknown
err);
}
return {};
Avoid Mixing UI and Business Logic
Rule 6.2: Decouple UI from Business Logic in Composables
Composables should focus on managing state and business logic, avoiding UI-specific behavior like toasts or alerts. Keeping UI logic separate from business logic will ensure that your composable is reusable and testable.
// Good
export function function useUserData(userId: any): {
user: any;
error: any;
fetchUser: () => Promise<void>;
} (+1 overload)
useUserData(userId: any
userId) {
const const user: any
user = ref(null);
const const error: any
error = ref(null);
const const fetchUser: () => Promise<void>
fetchUser = async () => {
try {
const const response: any
response = await axios.get(`/api/users/${userId: any
userId}`);
const user: any
user.value = const response: any
response.data;
} catch (function (local var) e: unknown
e) {
const error: any
error.value = function (local var) e: unknown
e;
}
};
return { user: any
user, error: any
error, fetchUser: () => Promise<void>
fetchUser };
}
// In component
setup() {
const { const user: any
user, const error: any
error, const fetchUser: () => Promise<void>
fetchUser } = function useUserData(userId: any): {
user: any;
error: any;
fetchUser: () => Promise<void>;
} (+1 overload)
useUserData(userId);
watch(const error: any
error, (newValue: any
newValue) => {
if (newValue: any
newValue) {
showToast("An error occurred."); // UI logic in component
}
});
return { user: any
user, fetchUser: () => Promise<void>
fetchUser };
}
// Bad
export function function useUserData(userId: any): {
user: any;
error: any;
fetchUser: () => Promise<void>;
} (+1 overload)
useUserData(userId: any
userId) {
const const user: any
user = ref(null);
const const fetchUser: () => Promise<void>
fetchUser = async () => {
try {
const const response: any
response = await axios.get(`/api/users/${userId: any
userId}`);
const user: any
user.value = const response: any
response.data;
} catch (function (local var) e: unknown
e) {
showToast("An error occurred."); // UI logic inside composable
}
};
return { user: any
user, fetchUser: () => Promise<void>
fetchUser };
}
Anatomy of a Composable
Rule 7.2: Structure Your Composables Well
A well-structured composable improves understanding, usage, and maintenance. It consists of these components:
- Primary State: The main reactive state that the composable manages.
- State Metadata: States that hold values like API request status or errors.
- Methods: Functions that update the Primary State and State Metadata. These functions can call APIs, manage cookies, or integrate with other composables.
Following this structure makes your composables more intuitive and improves code quality across your project.
// Good Example: Anatomy of a Composable
// Well-structured according to Anatomy of a Composable
export function function useUserData(userId: any): {
user: any;
status: any;
error: any;
fetchUser: () => Promise<void>;
}
useUserData(userId: any
userId) {
// Primary State
const const user: any
user = ref(null);
// Supportive State
const const status: any
status = ref("idle");
const const error: any
error = ref(null);
// Methods
const const fetchUser: () => Promise<void>
fetchUser = async () => {
const status: any
status.value = "loading";
try {
const const response: any
response = await axios.get(`/api/users/${userId: any
userId}`);
const user: any
user.value = const response: any
response.data;
const status: any
status.value = "success";
} catch (function (local var) e: unknown
e) {
const status: any
status.value = "error";
const error: any
error.value = function (local var) e: unknown
e;
}
};
return { user: any
user, status: any
status, error: any
error, fetchUser: () => Promise<void>
fetchUser };
}
// Bad Example: Anatomy of a Composable
// Lacks well-defined structure and mixes concerns
export function function useUserDataAndMore(userId: any): {
user: any;
count: any;
message: any;
fetchUserAndIncrement: () => Promise<void>;
setMessage: (newMessage: any) => void;
}
useUserDataAndMore(userId: any
userId) {
// Muddled State: Not clear what's Primary or Supportive
const const user: any
user = ref(null);
const const count: any
count = ref(0);
const const message: any
message = ref("Initializing...");
// Methods: Multiple responsibilities and side-effects
const const fetchUserAndIncrement: () => Promise<void>
fetchUserAndIncrement = async () => {
const message: any
message.value = "Fetching user and incrementing count...";
try {
const const response: any
response = await axios.get(`/api/users/${userId: any
userId}`);
const user: any
user.value = const response: any
response.data;
} catch (function (local var) e: unknown
e) {
const message: any
message.value = "Failed to fetch user.";
}
const count: any
count.value++; // Incrementing count, unrelated to user fetching
};
// More Methods: Different kind of task entirely
const const setMessage: (newMessage: any) => void
setMessage = newMessage: any
newMessage => {
const message: any
message.value = newMessage: any
newMessage;
};
return { user: any
user, count: any
count, message: any
message, fetchUserAndIncrement: () => Promise<void>
fetchUserAndIncrement, setMessage: (newMessage: any) => void
setMessage };
}
Functional Core, Imperative Shell
Rule 8.2: (optional) use functional core imperative shell pattern
Structure your composable such that the core logic is functional and devoid of side effects, while the imperative shell handles the Vue-specific or side-effecting operations. Following this principle makes your composable easier to test, debug, and maintain.
Example: Functional Core, Imperative Shell
// good
// Functional Core
const const calculate: (a: any, b: any) => any
calculate = (a: any
a, b: any
b) => a: any
a + b: any
b;
// Imperative Shell
export function function useCalculatorGood(): {
result: any;
add: (a: any, b: any) => void;
}
useCalculatorGood() {
const const result: any
result = ref(0);
const const add: (a: any, b: any) => void
add = (a: any
a, b: any
b) => {
const result: any
result.value = const calculate: (a: any, b: any) => any
calculate(a: any
a, b: any
b); // Using the functional core
};
// Other side-effecting code can go here, e.g., logging, API calls
return { result: any
result, add: (a: any, b: any) => void
add };
}
// wrong
// Mixing core logic and side effects
export function function useCalculatorBad(): {
result: any;
add: (a: any, b: any) => void;
}
useCalculatorBad() {
const const result: any
result = ref(0);
const const add: (a: any, b: any) => void
add = (a: any
a, b: any
b) => {
// Side-effect within core logic
var console: Console
The `console` module provides a simple debugging console that is similar to the
JavaScript console mechanism provided by web browsers.
The module exports two specific components:
* A `Console` class with methods such as `console.log()`, `console.error()` and `console.warn()` that can be used to write to any Node.js stream.
* A global `console` instance configured to write to [`process.stdout`](https://nodejs.org/docs/latest-v22.x/api/process.html#processstdout) and
[`process.stderr`](https://nodejs.org/docs/latest-v22.x/api/process.html#processstderr). The global `console` can be used without importing the `node:console` module.
_**Warning**_: The global console object's methods are neither consistently
synchronous like the browser APIs they resemble, nor are they consistently
asynchronous like all other Node.js streams. See the [`note on process I/O`](https://nodejs.org/docs/latest-v22.x/api/process.html#a-note-on-process-io) for
more information.
Example using the global `console`:
```js
console.log('hello world');
// Prints: hello world, to stdout
console.log('hello %s', 'world');
// Prints: hello world, to stdout
console.error(new Error('Whoops, something bad happened'));
// Prints error message and stack trace to stderr:
// Error: Whoops, something bad happened
// at [eval]:5:15
// at Script.runInThisContext (node:vm:132:18)
// at Object.runInThisContext (node:vm:309:38)
// at node:internal/process/execution:77:19
// at [eval]-wrapper:6:22
// at evalScript (node:internal/process/execution:76:60)
// at node:internal/main/eval_string:23:3
const name = 'Will Robinson';
console.warn(`Danger ${name}! Danger!`);
// Prints: Danger Will Robinson! Danger!, to stderr
```
Example using the `Console` class:
```js
const out = getStreamSomehow();
const err = getStreamSomehow();
const myConsole = new console.Console(out, err);
myConsole.log('hello world');
// Prints: hello world, to out
myConsole.log('hello %s', 'world');
// Prints: hello world, to out
myConsole.error(new Error('Whoops, something bad happened'));
// Prints: [Error: Whoops, something bad happened], to err
const name = 'Will Robinson';
myConsole.warn(`Danger ${name}! Danger!`);
// Prints: Danger Will Robinson! Danger!, to err
```console.Console.log(message?: any, ...optionalParams: any[]): void (+1 overload)
Prints to `stdout` with newline. Multiple arguments can be passed, with the
first used as the primary message and all additional used as substitution
values similar to [`printf(3)`](http://man7.org/linux/man-pages/man3/printf.3.html)
(the arguments are all passed to [`util.format()`](https://nodejs.org/docs/latest-v22.x/api/util.html#utilformatformat-args)).
```js
const count = 5;
console.log('count: %d', count);
// Prints: count: 5, to stdout
console.log('count:', count);
// Prints: count: 5, to stdout
```
See [`util.format()`](https://nodejs.org/docs/latest-v22.x/api/util.html#utilformatformat-args) for more information.log("Adding:", a: any
a, b: any
b);
const result: any
result.value = a: any
a + b: any
b;
};
return { result: any
result, add: (a: any, b: any) => void
add };
}
Single Responsibility Principle
Rule 9.1: Use SRP for composables
A composable should follow the Single Responsibility Principle: one reason to change. This means each composable handles one specific task. Following this principle creates composables that are clear, maintainable, and testable.
// Good
export function function useCounter(): {
count: any;
increment: () => void;
decrement: () => void;
}
useCounter() {
const const count: any
count = ref(0);
const const increment: () => void
increment = () => {
const count: any
count.value++;
};
const const decrement: () => void
decrement = () => {
const count: any
count.value--;
};
return { count: any
count, increment: () => void
increment, decrement: () => void
decrement };
}
// Bad
export function function useUserAndCounter(userId: any): {
user: any;
fetchUser: () => Promise<void>;
count: any;
increment: () => void;
decrement: () => void;
}
useUserAndCounter(userId: any
userId) {
const const user: any
user = ref(null);
const const count: any
count = ref(0);
const const fetchUser: () => Promise<void>
fetchUser = async () => {
try {
const const response: any
response = await axios.get(`/api/users/${userId: any
userId}`);
const user: any
user.value = const response: any
response.data;
} catch (function (local var) error: unknown
error) {
var console: Console
The `console` module provides a simple debugging console that is similar to the
JavaScript console mechanism provided by web browsers.
The module exports two specific components:
* A `Console` class with methods such as `console.log()`, `console.error()` and `console.warn()` that can be used to write to any Node.js stream.
* A global `console` instance configured to write to [`process.stdout`](https://nodejs.org/docs/latest-v22.x/api/process.html#processstdout) and
[`process.stderr`](https://nodejs.org/docs/latest-v22.x/api/process.html#processstderr). The global `console` can be used without importing the `node:console` module.
_**Warning**_: The global console object's methods are neither consistently
synchronous like the browser APIs they resemble, nor are they consistently
asynchronous like all other Node.js streams. See the [`note on process I/O`](https://nodejs.org/docs/latest-v22.x/api/process.html#a-note-on-process-io) for
more information.
Example using the global `console`:
```js
console.log('hello world');
// Prints: hello world, to stdout
console.log('hello %s', 'world');
// Prints: hello world, to stdout
console.error(new Error('Whoops, something bad happened'));
// Prints error message and stack trace to stderr:
// Error: Whoops, something bad happened
// at [eval]:5:15
// at Script.runInThisContext (node:vm:132:18)
// at Object.runInThisContext (node:vm:309:38)
// at node:internal/process/execution:77:19
// at [eval]-wrapper:6:22
// at evalScript (node:internal/process/execution:76:60)
// at node:internal/main/eval_string:23:3
const name = 'Will Robinson';
console.warn(`Danger ${name}! Danger!`);
// Prints: Danger Will Robinson! Danger!, to stderr
```
Example using the `Console` class:
```js
const out = getStreamSomehow();
const err = getStreamSomehow();
const myConsole = new console.Console(out, err);
myConsole.log('hello world');
// Prints: hello world, to out
myConsole.log('hello %s', 'world');
// Prints: hello world, to out
myConsole.error(new Error('Whoops, something bad happened'));
// Prints: [Error: Whoops, something bad happened], to err
const name = 'Will Robinson';
myConsole.warn(`Danger ${name}! Danger!`);
// Prints: Danger Will Robinson! Danger!, to err
```console.Console.error(message?: any, ...optionalParams: any[]): void (+1 overload)
Prints to `stderr` with newline. Multiple arguments can be passed, with the
first used as the primary message and all additional used as substitution
values similar to [`printf(3)`](http://man7.org/linux/man-pages/man3/printf.3.html)
(the arguments are all passed to [`util.format()`](https://nodejs.org/docs/latest-v22.x/api/util.html#utilformatformat-args)).
```js
const code = 5;
console.error('error #%d', code);
// Prints: error #5, to stderr
console.error('error', code);
// Prints: error 5, to stderr
```
If formatting elements (e.g. `%d`) are not found in the first string then
[`util.inspect()`](https://nodejs.org/docs/latest-v22.x/api/util.html#utilinspectobject-options) is called on each argument and the
resulting string values are concatenated. See [`util.format()`](https://nodejs.org/docs/latest-v22.x/api/util.html#utilformatformat-args)
for more information.error("An error occurred while fetching user data:", function (local var) error: unknown
error);
}
};
const const increment: () => void
increment = () => {
const count: any
count.value++;
};
const const decrement: () => void
decrement = () => {
const count: any
count.value--;
};
return { user: any
user, fetchUser: () => Promise<void>
fetchUser, count: any
count, increment: () => void
increment, decrement: () => void
decrement };
}
File Structure of a Composable
Rule 10.1: Rule: Consistent Ordering of Composition API Features
Your team should establish and follow a consistent order for Composition API features throughout the codebase.
Here’s a recommended order:
- Initializing: Setup logic
- Refs: Reactive references
- Computed: Computed properties
- Methods: Functions for state manipulation
- Lifecycle Hooks: onMounted, onUnmounted, etc.
- Watch
Pick an order that works for your team and apply it consistently across all composables.
// Example in useCounter.ts
import { function ref<T>(value: T): [T] extends [Ref] ? IfAny<T, Ref<T>, T> : Ref<UnwrapRef<T>, UnwrapRef<T> | T> (+1 overload)
Takes an inner value and returns a reactive and mutable ref object, which
has a single property `.value` that points to the inner value.ref, const computed: {
<T>(getter: ComputedGetter<T>, debugOptions?: DebuggerOptions): ComputedRef<T>;
<T, S = T>(options: WritableComputedOptions<T, S>, debugOptions?: DebuggerOptions): WritableComputedRef<T, S>;
}
computed, const onMounted: CreateHook<any>
onMounted } from "vue";
export default function function useCounter(): {
count: Ref<number, number>;
isEven: ComputedRef<boolean>;
increment: () => void;
decrement: () => void;
}
useCounter() {
// Initializing
// Initialize variables, make API calls, or any setup logic
// For example, using a router
// ...
// Refs
const const count: Ref<number, number>
count = ref<number>(value: number): Ref<number, number> (+1 overload)
Takes an inner value and returns a reactive and mutable ref object, which
has a single property `.value` that points to the inner value.ref(0);
// Computed
const const isEven: ComputedRef<boolean>
isEven = computed<boolean>(getter: ComputedGetter<boolean>, debugOptions?: DebuggerOptions): ComputedRef<boolean> (+1 overload)
Takes a getter function and returns a readonly reactive ref object for the
returned value from the getter. It can also take an object with get and set
functions to create a writable ref object.computed(() => const count: Ref<number, number>
count.Ref<number, number>.value: number
value % 2 === 0);
// Methods
const const increment: () => void
increment = () => {
const count: Ref<number, number>
count.Ref<number, number>.value: number
value++;
};
const const decrement: () => void
decrement = () => {
const count: Ref<number, number>
count.Ref<number, number>.value: number
value--;
};
// Lifecycle
function onMounted(hook: any, target?: ComponentInternalInstance | null): void
onMounted(() => {
var console: Console
The `console` module provides a simple debugging console that is similar to the
JavaScript console mechanism provided by web browsers.
The module exports two specific components:
* A `Console` class with methods such as `console.log()`, `console.error()` and `console.warn()` that can be used to write to any Node.js stream.
* A global `console` instance configured to write to [`process.stdout`](https://nodejs.org/docs/latest-v22.x/api/process.html#processstdout) and
[`process.stderr`](https://nodejs.org/docs/latest-v22.x/api/process.html#processstderr). The global `console` can be used without importing the `node:console` module.
_**Warning**_: The global console object's methods are neither consistently
synchronous like the browser APIs they resemble, nor are they consistently
asynchronous like all other Node.js streams. See the [`note on process I/O`](https://nodejs.org/docs/latest-v22.x/api/process.html#a-note-on-process-io) for
more information.
Example using the global `console`:
```js
console.log('hello world');
// Prints: hello world, to stdout
console.log('hello %s', 'world');
// Prints: hello world, to stdout
console.error(new Error('Whoops, something bad happened'));
// Prints error message and stack trace to stderr:
// Error: Whoops, something bad happened
// at [eval]:5:15
// at Script.runInThisContext (node:vm:132:18)
// at Object.runInThisContext (node:vm:309:38)
// at node:internal/process/execution:77:19
// at [eval]-wrapper:6:22
// at evalScript (node:internal/process/execution:76:60)
// at node:internal/main/eval_string:23:3
const name = 'Will Robinson';
console.warn(`Danger ${name}! Danger!`);
// Prints: Danger Will Robinson! Danger!, to stderr
```
Example using the `Console` class:
```js
const out = getStreamSomehow();
const err = getStreamSomehow();
const myConsole = new console.Console(out, err);
myConsole.log('hello world');
// Prints: hello world, to out
myConsole.log('hello %s', 'world');
// Prints: hello world, to out
myConsole.error(new Error('Whoops, something bad happened'));
// Prints: [Error: Whoops, something bad happened], to err
const name = 'Will Robinson';
myConsole.warn(`Danger ${name}! Danger!`);
// Prints: Danger Will Robinson! Danger!, to err
```console.Console.log(message?: any, ...optionalParams: any[]): void (+1 overload)
Prints to `stdout` with newline. Multiple arguments can be passed, with the
first used as the primary message and all additional used as substitution
values similar to [`printf(3)`](http://man7.org/linux/man-pages/man3/printf.3.html)
(the arguments are all passed to [`util.format()`](https://nodejs.org/docs/latest-v22.x/api/util.html#utilformatformat-args)).
```js
const count = 5;
console.log('count: %d', count);
// Prints: count: 5, to stdout
console.log('count:', count);
// Prints: count: 5, to stdout
```
See [`util.format()`](https://nodejs.org/docs/latest-v22.x/api/util.html#utilformatformat-args) for more information.log("Counter is mounted");
});
return {
count: Ref<number, number>
count,
isEven: ComputedRef<boolean>
isEven,
increment: () => void
increment,
decrement: () => void
decrement,
};
}
Conclusion
These guidelines provide best practices for writing clean, testable, and efficient Vue 3 composables. They combine established software design principles with practical experience, though they aren’t exhaustive.
Programming blends art and science. As you develop with Vue, you’ll discover patterns that match your needs. Focus on maintaining a consistent, scalable, and maintainable codebase. Adapt these guidelines to fit your project’s requirements.
Share your ideas, improvements, and real-world examples in the comments. Your input helps evolve these guidelines into a better resource for the Vue community.