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

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;

问题分析

  1. getServerSideProps运行在服务端,无法访问客户端localStorage,redux-persist的持久化数据仅存在于客户端
  2. HYDRATE动作是服务端向客户端同步渲染状态,并非反向同步客户端状态到服务端
  3. 当前服务端使用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配置

  1. 修改auth reducer的HYDRATE逻辑,避免客户端持久化状态被覆盖:
.addCase(HYDRATE, (state, action) => {
  return {
    ...state,
    // 仅当服务端有有效Token时才覆盖客户端状态
    ...(action.payload.auth.token ? action.payload.auth : {}),
  };
});
  1. 使用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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.26 01:57:36