Using NextJS with Redux. Should the store be re-initialised between pages with different preloaded states?

Viewed 796

I'm trying to make Redux work with NextJS. In the official NextJS repo, you can find the following example:

https://github.com/vercel/next.js/tree/canary/examples/with-redux-thunk

Here is the main part of the Redux code (the one that initialises the store).

import { useMemo } from 'react'
import { createStore, applyMiddleware } from 'redux'
import { composeWithDevTools } from 'redux-devtools-extension'
import thunkMiddleware from 'redux-thunk'
import reducers from './reducers'

let store

function initStore(initialState) {
  return createStore(
    reducers,
    initialState,
    composeWithDevTools(applyMiddleware(thunkMiddleware))
  )
}

export const initializeStore = (preloadedState) => {
  let _store = store ?? initStore(preloadedState)

  // After navigating to a page with an initial Redux state, merge that state
  // with the current state in the store, and create a new store
  if (preloadedState && store) {
    _store = initStore({
      ...store.getState(),
      ...preloadedState,
    })
    // Reset the current store
    store = undefined
  }

  // For SSG and SSR always create a new store
  if (typeof window === 'undefined') return _store
  // Create the store once in the client
  if (!store) store = _store

  return _store
}

export function useStore(initialState) {
  const store = useMemo(() => initializeStore(initialState), [initialState])
  return store
}

Here is what they say about it:

Every initial server-side request will utilize a new store. However, every Router or Link action will persist the same store as a user navigates through the pages. To demonstrate this example, we can navigate back and forth to /show-redux-state using the provided Links. However, if we navigate directly to /show-redux-state (or refresh the page), this will cause a server-side render, which will then utilize a new store.

Their full example is not a very good one, because none of the pages use the getInitialProps method. So you can't see the store changing when you switch pages.

// Create the store once in the client

From the comment above, I was under the impression that, no matter what, a store would only be initialised once (a single time) in the client. But that is not the case.

Every time I navigate to a page that adds a initialState to the Redux store (this is done by using the getInitialProps method), a get a new store on my client code. I've literally tested this behaviour. The store reference does change.

And that got me thinking: isn't mutating the Redux store references across multiple renders, a bad practice? I mean, React will re-render accordingly, but what about async code?

Let's say I have a Redux thunk that still pending and I navigate away from the page. Now, my Redux store will be re-initialised and what will happen if:

  • My thunk dispatch new actions? The dispatch that the thunk has is most likely linked to the previous store
  • My thunk accesses getState()? That getState() call is also probably linked to the previous store.

Aren't there some caveats because of the store re-initialisation?

1 Answers

Even though this is not mentioned in the Next.js docs, I think that there's definitely a limitation with the way NextJS suggests that the store should be initialised and re-initialised to merge server preloaded data.

I just ran the following test:

_app.tsx

import { useEffect } from "react";
import { Provider } from "react-redux";
import { useStore } from "./useStore";

export default function App({ Component, pageProps }) {
  console.log("Rendering App...");

  const store = useStore(pageProps.initialReduxState);
  const dispatch = store.dispatch;
  const getState = store.getState;

  useEffect(() => {
    console.log("store has changed");
  }, [store]);

  useEffect(() => {
    console.log("dispatch has changed");
  }, [dispatch]);

  useEffect(() => {
    console.log("getState has changed");
  }, [getState]);

  return (
    <Provider store={store}>
      <Component {...pageProps} />
    </Provider>
  );
}

When I navigate to pages without any pageProps.initialReduxState value, here is what I get:

enter image description here

When I navigate to pages with pageProps.initialReduxState value, here is what happens:

enter image description here

As a result from this test, I think it's safe to assume that pending code running inside a thunk will be stale and won't have any effect on the new store.


Redux points out that the most common use case for Redux on the server is to preload the initial render of the page. From that point on, as they say: the client takes over rendering duties.

https://redux.js.org/usage/server-rendering

enter image description here

Related