Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to properly hydrate Redux state in NextJS on page refresh?

The problem I'm having is hydrating the user data from local storage on app reload/page refresh.

In my project I am using NextJS for frontend, and for support libraries I am using redux-toolkit for redux management across the application and next-redux-wrapper for state hydration for the wrapped pages.

The user can log in, and in that case I store the isLoggedIn boolean in local storage and in the redux state. Depending on the isLoggedIn boolean value I change the Navbar component styles (the Navbar is included directly in _app.tsx).

When the user refreshes any page the isLoggedIn boolean is not loaded into the state but is present in local storage.

In the past I have been using redux-persist but I have opted out of using it because the PersistGate was blocking the UI from rendering until the persisted data is fetched from storage which conflicts with the idea of SSR.

Currently I have the isLoggedIn loading problem fixed by using the App.getInitialProps method in _app.ts which then results in hydration from next-redux-persist being called for each and every page loaded, but this introduces another problem: all pages are now server side rendered and there is no NextJS' static page optimisation.

Is there any way to not lose static page optimisation from NextJS, not use the redux-persist library and still be able to hydrate the client side store when any page is refreshed?

Current code structure (some code is omitted for simplicity):

file: _app.tsx
import { wrapper } from 'store';

const MyApp = ({ Component, pageProps }: AppProps) => {
  return (
    <>
      <Navbar />
      <Component {...pageProps} />
    </>
  );
};


MyApp.getInitialProps = async (appContext) => {
  const appProps = await App.getInitialProps(appContext);

  return { ...appProps };
};

export default wrapper.withRedux(MyApp);
file: store.ts
import {
  combineReducers,
  configureStore,
  EnhancedStore,
  getDefaultMiddleware
} from '@reduxjs/toolkit';
import { createWrapper, MakeStore } from 'next-redux-wrapper';
import userReducer from 'lib/slices/userSlice';

const rootReducer = combineReducers({
  user: userReducer
});

const setupStore = (context): EnhancedStore => {
  const middleware = [...getDefaultMiddleware(), thunkMiddleware];

  if (process.env.NODE_ENV === 'development') {
    middleware.push(logger);
  }

  return configureStore({
    reducer: rootReducer,
    middleware,
    // preloadedState,
    devTools: process.env.NODE_ENV === 'development'
  });
};

const makeStore: MakeStore = (context) => setupStore(context);
export const wrapper = createWrapper(makeStore, {
  debug: process.env.NODE_ENV === 'development'
});
file: userSlice.ts
import { createSlice } from '@reduxjs/toolkit';
import { HYDRATE } from 'next-redux-wrapper';

const initialState = {
  isLoggedIn: false
}

export const userSlice = createSlice({
  name: 'user',
  initialState,
  reducers: {
    login: (state) => {
      state.isLoggedIn = true;
      localStorage.setItem('loggedInData', { isLoggedIn: true });
    }
  },
  extraReducers: (builder) => {
    builder
      .addCase(HYDRATE, (state, action: any) => {
        if (typeof window !== 'undefined') {
          const storedLoggedInData = localStorage.getItem('loggedInData');
          if (storedLoggedInData != null && storedLoggedInData) {
            const parsedJson = JSON.parse(storedLoggedInData);
            state.isLoggedIn = parsedJson.isLoggedIn ?? false;
          } else {
            state.isLoggedIn = false
          }
        }
      });
  }
});

export const isLoggedInSelector = (state: RootState) => state.user.isLoggedIn;

export default userSlice.reducer;
file: Navbar.tsx
import { useSelector } from 'react-redux';
import { isLoggedInSelector } from 'lib/slices/userSlice';

export default function Navbar() {
   const isLoggedIn = useSelector(isLoggedInSelector);
   return (
    <div className={`${ isLoggedIn ? 'logged-in-style' : 'logged-out-style'}`}>...</div>
   )
}
like image 487
Lazar Avatar asked Nov 12 '20 14:11

Lazar


People also ask

How do I keep redux state on refresh?

When we refresh page in a web-app, the state always resets back to the initial values which in not a good thing when you try to build some large web-app like e-commerce. We can manually do the state persistent using the native JavaScript localStorage.

How do you hydrate redux state?

Hydrating the state after the store was created, can be achieved by creating a main reducer that can bypass the top level reducers, and replace the whole state. Reducers are functions that get the current state, combine it with the payload of an action, and return a new state.

What is hydrate in next redux wrapper?

hydration is a process of filling an object with some data. So we are filling the client side store with the server-side store data.

How do you persist state in redux?

If you would like to persist your redux state across a browser refresh, it's best to do this using redux middleware. Check out the redux-persist and redux-storage middleware. They both try to accomplish the same task of storing your redux state so that it may be saved and loaded at will.


2 Answers

Had the same issue today. The problem is that we need to decouple the client storage from the page rendering and move it into useEffect where the component is mounted. The basic idea is, you first render the page fully and then update the page using the clients storage information.

Directly incorporating the clients local storage can/will interfere with the hydration.

Here is a code sample that i use

export default const MenuBar = () => {
  const isLoggedIn = useSelector((state) => state.isLoggedIn);
  useEffect(() => {
    // loads from clients local storage
    const auth = loadAuthenticationToken(); 
    if (auth !== null) {
      // updates the store with local storage data
      dispatch(actions.jwtTokenUpdate(auth)); 
    }
  }, [dispatch]);
  
  if (isLoggedIn) {
    return <p>you are logged in</p>;
  } else {
    return <p>please log in</p>;
  }
}

For reference, a github issue on NextJS: https://github.com/vercel/next.js/discussions/17443

And a blog post where window access for rendering is required: https://dev.to/adrien/creating-a-custom-react-hook-to-get-the-window-s-dimensions-in-next-js-135k

like image 174
Iwan1993 Avatar answered Sep 28 '22 10:09

Iwan1993


It is mentioned in the doc that if you use getInitialProps in _app.js, you will lose the static optimization. I don't know why you are using redux on the server side, personally I will advise you to only use it on the client side and you won't need to use next-redux-wrapper anymore because it uses getInitialProps under the hood.

The example with redux-toolkit

like image 37
enoch Avatar answered Sep 28 '22 08:09

enoch