fix(mining-admin-web): 修复 React hydration 错误 #418 #423

- 修改 Zustand sidebar store 使用 skipHydration 避免 SSR 不匹配
- 移除 Redux auth slice 初始状态中的 localStorage 读取
- 在 providers 中使用 useEffect 初始化客户端状态

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
hailin 2026-01-11 20:47:00 -08:00
parent dc27044dab
commit fc3efe6a27
3 changed files with 34 additions and 4 deletions

View File

@ -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 (
<ReduxProvider store={store}>
<QueryClientProvider client={queryClient}>
<HydrationHandler />
{children}
<Toaster />
</QueryClientProvider>

View File

@ -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<string>) => {
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;

View File

@ -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<SidebarState>()(
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);
},
}
)
);