install
source · Clone the upstream repo
git clone https://github.com/Intense-Visions/harness-engineering
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/Intense-Visions/harness-engineering "$T" && mkdir -p ~/.claude/skills && cp -r "$T/agents/skills/claude-code/nuxt-state-management" ~/.claude/skills/intense-visions-harness-engineering-nuxt-state-management && rm -rf "$T"
manifest:
agents/skills/claude-code/nuxt-state-management/SKILL.mdsource content
Nuxt State Management
Share reactive state across components with SSR-safe useState and Pinia store hydration
When to Use
- You need state that persists across navigation without re-fetching (user session, cart, theme)
- You are seeing hydration mismatch errors caused by state divergence between server and client
- You want to use Pinia stores in a Nuxt SSR context with proper server-to-client serialization
- You need to share state between unrelated components without prop-drilling
Instructions
useState — built-in SSR-safe state:
- Use
instead ofuseState
for any state that must be consistent between server and client renders.ref
is keyed: the same key always returns the same reactive reference:useState
// composables/useCounter.ts export const useCounter = () => useState<number>('counter', () => 0);
<!-- pages/index.vue --> <script setup> const counter = useCounter(); </script> <template> <button @click="counter++">{{ counter }}</button> </template>
- The second argument to
is an initializer factory — it runs only once on the server and is never called again on the client (state is transferred via payload):useState
const user = useState<User | null>('current-user', () => null);
- Always provide a unique, descriptive key to avoid collisions across different composables:
// Prefer namespaced keys for large apps const cartItems = useState<CartItem[]>('cart:items', () => []); const cartTotal = useState<number>('cart:total', () => 0);
- Reset state on the server-per-request boundary using
— useful for user-specific data:clearNuxtState
// plugins/reset-state.ts export default defineNuxtPlugin(() => { addRouteMiddleware(() => { clearNuxtState(['cart:items', 'cart:total']); }); });
Pinia — structured stores:
- Install
and add it to@pinia/nuxt
inmodules
:nuxt.config.ts
// nuxt.config.ts export default defineNuxtConfig({ modules: ['@pinia/nuxt'], });
- Define stores using
— they are auto-imported when placed indefineStore
(withstores/
configured):imports.dirs
// stores/useAuthStore.ts export const useAuthStore = defineStore('auth', () => { const user = ref<User | null>(null); const isAuthenticated = computed(() => !!user.value); async function login(credentials: Credentials) { user.value = await $fetch('/api/auth/login', { method: 'POST', body: credentials }); } function logout() { user.value = null; } return { user, isAuthenticated, login, logout }; });
- Hydrate Pinia stores server-side using
within the store or in the page:useAsyncData
// Hydrate in page const authStore = useAuthStore(); await useAsyncData('auth', () => authStore.fetchCurrentUser());
- Use
in server plugins to initialize store state from server-side sources:pinia.state.value
// plugins/init-state.server.ts export default defineNuxtPlugin(async (nuxtApp) => { const authStore = useAuthStore(nuxtApp.$pinia); const sessionUser = await getSessionUser(useRequestEvent()); authStore.user = sessionUser; });
Details
Why
causes hydration mismatches:ref
A plain
ref in a composable creates a new reactive instance per component call. During SSR, the server renders with its own instance; on the client, a fresh ref initializes to the default value — causing a mismatch. useState solves this by storing state in Nuxt's SSR payload and rehydrating from it on the client.
useState vs. Pinia:
| Concern | useState | Pinia |
|---|---|---|
| Simple scalar/object state | Best fit | Overkill |
| Complex logic, actions, getters | Awkward | Best fit |
| DevTools time-travel | No | Yes |
| Plugin ecosystem | None | Rich |
| SSR safety | Built-in | Requires setup |
Serialization requirements:
State transferred via the SSR payload must be JSON-serializable. Do not store class instances, functions, or circular references in
useState or Pinia stores. Use plain objects and primitives.
Pinia store persistence (client-only):
Use
pinia-plugin-persistedstate for localStorage sync. Mark it client-only to avoid SSR issues:
// plugins/pinia-persist.client.ts import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'; export default defineNuxtPlugin(({ $pinia }) => { $pinia.use(piniaPluginPersistedstate); });
Avoiding state pollution between requests:
In SSR, all requests share the same module scope. Never use module-level
ref or reactive for per-user state — it leaks between requests. Always use useState (keyed per request) or Pinia (reset via $reset() in server middleware).
Source
https://nuxt.com/docs/getting-started/state-management
Process
- Read the instructions and examples in this document.
- Apply the patterns to your implementation, adapting to your specific context.
- Verify your implementation against the details and edge cases listed above.
Harness Integration
- Type: knowledge — this skill is a reference document, not a procedural workflow.
- No tools or state — consumed as context by other skills and agents.
Success Criteria
- The patterns described in this document are applied correctly in the implementation.
- Edge cases and anti-patterns listed in this document are avoided.