next-redux-wrapper中Hydration Action未执行导致getServerSideProps状态异常
Hydration Action未执行,getServerSideProps始终获取Redux初始状态
技术栈版本
"@reduxjs/toolkit": "^1.9.2", "next-redux-wrapper": "^8.1.0", "react-redux": "^8.0.5", "redux-persist": "^6.0.0", "redux-thunk": "^2.4.2",
Store配置代码
import type { Action, ThunkAction } from '@reduxjs/toolkit'; import { configureStore } from '@reduxjs/toolkit'; import { combineReducers } from 'redux'; import { FLUSH, PAUSE, PERSIST, persistReducer, PURGE, REGISTER, REHYDRATE, } from 'redux-persist'; import thunk from 'redux-thunk'; import { createWrapper, HYDRATE } from 'next-redux-wrapper'; import createWebStorage from 'redux-persist/lib/storage/createWebStorage'; import { alertsReducer as alerts } from '@/store/features/alert'; import { authReducer as auth } from '@/store/features/auth'; import { registrationReducer as registration } from '@/store/features/registration'; import { forgotPasswordReducer as forgotPassword } from '@/store/features/forgotPassword'; import { userReducer as user } from '@/store/features/user'; import { setStore } from './storeInjector'; const reducers = combineReducers({ registration, alerts, auth, forgotPassword, user, }); const createNoopStorage = () => { return { getItem(_key) { return Promise.resolve(null); }, setItem(_key, value) { return Promise.resolve(value); }, removeItem(_key) { return Promise.resolve(); }, }; }; const storage = typeof window !== 'undefined' ? createWebStorage('local') : createNoopStorage(); const persistConfig = { key: 'root', storage, timeout: 100, whitelist: ['registration', 'auth', 'forgotPassword', 'user'], }; const persistedReducer = persistReducer(persistConfig, reducers); export const store = configureStore({ reducer: persistedReducer, devTools: process.env.NODE_ENV !== 'production', middleware: (getDefaultMiddleware) => getDefaultMiddleware({ serializableCheck: { ignoredActions: [FLUSH, PAUSE, PERSIST, PURGE, REGISTER], }, }).concat(thunk), }); setStore(store); export const wrapper = createWrapper(() => store); export type AppDispatch = typeof store.dispatch; export type RootState = ReturnType<typeof store.getState>; export type AppThunk<ReturnType = void> = ThunkAction< ReturnType, RootState, unknown, Action<string> >;
_app.tsx配置
import type { AppProps } from 'next/app'; import { Provider } from 'react-redux'; import { PersistGate } from 'redux-persist/integration/react'; import { ThemeProvider } from 'styled-components'; import { persistStore } from 'redux-persist'; import { Alerts } from '@/components/Alerts'; import { GlobalStyle, theme } from '@/styles'; import { store, wrapper } from '@/store'; function MyApp({ Component, pageProps }: AppProps) { const { store: wrappedStore } = wrapper.useWrappedStore(store); const persistedStore = persistStore(store); return ( <ThemeProvider theme={theme}> <GlobalStyle /> <Provider store={wrappedStore}> <PersistGate loading={null} persistor={persistedStore}> <Component {...pageProps} /> <Alerts /> </PersistGate> </Provider> </ThemeProvider> ); } export default MyApp;
auth reducer示例
import type { PayloadAction } from '@reduxjs/toolkit'; import { createReducer } from '@reduxjs/toolkit'; import { HYDRATE } from 'next-redux-wrapper'; import type { LoginValues } from './actions'; import { logoutUser, loginUser } from './actions'; interface AuthState { token: string | null; pending: boolean; error: boolean; } const initialState: AuthState = { token: null, pending: false, error: false, }; export const authReducer = createReducer(initialState, (builder) => { builder .addCase(logoutUser, (state) => { state.token = initialState.token; }) .addCase(loginUser.pending, (state) => { state.pending = true; }) .addCase( loginUser.fulfilled, ( state, action: PayloadAction< { token: string }, string, { arg: LoginValues; requestId: string; requestStatus: 'fulfilled' } > ) => { state.pending = false; state.error = false; if (action.payload.token) { state.token = action.payload.token; } } ) .addCase(loginUser.rejected, (state) => { state.pending = false; state.error = true; }) .addCase(HYDRATE, (state, action) => { console.log('hydrate', action); return { ...state, ...action.payload.auth, }; }); });
问题复现场景
首页的getServerSideProps中,即使客户端Redux已存储token,但获取到的始终是初始状态:
import { LOGIN } from '@/constants/routes'; import { Home } from '@/routes/Home'; import { wrapper } from '@/store'; import { selectAuthToken } from '@/store/features/auth'; import { getUser } from '@/store/features/user'; const HomePage = () => <Home />; export const getServerSideProps = wrapper.getServerSideProps( (store) => async (_) => { const isLoggedIn = selectAuthToken(store.getState()); // 始终为初始状态 console.log(isLoggedIn); if (!isLoggedIn) { return { redirect: { destination: LOGIN, permanent: false, }, }; } // await store.dispatch(getUser()); return { props: {}, }; } ); export default HomePage;
问题分析
getServerSideProps运行在服务端,无法访问客户端localStorage,redux-persist的持久化数据仅存在于客户端HYDRATE动作是服务端向客户端同步渲染状态,并非反向同步客户端状态到服务端- 当前服务端使用noopStorage,完全无法读取客户端的持久化数据
解决方案
核心思路:用Cookie传递认证信息
服务端无法直接读取客户端localStorage,Cookie是前后端共享的存储方式,适合传递认证token。
步骤1:登录成功后将Token存入Cookie
// auth/actions.ts import { createAsyncThunk } from '@reduxjs/toolkit'; import Cookies from 'js-cookie'; export const loginUser = createAsyncThunk( 'auth/login', async (values: LoginValues) => { const response = await fetch('/api/login', { method: 'POST', body: JSON.stringify(values), headers: { 'Content-Type': 'application/json' }, }); const data = await response.json(); // 将Token存入Cookie,设置7天过期 Cookies.set('authToken', data.token, { expires: 7, path: '/' }); return data; } );
步骤2:在getServerSideProps中从Cookie读取Token
export const getServerSideProps = wrapper.getServerSideProps( (store) => async (context) => { // 从请求Cookie中获取认证Token const authToken = context.req?.cookies?.authToken; if (!authToken) { return { redirect: { destination: LOGIN, permanent: false, }, }; } // 将Token同步到服务端Redux状态(可选,用于后续逻辑) await store.dispatch(loginUser.fulfilled( { token: authToken }, '', { arg: {} as LoginValues, requestId: '', requestStatus: 'fulfilled' } )); // 可继续使用同步后的状态获取用户信息 // await store.dispatch(getUser()); return { props: {}, }; } );
步骤3:调整Redux和_app.tsx配置
- 修改auth reducer的HYDRATE逻辑,避免客户端持久化状态被覆盖:
.addCase(HYDRATE, (state, action) => { return { ...state, // 仅当服务端有有效Token时才覆盖客户端状态 ...(action.payload.auth.token ? action.payload.auth : {}), }; });
- 使用
wrapper.withRedux包装App组件(next-redux-wrapper v8推荐方式):
// 修改_app.tsx function MyApp({ Component, pageProps }: AppProps) { const persistedStore = persistStore(store); return ( <ThemeProvider theme={theme}> <GlobalStyle /> <Provider store={store}> <PersistGate loading={null} persistor={persistedStore}> <Component {...pageProps} /> <Alerts /> </PersistGate> </Provider> </ThemeProvider> ); } // 用wrapper.withRedux包装组件 export default wrapper.withRedux(MyApp);
内容的提问来源于stack exchange,提问作者toster
相关产品推荐
相关产品推荐

