| 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-28T00:00:00.000Z","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.
export const useCounter = () => {
const count = useState('counter', () => 0)
const increment = () => count.value++
const decrement = () => count.value--
const reset = () => count.value = 0
return { count, increment, decrement, reset }
}
useState vs ref - Critical Distinction
export const useAuth = () => {
const user = useState('auth-user', () => null)
return { user }
}
export const useAuth = () => {
const user = ref(null)
return { user }
}
Rule: Use useState for shared/global state. Use ref for local component state only.
Complete Authentication Composable
export 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 = true
try {
const data = await $fetch('/api/auth/login', {
method: 'POST',
body: { email, password }
})
user.value = data.user
return { success: true }
} catch (error) {
return { success: false, error: error.message }
} finally {
isLoading.value = false
}
}
const = () => {
$fetch(, { : })
user. =
()
}
= () => {
(..)
{
data = $fetch()
user. = data.
} {
user. =
}
}
{ user, isAuthenticated, isLoading, login, logout, checkSession }
}
SSR-Safe Browser APIs
export const useLocalStorage = <T>(key: string, defaultValue: T) => {
const data = useState<T>(key, () => defaultValue)
if (import.meta.client) {
const stored = localStorage.getItem(key)
if (stored) {
data.value = JSON.parse(stored)
}
watch(data, (newValue) => {
localStorage.setItem(key, JSON.stringify(newValue))
}, { deep: true })
}
return data
}
Data Fetching
useFetch - Basic Usage
const { data, error, pending, refresh } = await useFetch('/api/users')
const { 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 changes
const { data: users, pending } = await useFetch('/api/users', {
query: {
page,
search,
limit: 10
}
})
// Or with computed
const 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)
})
Pick Specific Fields
const { data } = await useFetch('/api/user', {
pick: ['id', 'name', 'email']
})
useAsyncData - Custom Logic
const { 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 }
})
Error Handling
const { data, error, status } = await useFetch('/api/users')
if (error.value) {
console.error('Error:', error.value.message)
console.error('Status:', error.value.statusCode)
}
if (status.value === 'error') {
showError(error.value)
}
Manual Refresh
const { data, refresh, execute } = await useFetch('/api/users', {
immediate: false
})
await execute()
await refresh()
await refresh({ dedupe: true })
Shallow vs Deep Reactivity (v4 Change)
const { data } = await useFetch('/api/user')
data.value.name = 'New Name'
const { data } = await useFetch('/api/user', {
deep: true
})
data.value.name = 'New Name'
const { data, refresh } = await useFetch('/api/user')
await $fetch('/api/user', { method: 'PATCH', body: { name: 'New Name' } })
await refresh()
Caching and Deduplication
const { data } = await useFetch('/api/users', {
key: 'users-list',
dedupe: 'cancel',
getCachedData: (key, nuxtApp) => {
return nuxtApp.payload.data[key]
}
})
Lazy Loading Data
const { data, pending } = useLazyFetch('/api/users')
const { data, pending } = useLazyAsyncData('users', () => $fetch('/api/users'))
$fetch - Client-Side Only
const submitForm = async () => {
const result = await $fetch('/api/submit', {
method: 'POST',
body: formData.value
})
}
export default defineEventHandler(async (event) => {
const externalData = await $fetch('https://api.example.com/data')
return externalData
})
State Management
useState Patterns
const count = useState('count', () => 0)
const settings = useState('settings', () => ({
theme: 'light',
notifications: true,
language: 'en'
}))
interface User {
id: string
name: string
email: string
}
const user = useState<User | null>('user', () => null)
Shared Cart Example
interface CartItem {
id: string
name: string
price: number
quantity: 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.++
} {
items..({ ...product, : })
}
}
= () => {
items. = items..( i. !== id)
}
= () => {
item = items..( i. === id)
(item) {
item. = .(, quantity)
(item. === ) (id)
}
}
= () => {
items. = []
}
{ items, total, itemCount, addItem, removeItem, updateQuantity, clearCart }
}
Pinia Integration
bun add pinia @pinia/nuxt
export default defineNuxtConfig({
modules: ['@pinia/nuxt']
})
import { 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 = user
. = token
},
() {
. =
. =
}
}
})
authStore = ()
authStore.(, )
.(authStore.)
Common Anti-Patterns
Using ref Instead of useState
export const useAuth = () => {
const user = ref(null)
return { user }
}
export const useAuth = () => {
const user = useState('auth-user', () => null)
return { user }
}
Missing Error Handling
const { data } = await useFetch('/api/users')
console.log(data.value.length)
const { 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
const { data } = await useFetch('/api/users', {
transform: (users) => users.sort(() => Math.random() - 0.5)
})
const { data } = await useFetch('/api/users', {
transform: (users) => users.sort((a, b) => a.name.localeCompare(b.name))
})
Mutating Shallow Refs
const { data } = await useFetch('/api/user')
data.value.name = 'New Name'
const { data } = await useFetch('/api/user', { deep: true })
data.value.name = 'New Name'
data.value = { ...data.value, name: 'New Name' }
await $fetch('/api/user', { method: 'PATCH', body: { name: 'New Name' } })
await refresh()
Troubleshooting
Data Not Refreshing When Params Change:
- Ensure params are reactive:
{ query: { page } } where page = 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() or Date.now() in initial values
State Lost on Navigation:
- Use
useState instead of ref for persistent state
- Check you're using the same key across components
Infinite Refetch Loop:
- Check for reactive dependencies in transform function
- Use
watch with { 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