Next.js 14中使用Zustand persist中间件导航后状态重置且出现hydration不匹配问题
Next.js 14中使用Zustand persist中间件导航后状态重置且出现hydration不匹配问题
我之前也遇到过几乎一模一样的问题,Next.js 14的SSR/SSG机制和Zustand persist的交互确实容易踩这些坑,咱们一步步拆解解决:
问题根源先理清楚
为啥会出现状态重置+hydration警告?核心原因有两个:
- SSR与客户端状态不一致:服务端渲染时没有
localStorage,Zustand只能用默认的null状态初始化;但客户端hydrate阶段会从localStorage读取之前保存的登录状态,这就导致服务端和客户端的初始渲染结果不匹配,触发警告甚至被React强制重置状态。 - 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) { // 执行重定向逻辑 }
额外避坑提示
- 不要在服务端组件里直接使用
useAuthStore,如果需要在服务端验证权限,应该用Next.js的Middleware(比如middleware.ts)来校验token,而不是依赖客户端store。 - 如果你在App Router的Layout中需要用auth状态,必须给Layout加上
'use client'指令,或者把状态相关逻辑拆分到子客户端组件中。
内容来源于stack exchange
相关产品推荐
相关产品推荐

