One common use for Vue 3 composable functions is to act as a global state store. However, they can also be useful without a global state.
A stateless composable doesn't mean it has no state, but rather that the state lives inside the function and is recreated on each execution.
Stateless Composable
Here’s useAsync
, a composable for handling async calls (part of the Composition API Workshop).
import { ref, type UnwrapRef } from 'vue';
export async function useAsync<T>(fn: () => Promise<T>) {
const loading = ref(false);
const error = ref<Error | null>(null);
const result = ref<T | null>(null);
try {
result.value = (await fn()) as UnwrapRef<T>;
} catch (e) {
if (e instanceof Error) {
error.value = e;
}
} finally {
loading.value = false;
}
return { loading, error, result };
}
The reactive state properties loading
, error
, and result
live inside useAsync
, meaning they are recreated with each execution and are not shared.
Stateful Composable
Now, suppose we want to add an option to log all errors and provide a function to clear them. This functionality should live outside useAsync
to be global and shared.
import { ref, type UnwrapRef } from 'vue';
// Global state
export const errors = ref<string[]>([]);
export const resetErrors = () => (errors.value = []);
interface Options {
recordError?: boolean;
}
export async function useAsync<T>(fn: () => Promise<T>, options: Options = {}) {
const { recordError = true } = options;
const loading = ref(false);
const error = ref<Error | null>(null);
const result = ref<T | null>(null);
try {
result.value = (await fn()) as UnwrapRef<T>;
} catch (e) {
if (e instanceof Error) {
error.value = e;
recordError && errors.value.push(e.message);
}
} finally {
loading.value = false;
}
return { loading, error, result };
}
Here, errors
and resetErrors
live outside useAsync
, but are consumed inside. This is ideal for globally tracking all failed operations.
Conclusion
A composable can have both global and local reactive state. The key is the scope: global (stateful) vs local (stateless).