Skill v1.0.0
currentAutomated scan100/100version: "1.0.0" name: nuxt-data description: "| Nuxt 4 data management: composables, data fetching with useFetch/useAsyncData, and state management with useState and Pinia. Use when: creating custom composables, fetching data with useFetch or useAsyncData, managing global state with useState, integrating Pinia, debugging reactive data issues, or implementing SSR-safe state patterns." license: MIT metadata: version: 4.0.0 author: Claude Skills Maintainers category: Framework framework: Nuxt framework-version: 4.x last-verified: 2025-12-28 keywords:
- useFetch
- useAsyncData
- $fetch
- useState
- composables
- Pinia
- data fetching
- state management
- reactive
- shallow reactivity
- reactive keys
- transform
- pending
- error
- refresh
- dedupe
- caching
Nuxt 4 Data Management
Composables, data fetching, and state management patterns for Nuxt 4 applications.
Quick Reference
Data Fetching Methods
| Method | Use Case | SSR | Caching | Reactive | |
|---|---|---|---|---|---|
useFetch | Simple API calls | Yes | Yes | Yes | |
useAsyncData | Custom async logic | Yes | Yes | Yes | |
$fetch | Client-side only, events | No | No | No |
Composable Naming
| Prefix | Purpose | Example | |
|---|---|---|---|
use | State/logic composable | useAuth, useCart | |
fetch | Data fetching only | fetchUsers (rare) |
When to Load References
Load `references/composables.md` when:
- Writing custom composables with complex state
- Debugging state management issues or memory leaks
- Implementing SSR-safe patterns with browser APIs
- Building authentication or complex state composables
- Understanding singleton vs per-call composable patterns
Load `references/data-fetching.md` when:
- Implementing API data fetching with reactive parameters
- Troubleshooting shallow vs deep reactivity issues
- Debugging data not refreshing when params change
- Implementing pagination, infinite scroll, or search
- Understanding transform functions, caching, or error handling
Load `references/pinia-integration.md` when:
- Setting up Pinia for complex state management
- Creating stores with getters and actions
- Integrating Pinia with SSR
- Persisting state across page reloads
Composables
useState - The Foundation
useState creates SSR-safe, shared reactive state that persists across component instances.
// composables/useCounter.tsexport const useCounter = () => {// Singleton - shared across all componentsconst count = useState('counter', () => 0)const increment = () => count.value++const decrement = () => count.value--const reset = () => count.value = 0return { count, increment, decrement, reset }}
useState vs ref - Critical Distinction
// CORRECT: Shared state (singleton pattern)export const useAuth = () => {const user = useState('auth-user', () => null) // Shared!return { user }}// WRONG: Creates new instance every call!export const useAuth = () => {const user = ref(null) // Not shared!return { user }}
Rule: Use useState for shared/global state. Use ref for local component state only.
Complete Authentication Composable
// composables/useAuth.tsexport const useAuth = () => {const user = useState<User | null>('auth-user', () => null)const isAuthenticated = computed(() => !!user.value)const isLoading = useState('auth-loading', () => false)const login = async (email: string, password: string) => {isLoading.value = truetry {const data = await $fetch('/api/auth/login', {method: 'POST',body: { email, password }})user.value = data.userreturn { success: true }} catch (error) {return { success: false, error: error.message }} finally {isLoading.value = false}}const logout = async () => {await $fetch('/api/auth/logout', { method: 'POST' })user.value = nullnavigateTo('/login')}const checkSession = async () => {if (import.meta.server) return // Skip on servertry {const data = await $fetch('/api/auth/session')user.value = data.user} catch {user.value = null}}return { user, isAuthenticated, isLoading, login, logout, checkSession }}
SSR-Safe Browser APIs
// composables/useLocalStorage.tsexport const useLocalStorage = <T>(key: string, defaultValue: T) => {const data = useState<T>(key, () => defaultValue)// Only access localStorage on clientif (import.meta.client) {const stored = localStorage.getItem(key)if (stored) {data.value = JSON.parse(stored)}// Watch and persist changeswatch(data, (newValue) => {localStorage.setItem(key, JSON.stringify(newValue))}, { deep: true })}return data}
Data Fetching
useFetch - Basic Usage
// Simple GET requestconst { data, error, pending, refresh } = await useFetch('/api/users')// With optionsconst { data: users } = await useFetch('/api/users', {method: 'GET',query: { limit: 10, offset: 0 },headers: { 'X-Custom-Header': 'value' }})
Reactive Parameters
<script setup lang="ts">const page = ref(1)const search = ref('')// Auto-refetches when page or search changesconst { data: users, pending } = await useFetch('/api/users', {query: {page,search,limit: 10}})// Or with computedconst query = computed(() => ({page: page.value,search: search.value,limit: 10}))const { data } = await useFetch('/api/users', { query })</script>
Transform Data
const { data: userNames } = await useFetch('/api/users', {transform: (users) => users.map(u => u.name)})// data.value is now string[] instead of User[]
Pick Specific Fields
const { data } = await useFetch('/api/user', {pick: ['id', 'name', 'email'] // Only these fields in payload})
useAsyncData - Custom Logic
// Multiple parallel requestsconst { data } = await useAsyncData('dashboard', async () => {const [users, posts, stats] = await Promise.all([$fetch('/api/users'),$fetch('/api/posts'),$fetch('/api/stats')])return { users, posts, stats }})// Access: data.value.users, data.value.posts, data.value.stats
Error Handling
const { data, error, status } = await useFetch('/api/users')// Check errorif (error.value) {console.error('Error:', error.value.message)console.error('Status:', error.value.statusCode)}// Status values: 'idle' | 'pending' | 'success' | 'error'if (status.value === 'error') {showError(error.value)}
Manual Refresh
const { data, refresh, execute } = await useFetch('/api/users', {immediate: false // Don't fetch on mount})// Fetch manuallyawait execute()// Refresh (re-fetch)await refresh()// Refresh with new paramsawait refresh({ dedupe: true })
Shallow vs Deep Reactivity (v4 Change)
// Nuxt 4 default: Shallow reactivityconst { data } = await useFetch('/api/user')data.value.name = 'New Name' // Won't trigger reactivity!// Enable deep reactivity for mutationsconst { data } = await useFetch('/api/user', {deep: true})data.value.name = 'New Name' // Now works!// Or refresh instead of mutatingconst { data, refresh } = await useFetch('/api/user')await $fetch('/api/user', { method: 'PATCH', body: { name: 'New Name' } })await refresh() // Re-fetch updated data
Caching and Deduplication
const { data } = await useFetch('/api/users', {key: 'users-list', // Custom cache keydedupe: 'cancel', // Cancel duplicate requestsgetCachedData: (key, nuxtApp) => {// Return cached data if validreturn nuxtApp.payload.data[key]}})
Lazy Loading Data
// useLazyFetch - Navigation happens immediately, data loads in backgroundconst { data, pending } = useLazyFetch('/api/users')// useLazyAsyncDataconst { data, pending } = useLazyAsyncData('users', () => $fetch('/api/users'))
$fetch - Client-Side Only
// In event handlers (not during SSR)const submitForm = async () => {const result = await $fetch('/api/submit', {method: 'POST',body: formData.value})}// In server routesexport default defineEventHandler(async (event) => {const externalData = await $fetch('https://api.example.com/data')return externalData})
State Management
useState Patterns
// Simple counterconst count = useState('count', () => 0)// Complex objectconst settings = useState('settings', () => ({theme: 'light',notifications: true,language: 'en'}))// Typed stateinterface User {id: stringname: stringemail: string}const user = useState<User | null>('user', () => null)
Shared Cart Example
// composables/useCart.tsinterface CartItem {id: stringname: stringprice: numberquantity: number}export const useCart = () => {const items = useState<CartItem[]>('cart-items', () => [])const total = computed(() =>items.value.reduce((sum, item) => sum + item.price * item.quantity, 0))const itemCount = computed(() =>items.value.reduce((sum, item) => sum + item.quantity, 0))const addItem = (product: Omit<CartItem, 'quantity'>) => {const existing = items.value.find(i => i.id === product.id)if (existing) {existing.quantity++} else {items.value.push({ ...product, quantity: 1 })}}const removeItem = (id: string) => {items.value = items.value.filter(i => i.id !== id)}const updateQuantity = (id: string, quantity: number) => {const item = items.value.find(i => i.id === id)if (item) {item.quantity = Math.max(0, quantity)if (item.quantity === 0) removeItem(id)}}const clearCart = () => {items.value = []}return { items, total, itemCount, addItem, removeItem, updateQuantity, clearCart }}
Pinia Integration
bun add pinia @pinia/nuxt
// nuxt.config.tsexport default defineNuxtConfig({modules: ['@pinia/nuxt']})// stores/auth.tsimport { defineStore } from 'pinia'export const useAuthStore = defineStore('auth', {state: () => ({user: null as User | null,token: null as string | null}),getters: {isAuthenticated: (state) => !!state.user,userName: (state) => state.user?.name ?? 'Guest'},actions: {async login(email: string, password: string) {const { user, token } = await $fetch('/api/auth/login', {method: 'POST',body: { email, password }})this.user = userthis.token = token},logout() {this.user = nullthis.token = null}}})// Usage in componentsconst authStore = useAuthStore()await authStore.login('user@example.com', 'password')console.log(authStore.userName)
Common Anti-Patterns
Using ref Instead of useState
// WRONG - Creates new instance every time!export const useAuth = () => {const user = ref(null) // Not sharedreturn { user }}// CORRECTexport const useAuth = () => {const user = useState('auth-user', () => null)return { user }}
Missing Error Handling
// WRONGconst { data } = await useFetch('/api/users')console.log(data.value.length) // Crashes if error!// CORRECTconst { data, error } = await useFetch('/api/users')if (error.value) {showToast({ type: 'error', message: error.value.message })return}console.log(data.value.length)
Non-Deterministic Transform
// WRONG - Causes hydration mismatch!const { data } = await useFetch('/api/users', {transform: (users) => users.sort(() => Math.random() - 0.5)})// CORRECTconst { data } = await useFetch('/api/users', {transform: (users) => users.sort((a, b) => a.name.localeCompare(b.name))})
Mutating Shallow Refs
// WRONG - v4 uses shallow refs by defaultconst { data } = await useFetch('/api/user')data.value.name = 'New Name' // Won't trigger reactivity!// CORRECT - Option 1: Enable deepconst { data } = await useFetch('/api/user', { deep: true })data.value.name = 'New Name'// CORRECT - Option 2: Replace entire valuedata.value = { ...data.value, name: 'New Name' }// CORRECT - Option 3: Refresh after mutationawait $fetch('/api/user', { method: 'PATCH', body: { name: 'New Name' } })await refresh()
Troubleshooting
Data Not Refreshing When Params Change:
- Ensure params are reactive:
{ query: { page } }wherepage = ref(1) - Check you're using the ref itself, not
.value
Hydration Mismatch with useState:
- Ensure key is unique:
useState('unique-key', () => value) - Avoid
Math.random()orDate.now()in initial values
State Lost on Navigation:
- Use
useStateinstead ofreffor persistent state - Check you're using the same key across components
Infinite Refetch Loop:
- Check for reactive dependencies in transform function
- Use
watchwith{ immediate: false }for side effects
Related Skills
- nuxt-core: Project setup, routing, configuration
- nuxt-server: Server routes, API patterns
- nuxt-production: Performance, testing, deployment
Version: 4.0.0 | Last Updated: 2025-12-28 | License: MIT