DEV Community

Cover image for State Hydration & Persisted State in Nuxt with Pinia
Jakub Andrzejewski
Jakub Andrzejewski

Posted on

State Hydration & Persisted State in Nuxt with Pinia

When working with Nuxt (or any SSR framework), one of the trickiest parts is handling state hydration — making sure that the state you generate on the server matches what runs on the client. If these diverge, you’ll often see hydration warnings, flickering UI, or even broken interactivity.

In this article, we’ll explore:

  • What state hydration means in SSR
  • Common pitfalls developers face
  • How to persist state across SSR and client-side navigation with Pinia and TypeScript
  • Practical fixes and patterns you can adopt

Enjoy!

🤔 State Hydration and common pitfalls

When a page is rendered on the server, Nuxt sends HTML to the browser. Then, Vue takes over and hydrates that markup with client-side JavaScript, attaching reactivity and event listeners. If the client-side state doesn’t match what was rendered on the server, Vue will complain:

[Vue warn]: Hydration completed but contains mismatches.
Enter fullscreen mode Exit fullscreen mode

This happens a lot with dynamic state, especially when using stores.

Common Pitfalls

a) Using Browser-Only APIs in Server Context

import { defineStore } from 'pinia'

export const useUserStore = defineStore('user', {
  state: () => ({
    theme: localStorage.getItem('theme') || 'light', // ❌ breaks in SSR
  }),
})
Enter fullscreen mode Exit fullscreen mode

On the server, localStorage doesn’t exist, so Nuxt either errors out or renders a fallback value. Then on the client, the value changes → hydration mismatch.

b) Non-Deterministic Data

Anything that changes between server and client — e.g., Date.now(), Math.random() — will cause mismatches if used directly in state.

c) Forgetting to Persist State Between Navigations

If your store resets on each navigation, you’ll lose state (like user preferences, auth tokens, or cart data).

🟢 Using Pinia with Nuxt

Nuxt comes with first-class Pinia support via the @pinia/nuxt module. This takes care of serializing and deserializing store state between server and client.

Install:

npm install pinia @pinia/nuxt
Enter fullscreen mode Exit fullscreen mode

Add to nuxt.config.ts:

export default defineNuxtConfig({
  modules: [
    '@pinia/nuxt',
  ],
  pinia: {
    autoImports: ['defineStore', 'storeToRefs'],
  },
})
Enter fullscreen mode Exit fullscreen mode

4. Persisting State with Plugins

Pinia itself doesn’t persist state automatically. But you can add a plugin:

// plugins/persistedState.client.ts
import { defineNuxtPlugin } from '#app'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'

export default defineNuxtPlugin(({ $pinia }) => {
  $pinia.use(piniaPluginPersistedstate)
})
Enter fullscreen mode Exit fullscreen mode

Then in your store:

export const useUserStore = defineStore('user', {
  state: () => ({
    theme: 'light' as 'light' | 'dark',
  }),
  persist: true, // 👈 stored in localStorage by default
})
Enter fullscreen mode Exit fullscreen mode

Now your state survives page reloads and client-side navigation.


5. Avoiding SSR Mismatches with TypeScript

When defining state with TypeScript, always provide initial values that are safe for SSR:

export const useCartStore = defineStore('cart', {
  state: () => ({
    items: [] as { id: string; qty: number }[],
    initialized: false,
  }),
  actions: {
    init() {
      if (process.client) {
        this.items = JSON.parse(localStorage.getItem('cart') || '[]')
      }
      this.initialized = true
    },
  },
})
Enter fullscreen mode Exit fullscreen mode

Key takeaways:

  • Always check process.client before using browser APIs.
  • Initialize state with SSR-safe defaults.
  • Hydrate additional data only after the client takes over.

✅ Best Practices

  1. Use SSR-safe defaults for all store state.
  2. Wrap client-only logic in if (process.client) blocks.
  3. Persist critical state (auth, preferences, cart) with pinia-plugin-persistedstate.
  4. Consider server-side fetching for initial state, then hydrate client-only extras later.
  5. Use TypeScript to strictly type your stores, avoiding accidental undefined or any values that cause mismatch.

📖 Learn more

If you would like to learn more about Vue, Nuxt, JavaScript or other useful technologies, checkout VueSchool by clicking this link or by clicking the image below:

Vue School Link

It covers most important concepts while building modern Vue or Nuxt applications that can help you in your daily work or side projects 😉

🧪 Advance skills

A certification boosts your skills, builds credibility, and opens doors to new opportunities. Whether you're advancing your career or switching paths, it's a smart step toward success.

Check out Certificates.dev by clicking this link or by clicking the image below:

Certificates.dev Link

Invest in yourself—get certified in Vue.js, JavaScript, Nuxt, Angular, React, and more!

✅ Summary

State hydration is one of the most subtle pitfalls of SSR in Nuxt. By combining Pinia, Nuxt’s SSR integration, and TypeScript’s type safety, you can avoid hydration mismatches and ensure your app behaves consistently across server and client.

Next time you see [Vue warn]: Hydration completed but contains mismatches, check your store initialization and persistence setup first — chances are, the fix is there.

Take care!

And happy coding as always 🖥️

Top comments (0)