Files
gh-jezweb-claude-skills-ski…/references/nextjs-hydration.md
2025-11-30 08:25:53 +08:00

5.0 KiB

Zustand + Next.js Hydration Guide

Complete guide for using Zustand with Next.js SSR. Load for Next.js-specific problems.


The Hydration Problem

Error: "Text content does not match server-rendered HTML"

Why it happens:

  • Server renders with default state
  • Client loads persisted state from localStorage
  • Content doesn't match → hydration error

Solution: Hydration Flag Pattern

Store Setup

'use client'

import { create } from 'zustand'
import { persist } from 'zustand/middleware'

interface Store {
  _hasHydrated: boolean  // Track hydration
  count: number
  setHasHydrated: (hydrated: boolean) => void
  increment: () => void
}

export const useStore = create<Store>()(
  persist(
    (set) => ({
      _hasHydrated: false,
      count: 0,
      setHasHydrated: (hydrated) => set({ _hasHydrated: hydrated }),
      increment: () => set((state) => ({ count: state.count + 1 })),
    }),
    {
      name: 'storage',
      onRehydrateStorage: () => (state) => {
        state?.setHasHydrated(true)  // Set true when done
      },
    },
  ),
)

Component Usage

'use client'

function Component() {
  const hasHydrated = useStore((state) => state._hasHydrated)
  const count = useStore((state) => state.count)

  if (!hasHydrated) {
    return <div>Loading...</div>
  }

  return <div>Count: {count}</div>
}

Alternative: Accept Flash

If loading state is unacceptable, accept brief flash:

'use client'

function Component() {
  const count = useStore((state) => state.count)

  // Will show default (0) briefly, then correct value
  return <div>Count: {count}</div>
}

Trade-off: Simpler code, but user sees flash.


App Router vs Pages Router

// app/layout.tsx
export default function RootLayout({ children }) {
  return (
    <html>
      <body>{children}</body>
    </html>
  )
}

// app/page.tsx
'use client'

import { useStore } from './store'

export default function Page() {
  return <Component />
}

Note: Add 'use client' to components using Zustand.

Pages Router

// pages/_app.tsx
export default function App({ Component, pageProps }) {
  return <Component {...pageProps} />
}

// pages/index.tsx
import { useStore } from '../store'

export default function Page() {
  return <Component />
}

Note: No 'use client' needed in Pages Router.


Server Components

NEVER use Zustand in Server Components:

// ❌ WRONG - Server Component
export default async function ServerComponent() {
  const count = useStore((state) => state.count)  // Error!
  return <div>{count}</div>
}

// ✅ CORRECT - Client Component
'use client'

export default function ClientComponent() {
  const count = useStore((state) => state.count)
  return <div>{count}</div>
}

SSR with getServerSideProps

// pages/index.tsx
import { GetServerSideProps } from 'next'

export const getServerSideProps: GetServerSideProps = async () => {
  // Fetch data on server
  const data = await fetchData()

  return {
    props: { data },
  }
}

export default function Page({ data }) {
  const setData = useStore((state) => state.setData)

  // Sync server data to store
  useEffect(() => {
    setData(data)
  }, [data, setData])

  return <Component />
}

Common Patterns

Initialize Store from Props

'use client'

function Page({ serverData }) {
  const setData = useStore((state) => state.setData)

  useEffect(() => {
    setData(serverData)
  }, [serverData, setData])

  return <Component />
}

Conditional Rendering

'use client'

function Component() {
  const hasHydrated = useStore((state) => state._hasHydrated)
  const theme = useStore((state) => state.theme)

  return (
    <div className={hasHydrated ? theme : 'default'}>
      {hasHydrated ? <Content /> : <Skeleton />}
    </div>
  )
}

Progressive Enhancement

'use client'

function Component() {
  const hasHydrated = useStore((state) => state._hasHydrated)
  const preferences = useStore((state) => state.preferences)

  // Always render, but enhance after hydration
  return (
    <div>
      <DefaultUI />
      {hasHydrated && <EnhancedUI preferences={preferences} />}
    </div>
  )
}

Debugging Hydration Errors

Enable Strict Mode

// next.config.js
module.exports = {
  reactStrictMode: true,
}

Check for Differences

useEffect(() => {
  console.log('Server state:', defaultState)
  console.log('Client state:', persistedState)
}, [])

Use React DevTools

Look for red warnings about hydration mismatches.


Official Resources