From harness-claude
Manages SSR-safe reactive state in Nuxt using useState for persistence across navigation and Pinia stores with hydration to fix mismatches and share state without prop-drilling.
npx claudepluginhub intense-visions/harness-engineering --plugin harness-claudeThis skill uses the workspace's default tool permissions.
> Share reactive state across components with SSR-safe useState and Pinia store hydration
Guides Nuxt 5 data management with useFetch, useAsyncData, useState, and Pinia for creating composables, fetching data, managing state, and debugging reactive/SSR issues.
Guides Nuxt 4 data fetching with useFetch/useAsyncData, state management via useState/Pinia, and custom composables for SSR-safe reactive patterns.
Guides Pinia v3 setup for Vue 3 state management with defineStore, getters, actions, option/setup syntaxes, Nuxt SSR, Vuex migration, hydration, and testing.
Share bugs, ideas, or general feedback.
Share reactive state across components with SSR-safe useState and Pinia store hydration
useState — built-in SSR-safe state:
useState instead of ref for any state that must be consistent between server and client renders. useState is keyed: the same key always returns the same reactive reference:// 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>
useState is an initializer factory — it runs only once on the server and is never called again on the client (state is transferred via payload):const user = useState<User | null>('current-user', () => null);
// Prefer namespaced keys for large apps
const cartItems = useState<CartItem[]>('cart:items', () => []);
const cartTotal = useState<number>('cart:total', () => 0);
clearNuxtState — useful for user-specific data:// plugins/reset-state.ts
export default defineNuxtPlugin(() => {
addRouteMiddleware(() => {
clearNuxtState(['cart:items', 'cart:total']);
});
});
Pinia — structured stores:
@pinia/nuxt and add it to modules in nuxt.config.ts:// nuxt.config.ts
export default defineNuxtConfig({
modules: ['@pinia/nuxt'],
});
defineStore — they are auto-imported when placed in stores/ (with imports.dirs configured):// 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 };
});
useAsyncData within the store or in the page:// Hydrate in page
const authStore = useAuthStore();
await useAsyncData('auth', () => authStore.fetchCurrentUser());
pinia.state.value in server plugins to initialize store state from server-side sources:// plugins/init-state.server.ts
export default defineNuxtPlugin(async (nuxtApp) => {
const authStore = useAuthStore(nuxtApp.$pinia);
const sessionUser = await getSessionUser(useRequestEvent());
authStore.user = sessionUser;
});
Why ref causes hydration mismatches:
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).
https://nuxt.com/docs/getting-started/state-management