Writing Custom Composables
Putting your own reusable logic in composables/ so it's auto-imported everywhere, the same way Nuxt's built-ins are.
阅读需 2 分钟
You've been using composables like useFetch and useState throughout this course — functions, prefixed with use, that bundle reactive state and logic together. Nuxt doesn't reserve that pattern for its own built-ins: any function you put in the composables/ directory gets the exact same auto-import treatment.
A composable is just a function
There's no special syntax or registration step. A composable is a regular function, conventionally starting with use, that can call other composables (including Vue's ref/computed and Nuxt's useFetch/useState) because it runs in the same reactive context as a component's <script setup>:
// composables/useCart.ts
export function useCart() {
const items = useState<CartItem[]>("cart-items", () => []);
const total = computed(() =>
items.value.reduce((sum, item) => sum + item.price * item.quantity, 0)
);
function addItem(item: CartItem) {
items.value.push(item);
}
function removeItem(id: string) {
items.value = items.value.filter((i) => i.id !== id);
}
return { items, total, addItem, removeItem };
}<script setup>
const { items, total, addItem } = useCart();
</script>
<template>
<p>{{ items.length }} items — ${{ total.toFixed(2) }}</p>
</template>Notice useCart uses useState internally rather than a plain ref — that's what makes the cart SSR-safe and shared across every component that calls useCart(), for the reasons covered in the previous lesson.
Why pull this out of the component at all
The logic above could live directly inside a component's <script setup>. Pulling it into a composable pays off the moment more than one component needs it — a cart icon in the header and a full cart page both calling useCart() share the exact same state and behavior, with no prop drilling or event emitting between them. It's the same motivation as extracting a function in any codebase: once two places need the same behavior, a shared function beats copy-pasting it.
Async setup in a composable
A composable can wrap a data-fetching composable too, giving a feature its own focused API instead of leaving useFetch calls scattered across components:
// composables/useUser.ts
export function useUser(userId: string) {
return useFetch(`/api/users/${userId}`, {
key: `user-${userId}`,
});
}<script setup>
const route = useRoute();
const { data: user, pending } = await useUser(route.params.id);
</script>Where the line is
Not everything needs to become a composable. If a piece of logic is genuinely only used in one component and isn't likely to be reused, leaving it inline is simpler to follow than jumping to a separate file. Reach for composables/ when state or logic needs to be shared — across components, or across more than one page — not as a default for every function you write.