You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Next.js 14中使用Zustand persist中间件导航后状态重置且出现hydration不匹配问题

Next.js 14中使用Zustand persist中间件导航后状态重置且出现hydration不匹配问题

我之前也遇到过几乎一模一样的问题,Next.js 14的SSR/SSG机制和Zustand persist的交互确实容易踩这些坑,咱们一步步拆解解决:


问题根源先理清楚

为啥会出现状态重置+hydration警告?核心原因有两个:

  1. SSR与客户端状态不一致:服务端渲染时没有localStorage,Zustand只能用默认的null状态初始化;但客户端hydrate阶段会从localStorage读取之前保存的登录状态,这就导致服务端和客户端的初始渲染结果不匹配,触发警告甚至被React强制重置状态。
  2. Hooks调用违规/组件环境错误:如果你在服务端组件里直接调用了useAuthStore,或者在组件中条件性调用Hooks,就会触发Hooks顺序错误的警告,同时状态逻辑也会混乱。

具体解决方案(附代码修改)

1. 给Store加上use client指令,锁死客户端运行

Next.js 14 App Router的组件默认是服务端组件,你的store文件必须开头加上'use client',否则服务端执行时会直接报错或状态异常。

修改你的auth.store.ts:

'use client'; // 这行必须加!确保整个store在客户端环境运行

import { create } from 'zustand';
import { AuthTokens, User } from '../types/auth.types';
import { login as loginApi, register as registerApi, refresh as refreshApi, getMe } from '../features/auth/auth.api';
import { persist, createJSONStorage } from 'zustand/middleware';

2. 给Store添加Hydration状态,避免渲染不匹配

在Store中新增hasHydrated状态,标记persist的状态是否已经从localStorage恢复完成。这样页面可以等hydration完成后再使用状态,避免误判用户未登录。

修改后的完整Store代码:

'use client';

import { create } from 'zustand';
import { AuthTokens, User } from '../types/auth.types';
import { login as loginApi, register as registerApi, refresh as refreshApi, getMe } from '../features/auth/auth.api';
import { persist, createJSONStorage } from 'zustand/middleware';

interface AuthState {
  user: User | null;
  tokens: AuthTokens | null;
  hasHydrated: boolean; // 新增:标记rehydration是否完成
  setUser: (user: User | null) => void;
  setTokens: (tokens: AuthTokens | null) => void;
  setHasHydrated: (state: boolean) => void; // 新增:更新hydration状态
  logout: () => void;
  login: (creds: { username: string; pass: string }) => Promise<void>;
  register: (creds: { username: string; pass: string }) => Promise<void>;
  refresh: () => Promise<void>;
}

export const useAuthStore = create<AuthState>()(
  persist(
    (set, get) => ({
      user: null,
      tokens: null,
      hasHydrated: false,
      setUser: (user) => set({ user }),
      setTokens: (tokens) => set({ tokens }),
      setHasHydrated: (state) => set({ hasHydrated: state }),
      logout: () => set({ user: null, tokens: null }),
      login: async (creds) => {
        const tokens = await loginApi(creds);
        set({ tokens });
        const user = await getMe(tokens.access_token);
        set({ user });
      },
      register: async (creds) => {
        const tokens = await registerApi(creds);
        set({ tokens });
        const user = await getMe(tokens.access_token);
        set({ user });
      },
      refresh: async () => {
        const { tokens } = get();
        if (!tokens?.refresh_token) throw new Error('No refresh token');
        const newTokens = await refreshApi({ refreshToken: tokens.refresh_token });
        set({ tokens: newTokens });
        const user = await getMe(newTokens.access_token);
        set({ user });
      },
    }),
    {
      name: 'auth',
      partialize: (state) => ({ tokens: state.tokens, user: state.user }),
      // 新增:rehydration完成后更新状态标记
      onRehydrateStorage: () => (state) => {
        if (state) state.setHasHydrated(true);
      },
      // 适配SSR:服务端时返回空存储,避免报错
      storage: createJSONStorage(() => {
        if (typeof window === 'undefined') {
          return {
            getItem: async () => null,
            setItem: async () => {},
            removeItem: async () => {},
          };
        }
        return localStorage;
      }),
    }
  )
);

3. 受保护页面要等Hydration完成再判断权限

之前导航后被重定向,本质是页面在hydration完成前就读取了初始的tokens: null状态,误以为用户未登录。现在用hasHydrated做判断:

比如你的受保护页面dashboard/page.tsx:

'use client'; // 必须是客户端组件才能调用useAuthStore

import { useAuthStore } from '@/stores/auth.store';
import { useEffect, useState } from 'react';
import { useRouter } from 'next/navigation';

export default function Dashboard() {
  const { hasHydrated, tokens } = useAuthStore();
  const router = useRouter();
  const [isLoading, setIsLoading] = useState(true);

  useEffect(() => {
    if (hasHydrated) {
      setIsLoading(false);
      // 真正判断是否登录:hydration完成后再读取tokens
      if (!tokens) router.push('/error/unauthorized');
    }
  }, [hasHydrated, tokens, router]);

  // Hydration未完成时显示加载态,避免渲染不匹配
  if (isLoading) return <div>加载中...</div>;

  return (
    <div className="p-4">
      <h1>欢迎回来,{useAuthStore.getState().user?.username}!</h1>
    </div>
  );
}

4. 修复Hooks顺序警告

你提到的React has detected a change in the order of Hooks警告,90%是因为条件性调用Hooks。比如:

// ❌ 错误示例:条件里调用Hooks,会打乱顺序
if (hasHydrated) {
  const tokens = useAuthStore(state => state.tokens);
}

// ✅ 正确示例:先统一调用Hooks,再做逻辑判断
const { hasHydrated, tokens } = useAuthStore();
if (hasHydrated && !tokens) {
  // 执行重定向逻辑
}

额外避坑提示

  1. 不要在服务端组件里直接使用useAuthStore,如果需要在服务端验证权限,应该用Next.js的Middleware(比如middleware.ts)来校验token,而不是依赖客户端store。
  2. 如果你在App Router的Layout中需要用auth状态,必须给Layout加上'use client'指令,或者把状态相关逻辑拆分到子客户端组件中。

内容来源于stack exchange

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.07 07:42:59