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