javascriptreduxreact-reduxnext.jsnext-redux-wrapper

Preserve state value on client side navigation - NextJs - Next-Redux-Wrapper


So I am trying to fix the hydrating issue I am facing when using wrapper.getServerSideProps. When I reroute with the current setup the store is cleared out and then the new data is added, which results in a white page since a lot of important data is no longer there (i.e, translations and cms data).

Screenshot from redux-dev-tools Hydrate action diff:

enter image description here

Screenshot is taken after routing from the homepage to a productpage, so that there was an existing store. Everything is reset to the initial app state.

What I am trying to do

In the store.js I create the store and foresee a reducer to handle the Hydrate call. The downside of this approach is that the payload will always be a new store object since it is called on the server. I was thinking to check the difference between the 2 json's and then only apply the difference instead of the whole initial store.

  1. Get the difference between the client and server state.
  2. Make the next state, overwrite clientstate with patched serverstate so this includes, updated state from hydrate and the existing client state.
  3. Currently results in a white page.

You can see the reducer code below in the store.js

//store.js

import combinedReducer from './reducer';

const bindMiddleware = (middleware) => {
    if (process.env.NODE_ENV !== 'production') {
        return composeWithDevTools(applyMiddleware(...middleware));
    }
    return applyMiddleware(...middleware);
};

const reducer = (state, action) => {
  if (action.type === HYDRATE) {
    const clientState = { ...state };
    const serverState = { ...action.payload };

    if (state) {
      // preserve state value on client side navigation

      // Get the difference between the client and server state.
      const diff = jsondiffpatch.diff(clientState, serverState);
      if (diff !== undefined) {
        // If there is a diff patch the serverState, with the existing diff
        jsondiffpatch.patch(serverState, diff);
      }
    }

    // Make next state, overwrite clientstate with patched serverstate
    const nextState = {
      ...clientState,
      ...serverState,
    };

    // Result, blank page.
    return nextState;
  }
  return combinedReducer(state, action);
};

export const makeStore = () => {
    const cookies = new Cookies();
    const client = new ApiClient(null, cookies);

    const middleware = [
        createMiddleware(client), 
        thunkMiddleware.withExtraArgument(cookies),
    ];

    return createStore(reducer, bindMiddleware(middleware));
};

const wrapper = createWrapper(makeStore);

export default wrapper;
//_app.jsx

const App = (props) => {
    const { Component, pageProps, router } = props;

    return (
        <AppComponent cookies={cookies} locale={router.locale} location={router}>
            <Component {...pageProps} />
        </AppComponent>
    );
};

App.getInitialProps = async ({ Component, ctx }) => {
    return {
        pageProps: {
            ...(Component.getInitialProps ? await Component.getInitialProps(ctx) : {}),
        },
    };
};

App.propTypes = {
    Component: PropTypes.objectOf(PropTypes.any).isRequired,
    pageProps: PropTypes.func,
    router: PropTypes.objectOf(PropTypes.any).isRequired,
};

App.defaultProps = {
    pageProps: () => null,
};

export default wrapper.withRedux(withRouter(App));
// Product page
export const getServerSideProps = wrapper.getServerSideProps(
async ({ query, store: { dispatch } }) => {
    const productCode = query.id?.split('-', 1).toString();
    await dispatch(getProductByCode(productCode, true));
});

const PDP = () => {
    const { product } = useSelector((state) => state.product);
    return (
        <PageLayout>
            <main>
                <h1>{product?.name}</h1>
                <div
                    className="description"
                    dangerouslySetInnerHTML={{ __html: product?.description }}
                />
            </main>
        </PageLayout>
    );
};

export default PDP;

Solution

  • Oke, so I solved my issue through not overthinking the concept. Went back to the drawing board and made a simple solution.

    Came to the conclusion that there are only a few state objects that need to persist during client navigation.

    I only had to make a change to my i18n, to make it dynamic since we fetch translations on page basis.

    This is the final reducer for anyone that might, in the future run into a similar problem.

    const reducer = (state, action) => {
      if (action.type === HYDRATE) {
        const clientState = { ...state };
        const serverState = { ...action.payload };
        const nextState = { ...clientState, ...serverState };
    
        const locale = nextState.i18n.defaultLocale || config.i18n.defaultLocale;
    
        const nextI18n = {
          ...state.i18n,
          locale,
          messages: {
            [locale]: {
              ...state.i18n.messages[locale],
              ...nextState.i18n.messages[locale],
            },
          },
          loadedGroups: {
            ...state.i18n.loadedGroups,
            ...nextState.i18n.loadedGroups,
          },
        };
    
        if (state) {
          nextState.i18n = nextI18n;
          nextState.configuration.webConfig = state.configuration.webConfig;
          nextState.category.navigation = state.category.navigation;
        }
    
        return nextState;
      }
      return combinedReducer(state, action);
    };