0

I have the issue of hydration action not being run, resulting in my redux state in getServerSideProps to be initial instead of the current state that is on the client side

This is my set up for the store it uses redux-persist, react-redux, redux-toolkit,next-redux-wrapper and redux-thunk:

"@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",
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>
>;

Ignore the setStore, its a way of getting rid of dependency cycle error.

Here is my _app.tsx file: This is where i set up my PersistGate, as well as provider for the store and finally use the useWrappedStore, to provide a wrapped store for the whole application

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;

As an example here is a reducer for a feature of auth, that stores a token after we log in and handles errror if we recieve any during the request to login:

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

Here is the part where the error shows up:

Here is a code for the homePage, when i access the site with a token stored in the redux, in the getServerSideProps it returns to me the initialState that we have for the reducer (I have tested this because i have changed the initialState and that's what gets returned)

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()); // THIS IS ALWAYS THE INITIAL STATE
    console.log(isLoggedIn);

    if (!isLoggedIn) {
      return {
        redirect: {
          destination: LOGIN,
          permanent: false,
        },
      };
    }
    // await store.dispatch(getUser());

    return {
      props: {},
    };
  }
);

export default HomePage;

I believe that the problem is that the hydration action does not run at all, meaning that the server side is only provided with the initialState and its not fed the previous state from client.

Any help on how i could fix this or where i went wrong would be greatly appreciated

toster
  • 1
  • 2

1 Answers1

0

If you do SSR, you should never have a module-scoped store variable like you have it here. That will create one store on the server and share it between all users making requests to your server.
You have to move all of that logic into the makeStore function.
As a result, you also cannot import your store into any other files: you can only ever access it through React Context from within your components - and if you have functions outside of that that need the store, you will have to rewrite those to thunks and dispatch them, or explicitly call them with the store as an argument.

For your question itself: that code in getServerSideProps will run on the server, not on the client - and the server has no way of knowing the Redux state on the client. In your case it shares the store with all other users, but once you fix that, you will have a completely new empty store on every render. You can only move data from the server to the client and hydrate it there. It doesn't work the other way round.

phry
  • 35,762
  • 5
  • 67
  • 81