Harness-engineering nuxt-state-management

Nuxt State Management

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.md
source 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:

  1. Use
    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>
  1. The second argument to
    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);
  1. 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);
  1. Reset state on the server-per-request boundary using
    clearNuxtState
    — useful for user-specific data:
// plugins/reset-state.ts
export default defineNuxtPlugin(() => {
  addRouteMiddleware(() => {
    clearNuxtState(['cart:items', 'cart:total']);
  });
});

Pinia — structured stores:

  1. Install
    @pinia/nuxt
    and add it to
    modules
    in
    nuxt.config.ts
    :
// nuxt.config.ts
export default defineNuxtConfig({
  modules: ['@pinia/nuxt'],
});
  1. Define stores using
    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 };
});
  1. Hydrate Pinia stores server-side using
    useAsyncData
    within the store or in the page:
// Hydrate in page
const authStore = useAuthStore();
await useAsyncData('auth', () => authStore.fetchCurrentUser());
  1. Use
    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;
});

Details

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:

ConcernuseStatePinia
Simple scalar/object stateBest fitOverkill
Complex logic, actions, gettersAwkwardBest fit
DevTools time-travelNoYes
Plugin ecosystemNoneRich
SSR safetyBuilt-inRequires 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

  1. Read the instructions and examples in this document.
  2. Apply the patterns to your implementation, adapting to your specific context.
  3. 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.