From fc3efe6a27a3f4a28919cae33aef6d7e3679e8e5 Mon Sep 17 00:00:00 2001 From: hailin Date: Sun, 11 Jan 2026 20:47:00 -0800 Subject: [PATCH] =?UTF-8?q?fix(mining-admin-web):=20=E4=BF=AE=E5=A4=8D=20R?= =?UTF-8?q?eact=20hydration=20=E9=94=99=E8=AF=AF=20#418=20#423?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修改 Zustand sidebar store 使用 skipHydration 避免 SSR 不匹配 - 移除 Redux auth slice 初始状态中的 localStorage 读取 - 在 providers 中使用 useEffect 初始化客户端状态 Co-Authored-By: Claude Opus 4.5 --- frontend/mining-admin-web/src/app/providers.tsx | 15 ++++++++++++++- .../src/store/slices/auth.slice.ts | 12 ++++++++++-- .../src/store/zustand/use-sidebar.ts | 11 ++++++++++- 3 files changed, 34 insertions(+), 4 deletions(-) diff --git a/frontend/mining-admin-web/src/app/providers.tsx b/frontend/mining-admin-web/src/app/providers.tsx index a8861d90..c6b88be3 100644 --- a/frontend/mining-admin-web/src/app/providers.tsx +++ b/frontend/mining-admin-web/src/app/providers.tsx @@ -2,9 +2,21 @@ import { Provider as ReduxProvider } from 'react-redux'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import { useState } from 'react'; +import { useState, useEffect } from 'react'; import { store } from '@/store'; import { Toaster } from '@/components/ui/toaster'; +import { useSidebar } from '@/store/zustand/use-sidebar'; +import { initializeAuth } from '@/store/slices/auth.slice'; + +function HydrationHandler() { + useEffect(() => { + // 客户端初始化 zustand 持久化状态 + useSidebar.persist.rehydrate(); + // 客户端初始化 auth token + store.dispatch(initializeAuth()); + }, []); + return null; +} export function Providers({ children }: { children: React.ReactNode }) { const [queryClient] = useState( @@ -22,6 +34,7 @@ export function Providers({ children }: { children: React.ReactNode }) { return ( + {children} diff --git a/frontend/mining-admin-web/src/store/slices/auth.slice.ts b/frontend/mining-admin-web/src/store/slices/auth.slice.ts index 504105c0..d4348d7f 100644 --- a/frontend/mining-admin-web/src/store/slices/auth.slice.ts +++ b/frontend/mining-admin-web/src/store/slices/auth.slice.ts @@ -17,7 +17,7 @@ interface AuthState { const initialState: AuthState = { user: null, - token: typeof window !== 'undefined' ? localStorage.getItem('admin_token') : null, + token: null, isAuthenticated: false, loading: false, error: null, @@ -62,6 +62,14 @@ const authSlice = createSlice({ setToken: (state, action: PayloadAction) => { state.token = action.payload; }, + initializeAuth: (state) => { + if (typeof window !== 'undefined') { + const token = localStorage.getItem('admin_token'); + if (token) { + state.token = token; + } + } + }, }, extraReducers: (builder) => { builder @@ -97,5 +105,5 @@ const authSlice = createSlice({ }, }); -export const { clearError, setToken } = authSlice.actions; +export const { clearError, setToken, initializeAuth } = authSlice.actions; export default authSlice.reducer; diff --git a/frontend/mining-admin-web/src/store/zustand/use-sidebar.ts b/frontend/mining-admin-web/src/store/zustand/use-sidebar.ts index 7c2a9aad..0b016c9c 100644 --- a/frontend/mining-admin-web/src/store/zustand/use-sidebar.ts +++ b/frontend/mining-admin-web/src/store/zustand/use-sidebar.ts @@ -1,21 +1,30 @@ import { create } from 'zustand'; -import { persist } from 'zustand/middleware'; +import { persist, createJSONStorage } from 'zustand/middleware'; interface SidebarState { isCollapsed: boolean; + _hasHydrated: boolean; toggle: () => void; setCollapsed: (collapsed: boolean) => void; + setHasHydrated: (state: boolean) => void; } export const useSidebar = create()( persist( (set) => ({ isCollapsed: false, + _hasHydrated: false, toggle: () => set((state) => ({ isCollapsed: !state.isCollapsed })), setCollapsed: (collapsed) => set({ isCollapsed: collapsed }), + setHasHydrated: (state) => set({ _hasHydrated: state }), }), { name: 'sidebar-state', + storage: createJSONStorage(() => localStorage), + skipHydration: true, + onRehydrateStorage: () => (state) => { + state?.setHasHydrated(true); + }, } ) );