Nuxt 5 data management with useFetch, useAsyncData, useState, and Pinia. Use when creating composables, fetching data, managing state, or debugging reactive/SSR data issues.
npx skills add https://github.com/secondsky/claude-skills --skill nuxt-data
Composables, data fetching, and state management patterns for Nuxt 5 applications.
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.
| 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 |
Load references/composables.md when:
Load references/data-fetching.md when:
useState creates SSR-safe, shared reactive state that persists across component instances.
// composables/useCounter.ts
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 }
}
// CORRECT: Shared state (singleton pattern)
export const useAuth = () => {
const user = useState('auth-user', () => null)
return { user }
}
// WRONG: Creates new instance every call!
export const useAuth = () => {
const user = ref(null)
return { user }
}
Rule: Use useState for shared/global state. Use ref for local component state only.
// Nuxt 5: clearNuxtState resets state to its initial value, not undefined
const count = useState('counter', () => 42)
count.value = 100
clearNuxtState('counter')
// count.value is now 42 (the initial default), not undefined
// composables/useAuth.ts
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 logout = async () => {
await $fetch('/api/auth/logout', { method: 'POST' })
user.value = null
navigateTo('/login')
}
const checkSession = async () => {
if (import.meta.server) return
try {
const data = await $fetch('/api/auth/session')
user.value = data.user
} catch {
user.value = null
}
}
return { user, isAuthenticated, isLoading, login, logout, checkSession }
}
// composables/useLocalStorage.ts
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
}
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' }
})
<script setup lang="ts">
const page = ref(1)
const search = ref('')
const { data: users, pending } = await useFetch('/api/users', {
query: {
page,
search,
limit: 10
}
})
</script>
const { data: userNames } = await useFetch('/api/users', {
transform: (users) => users.map(u => u.name)
})
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 }
})
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)
}
const { data, refresh, execute } = await useFetch('/api/users', {
immediate: false
})
await execute()
await refresh()
await refresh({ dedupe: true })
// Nuxt 5 default: Shallow reactivity
const { data } = await useFetch('/api/user')
data.value.name = 'New Name' // Won't trigger reactivity!
// Enable deep reactivity for mutations
const { data } = await useFetch('/api/user', {
deep: true
})
data.value.name = 'New Name' // Now works!
// Or refresh instead of mutating
const { data, refresh } = await useFetch('/api/user')
await $fetch('/api/user', { method: 'PATCH', body: { name: 'New Name' } })
await refresh()
const { data } = await useFetch('/api/users', {
key: 'users-list',
dedupe: 'cancel',
getCachedData: (key, nuxtApp) => {
return nuxtApp.payload.data[key]
}
})
const { data, pending } = useLazyFetch('/api/users')
const { data: data2, pending: pending2 } = useLazyAsyncData('users', () => $fetch('/api/users'))
const submitForm = async () => {
const result = await $fetch('/api/submit', {
method: 'POST',
body: formData.value
})
}
// composables/useCart.ts
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 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 clearCart = () => {
items.value = []
}
return { items, total, addItem, removeItem, clearCart }
}
bun add pinia @pinia/nuxt
// nuxt.config.ts
export default defineNuxtConfig({
modules: ['@pinia/nuxt']
})
// stores/auth.ts
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
this.token = token
},
logout() {
this.user = null
this.token = null
}
}
})
// WRONG
export const useAuth = () => {
const user = ref(null)
return { user }
}
// CORRECT
export const useAuth = () => {
const user = useState('auth-user', () => null)
return { user }
}
// WRONG - Causes hydration mismatch!
const { data } = await useFetch('/api/users', {
transform: (users) => users.sort(() => Math.random() - 0.5)
})
// CORRECT
const { data } = await useFetch('/api/users', {
transform: (users) => users.sort((a, b) => a.name.localeCompare(b.name))
})
// WRONG
const { data } = await useFetch('/api/user')
data.value.name = 'New Name'
// CORRECT - Enable deep
const { data } = await useFetch('/api/user', { deep: true })
// CORRECT - Replace entire value
data.value = { ...data.value, name: 'New Name' }
// CORRECT - Refresh after mutation
await $fetch('/api/user', { method: 'PATCH', body: { name: 'New Name' } })
await refresh()
Data Not Refreshing When Params Change:
{ query: { page } } where page = ref(1).valueHydration Mismatch with useState:
useState('unique-key', () => value)Math.random() or Date.now() in initial valuesState Lost on Navigation:
useState instead of ref for persistent stateclearNuxtState not working as expected:
undefinedVersion: 5.0.0 | Last Updated: 2026-03-30 | License: MIT
Integration with protocols.io API for managing scientific protocols. This skill should be used when working with protocols.io to search, create, update, or publish protocols; manage protocol steps and materials; handle discussions and comments; organize workspaces; upload and manage files; or integrate protocols.io functionality into workflows. Applicable for protocol discovery, collaborative protocol development, experiment tracking, lab protocol management, and scientific documentation.
Analyzes job descriptions and generates tailored resumes that highlight relevant experience, skills, and achievements to maximize interview chances
Generate Excalidraw diagrams from natural language descriptions. Use when asked to "create a diagram", "make a flowchart", "visualize a process", "draw a system architecture", "create a mind map", or "generate an Excalidraw file". Supports flowcharts, relationship diagrams, mind maps, and system architecture diagrams. Outputs .excalidraw JSON files that can be opened directly in Excalidraw.
Build and distribute Expo development clients locally or via TestFlight
Use when you have a written implementation plan to execute in a separate session with review checkpoints
Data structure for annotated matrices in single-cell analysis. Use when working with .h5ad files or integrating with the scverse ecosystem. This is the data format skill—for analysis workflows use scanpy; for probabilistic models use scvi-tools; for population-scale queries use cellxgene-census.
Benchling R&D platform integration. Access registry (DNA, proteins), inventory, ELN entries, workflows via API, build Benchling Apps, query Data Warehouse, for lab data management automation.
Comprehensive molecular biology toolkit. Use for sequence manipulation, file parsing (FASTA/GenBank/PDB), phylogenetics, and programmatic NCBI/PubMed access (Bio.Entrez). Best for batch processing, custom bioinformatics pipelines, BLAST automation. For quick lookups use gget; for multi-service integration use bioservices.
Take secondsky/claude-nuxt-data from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.