Composables: Composable Functions
Charlie's team has a lot of duplicate logic—price formatting is written separately in 10 components, and search logic is scattered across 3 pages. Alice's favorites feature needs to share state across pages. Bob needs a generic pagination logic. Composables are the solution—extract the logic into reusable functions.
1. What You'll Learn
- A Comprehensive Overview of Nuxt 3's Built-in Composables
- Custom Composables: Directory Conventions and Naming Guidelines
- Composable Design Pattern: Asynchronous / Parameter Reactivity / Return Value Destructuring
- Composable Composition: A Composable calls another Composable
- MegaShop: useProductSearch/usePriceFormat/useWishlist
2. A True Story of an Architect
(1) Pain Point: Duplicate logic scattered throughout the code
MegaShop has 10 components that need to format prices as "USD 2,999.99," and each one requires a custom Intl.NumberFormat implementation. Alice has implemented the "Favorites" feature on both the home page and the product detail page, but the logic is inconsistent—items favorited on the home page do not appear on the product detail page.
(2) The Composable Solution
Once extracted into a Composable, define it once and reuse it everywhere:
// composables/usePriceFormat.ts
export function usePriceFormat(price: Ref<number>, currency = 'USD') {
return computed(() => new Intl.NumberFormat('en-US', {
style: 'currency', currency
}).format(price.value))
}
(3) Benefits: DRY + Consistency
Ten components share the same price formatting logic, and the wishlist status remains consistent across all pages thanks to useWishlist. Code volume was reduced by 60%, and bugs were reduced by 80%.
3. Overview of Built-in Composables
(1) Built-in Composable Dependency Graph
graph TB
A[Nuxt Built-in Composables] --> B[Data Fetching]
A --> C[State Management]
A --> D[Navigation]
A --> E[Head Management]
A --> F[Context]
A --> G[Utility]
B --> B1[useFetch]
B --> B2[useAsyncData]
B --> B3[$fetch]
C --> C1[useState]
C --> C2[useCookie]
D --> D1[useRouter]
D --> D2[useRoute]
D --> D3[navigateTo]
E --> E1[useHead]
E --> E2[useSeoMeta]
F --> F1[useRequestHeaders]
F --> F2[useRuntimeConfig]
G --> G1[useAppConfig]
G --> G2[useHydration]
(2) Quick Reference for Core Built-in Composables
| Composable | Purpose | SSR Supported | Return Value |
|---|---|---|---|
| useFetch | Data Retrieval | ✅ | { data, pending, error, refresh } |
| useAsyncData | General Async | ✅ | { data, pending, error, refresh } |
| useState | Shared state | ✅ | Ref |
| useCookie | Cookie Operations | ✅ | Ref |
| useRouter | Router Instance | ⚠️ Client-side only | Router |
| useRoute | Current Route | ✅ | Route |
| useHead | Head Management | ✅ | void |
| useSeoMeta | SEO Metadata | ✅ | void |
| useRuntimeConfig | Runtime Configuration | ✅ | RuntimeConfig |
| useRequestHeaders | Request Headers | ✅ Server-side only | Headers |
(1) ▶ Example: Sharing State Across Components with useState
<script setup lang="ts">
// Share notification state across components
const notifications = useState<Notification[]>('notifications', () => [])
function addNotification(message: string, type: 'success' | 'error' = 'success') {
notifications.value.push({ id: Date.now(), message, type })
setTimeout(() => {
notifications.value = notifications.value.filter(n => n.id !== Date.now())
}, 3000)
}
</script>
Output:
// Execution Successful
4. Custom Composable
(1) Naming and Directory Conventions
| Rule | Description | Example |
|---|---|---|
| Table of Contents | composables/ | composables/useCart.ts |
| Name | use prefix | usePriceFormat |
| Export | Named Export | export function usePriceFormat() |
| Type | Interface defined in the same file | interface PriceOptions {} |
| Auto-import | ✅ Automatic | No manual import required |
(1) ▶ Example: usePriceFormat Price Formatting
// composables/usePriceFormat.ts
interface PriceFormatOptions {
currency?: string
locale?: string
showDecimals?: boolean
}
export function usePriceFormat(
price: Ref<number> | number,
options: PriceFormatOptions = {}
) {
const { currency = 'USD', locale = 'en-US', showDecimals = true } = options
const formatted = computed(() => {
const value = unref(price)
return new Intl.NumberFormat(locale, {
style: 'currency',
currency,
minimumFractionDigits: showDecimals ? 2 : 0,
maximumFractionDigits: showDecimals ? 2 : 0
}).format(value)
})
return { formatted }
}
Output:
// Execution Successful
(2) ▶ Example: useWishlist Favorites
// composables/useWishlist.ts
export function useWishlist() {
const wishlist = useState<number[]>('wishlist', () => [])
function toggle(productId: number) {
const index = wishlist.value.indexOf(productId)
if (index === -1) {
wishlist.value.push(productId)
} else {
wishlist.value.splice(index, 1)
}
}
function isWishlisted(productId: number): boolean {
return wishlist.value.includes(productId)
}
const count = computed(() => wishlist.value.length)
return { wishlist, toggle, isWishlisted, count }
}
Output:
// Execution Successful
(3) ▶ Example: useProductSearch Search Logic
// composables/useProductSearch.ts
interface SearchParams {
query?: string
category?: string
minPrice?: number
maxPrice?: number
sortBy?: 'price' | 'name' | 'rating'
page?: number
}
export function useProductSearch(initialParams: SearchParams = {}) {
const params = reactive<SearchParams>({
query: '',
category: '',
minPrice: 0,
maxPrice: 10000,
sortBy: 'rating',
page: 1,
...initialParams
})
const { data, pending, error, refresh } = useFetch('/api/products/search', {
query: params,
watch: [params],
default: () => ({ items: [], total: 0 })
})
function resetFilters() {
params.query = ''
params.category = ''
params.minPrice = 0
params.maxPrice = 10000
params.sortBy = 'rating'
params.page = 1
}
return { params, data, pending, error, refresh, resetFilters }
}
Output:
// Execution Successful
5. The Composable Design Pattern
(1) Comparison of Design Patterns
| Pattern | Characteristics | Use Cases | Examples |
|---|---|---|---|
| Sync Composable | Return computed/ref | Formatting/Computation | usePriceFormat |
| Asynchronous Composable | Internal call to useFetch | Data retrieval | useProductSearch |
| Parameter Responsiveness | Automatic Refresh via watch |
Filter/Search | useProductSearch |
| State Sharing | Inside useState | Cross-Component State | useWishlist |
| Composable | Calling Other Composables | Complex Logic | useCheckout |
(1) ▶ Example: Composable—useCheckout
// composables/useCheckout.ts
export function useCheckout() {
// Compose other composables
const cartStore = useCartStore()
const { formatted: total } = usePriceFormat(
computed(() => cartStore.totalPrice)
)
const { isAuthenticated, user } = storeToRefs(useUserStore())
const isProcessing = ref(false)
const orderId = ref<string | null>(null)
async function processCheckout() {
if (!isAuthenticated.value) {
navigateTo('/login')
return
}
isProcessing.value = true
try {
const order = await $fetch('/api/orders', {
method: 'POST',
body: {
items: cartStore.items,
userId: user.value?.id,
total: cartStore.totalPrice
}
})
orderId.value = order.id
cartStore.clearCart()
navigateTo(`/orders/${order.id}`)
} catch (err) {
console.error('Checkout failed:', err)
} finally {
isProcessing.value = false
}
}
return { total, isProcessing, orderId, processCheckout, isAuthenticated }
}
Output:
// Execution Successful
(2) Composable Design Principle
| Principle | Explanation | Counterexample |
|---|---|---|
| Single Responsibility | One Composable does one thing | useShopAndCart() |
| Parameter is responsive | Accepts Ref parameters | Accepts only raw values |
| Return Value Destructuring | Return Named Object | Return Array |
| Clear Naming | use + verb/noun | doStuff() |
| No side effects | Does not directly modify the DOM | Operates on the DOM internally |
6. Comprehensive Example: The MegaShop Composable Architecture
// composables/usePagination.ts
export function usePagination(totalItems: Ref<number>, pageSize = 20) {
const route = useRoute()
const router = useRouter()
const currentPage = computed(() => Number(route.query.page) || 1)
const totalPages = computed(() => Math.ceil(totalItems.value / pageSize))
const hasNext = computed(() => currentPage.value < totalPages.value)
const hasPrev = computed(() => currentPage.value > 1)
function goToPage(page: number) {
router.push({ query: { ...route.query, page: String(page) } })
}
function nextPage() {
if (hasNext.value) goToPage(currentPage.value + 1)
}
function prevPage() {
if (hasPrev.value) goToPage(currentPage.value - 1)
}
const offset = computed(() => (currentPage.value - 1) * pageSize)
return { currentPage, totalPages, hasNext, hasPrev, offset, goToPage, nextPage, prevPage }
}
// composables/useNotification.ts
interface Notification { id: number; message: string; type: 'success' | 'error' | 'info' }
export function useNotification() {
const notifications = useState<Notification[]>('notifications', () => [])
function notify(message: string, type: Notification['type'] = 'success') {
const id = Date.now()
notifications.value = [...notifications.value, { id, message, type }]
setTimeout(() => {
notifications.value = notifications.value.filter(n => n.id !== id)
}, 3000)
}
function dismiss(id: number) {
notifications.value = notifications.value.filter(n => n.id !== id)
}
return { notifications, notify, dismiss }
}
❓ FAQ
useFetch in a Composable?await when calling it, just like you would in a page. useProductSearch is designed this way.composables/ directory and does not require the "use" prefix. However, the Vue community convention is to start them with "use" for easier identification and tool support.useState in Composable?useState is automatically hydrated during SSR and on the client side, making it suitable for sharing across components. useWishlist uses useState for storage.unref() to support both types.useState is used inside the Composable, the state is shared. If ref or reactive is used, a separate state is created each time the Composable is called. The choice depends on your needs.📖 Summary
- Nuxt 3 includes over 20 built-in Composables: useFetch, useState, useCookie, useRouter, and more
- Place custom Composables in the
composables/directory, name them with theuseprefix, and they will be automatically imported. - Design Patterns: Synchronous (formatting), Asynchronous (data retrieval), Reactive (search), Composite (checkout)
- A Composable can call other Composables to implement layered complex logic
- MegaShop builds its business logic layer using usePriceFormat, useWishlist, useProductSearch, and useCheckout
📝 Exercises
- Basic Exercise (Difficulty: ⭐): Create a
usePriceFormatComposable to format prices as USD/JPY/CNY in different components. - Advanced Exercise (Difficulty: ⭐⭐): Create
useProductSearchto implement keyword search, category filtering, and pagination. Verify that the page automatically refreshes when thewatchparameter changes. - Challenge (Difficulty: ⭐⭐⭐): Create a
useCheckoutcomposite Composable that integrates the shopping cart Store, price formatting, and order submission to implement a complete checkout process.
---|



