Lazar
Lazar

Reputation: 690

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

Upvotes: 10

Views: 20347

Answers (3)

Wesley Janse
Wesley Janse

Reputation: 1784

Also a note, next-redux-wrapper uses getInitialProps on the app.js/ts when you wrap it with your store. So when you use this library and wrap your app with it you will automatically lose static optimization.

Upvotes: 0

Iwan1993
Iwan1993

Reputation: 1769

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

Upvotes: 3

enoch
enoch

Reputation: 3123

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

Upvotes: 1

Related Questions