question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Hydration happens after first render when navigating from a different page

See original GitHub issue

When I navigate to a page which I added the getStaticProps wrapper to, the hydration happens after an innitial render which results in my component getting rendered breifly without the new state changes made from the dispatch done in getStaticProps, then the hydration happens and the component rerenders with the new state.

Note that this issue only happens when I navigate from a different page. When I refresh from the page directly, the hydartion happens first, as it should, and then the component is rendered.

Sorry for lack of code snippets, will add if needed.

Edit: Code was added below.

I have create a simple counter app. The count starts at 0 but it is reset to 10 inside getStaticProps.

I have added console logs inside the HYDRATE function and the counter component

The console logs are used to indication when the hydration happens and when the counter app renders

The result of refreshing the http://localhost:3000/counter page is:

HYDRATE...
render...

However, the result of loading http://localhost:3000 and then navigating to http://localhost:3000/counter by clicking a Link is:

render...
HYDRATE...
render...

The problem is that there are 2 renders. One before hydration and another one after. The expected behaviour is one render AFTER hydration, same as above.

// store/counter.js

import { createSlice } from '@reduxjs/toolkit';
import { HYDRATE } from 'next-redux-wrapper';

const slice = createSlice({
  name: 'counter',
  initialState: {
    count: 0,
  },
  reducers: {
    increment: (counter, action) => {
      counter.count += action.payload;
    },
    decrement: (counter, action) => {
      counter.count -= action.payload;
    },
    setCount: (counter, action) => {
      counter.count = action.payload;
    },
  },
  extraReducers: {
    [HYDRATE]: (counter, action) => {
      console.log('HYDRATE...');
      counter.count = action.payload.counter.count;
    },
  },
});

export default slice.reducer;

export const { increment, decrement, setCount } = slice.actions;

export const selectCount = state => state.counter.count;

// store/configureStore.js

import { configureStore } from '@reduxjs/toolkit';
import { createWrapper } from 'next-redux-wrapper';
import counter from './counter';

export const wrapper = createWrapper(() =>
  configureStore({
    reducer: {
      counter,
    },
    devTools: true,
  })
);
// pages/_app.js

import '../styles/globals.css';

import { wrapper } from '../store/ configureStore';

function MyApp({ Component, pageProps }) {
  return <Component {...pageProps} />;
}

export default wrapper.withRedux(MyApp);

// pages/index.js

import Head from 'next/head';
import Link from 'next/link';

import styles from '../styles/Home.module.css';

export default function Home() {
  return (
    <div className={styles.container}>
      <Head>
        <title>Counter App</title>
        <link rel="icon" href="/favicon.ico" />
      </Head>

      <main className={styles.main}>
        <div className={styles.title}>
          <Link href="/counter">Go to counter</Link>
        </div>
      </main>
    </div>
  );
}

// pages/counter.js

import Head from 'next/head';
import styles from '../styles/Home.module.css';

import { wrapper } from '../store/ configureStore';
import { useDispatch, useSelector } from 'react-redux';

import { selectCount, increment, decrement, setCount } from '../store/counter';

export default function Counter() {
  const dispatch = useDispatch();
  const count = useSelector(selectCount);
  console.log('render...');

  return (
    <div className={styles.container}>
      <Head>
        <title>Counter App</title>
        <link rel="icon" href="/favicon.ico" />
      </Head>

      <main className={styles.main}>
        <h1>Counter App</h1>
        <h2>Count: {count}</h2>
        <button onClick={() => dispatch(increment(1))}>Increment</button>
        <button onClick={() => dispatch(decrement(1))}>Decrement</button>
      </main>
    </div>
  );
}

export const getStaticProps = wrapper.getStaticProps(async ({ store }) => {
  store.dispatch(setCount(10));
});

Issue Analytics

  • State:closed
  • Created 3 years ago
  • Reactions:17
  • Comments:42 (18 by maintainers)

github_iconTop GitHub Comments

18reactions
Jekinscommented, Oct 9, 2020

I noticed that after routing, the state is reset and then only adds new dispatches for the new page. State not persisted from page to page. This is a big problem for me and do not understand how fix it.

5reactions
Shaker-Hamdicommented, Jan 15, 2022

I fixed my issue by implementing this line of documentation …

if (state.count) nextState.count = state.count; // preserve count value on client side navigation,

So, here’s my code …

import { HYDRATE } from "next-redux-wrapper";

import { AnyAction, combineReducers } from "@reduxjs/toolkit";

import { counterReducer } from "../features/counter";
import { globalReducer } from "../features/global/reducer";
import { userReducer } from "../features/user";

const combinedReducer: any = combineReducers({
  global: globalReducer,
  counter: counterReducer,
  user: userReducer,
});

export const rootReducer = (
  state: ReturnType<typeof combinedReducer>,
  action: AnyAction
) => {
  if (action.type === HYDRATE) {
    const nextState = {
      ...state, // use previous state
      ...action.payload, // apply delta from hydration
    };
    if (state.user) nextState.user = state.user; //preserve user value on client side navigation
    return nextState;
  } else {
    return combinedReducer(state, action);
  }
};

The only question now is, should I do this to every piece of state that I have in order to preserve the value on client-side navigation?

If I have a state.product, I have to duplicate that line to be … if (state.product) nextState.product= state.product;

That sounds like there should be a better way to handle this.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Hydration and Server-side Rendering - somewhat abstract
The initial render cycle of the application must result in the same markup, whether run on the client or the server.
Read more >
The Perils of Rehydration - Josh W Comeau
A surprisingly-common misconception can lead to big rendering issues that are difficult to debug. This deep-dive tutorial examines how React ...
Read more >
Keeping Server-Side Rendering Cool With React Hydration
After Hydration the React reconciler APIs take over and the site becomes interactive.
Read more >
Understanding React Hydration | Gatsby
With client-side rendering, most actions trigger local DOM updates instead of network requests. Clicking a navigation link builds the requested page on the ......
Read more >
What's the difference between hydrate() and render() in React ...
On the "hydrate" page, all the markup is immediately rendered, because all the necessary html is served with the page. The button is ......
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found