Search code examples
typescriptreduxnext.jsredux-thunkredux-toolkit

How to use "next-redux-wrapper" with "Next.js", "Redux-ToolKit" and Typescript properly?


I'm using RTK (redux-toolkit) inside a Next.js App. And I'm trying to dispatch an AsyncThunk Action inside "getInitialProps". When searching I found a package called "next-redux-wrapper" that exposes the "store" inside "getInitialProps", but I'm struggling to figure out how to make it work with my project.

Here's a barebone sample of the project where I'm using Typescript with 2 reducers at the moment. One reducer is using AsyncThunk to get data from an API. I already installed "next-redux-wrapper" but I don't know how to implement it around the so that all pages get access to the "store" inside "getInitialProps". The Docs of that package has an example but rather a confusing one.

Here's how my store.ts looks like ...

import { Action, configureStore, ThunkAction } from '@reduxjs/toolkit';
import { createWrapper, HYDRATE } from 'next-redux-wrapper';
import { counterReducer } from '../features/counter';
import { kanyeReducer } from '../features/kanye';

export const store = configureStore({
  reducer: {
    counter: counterReducer,
    kanyeQuote: kanyeReducer,
  },
});

export type AppDispatch = typeof store.dispatch;
export type RootState = ReturnType<typeof store.getState>;
export type AppThunk<ReturnType = void> = ThunkAction<
  ReturnType,
  RootState,
  unknown,
  Action<string>
>;

As you can see I imported next-redux-wrapper, but that's abuout it.

And here's how my "_app.tsx" looks like ...

import { Provider } from 'react-redux';
import type { AppProps } from 'next/app';
import { store } from '../app/store';

function MyApp({ Component, pageProps }: AppProps) {
  return (
    <Provider store={store}>
      <Component {...pageProps} />
    </Provider>
  );
}

export default MyApp;

I need to be able to dispatch the "getKanyeQuote" action in "getInitialProps" on this page ...

import React from 'react';
import { useAppDispatch, useAppSelector } from '../app/hooks';
import { getKanyeQuote } from '../features/kanye';

const kanye: React.FC = () => {
  const dispatch = useAppDispatch();
  const { data, pending, error } = useAppSelector((state) => state.kanyeQuote);

  return (
    <div>
      <h2>Generate random Kanye West quote</h2>
      {pending && <p>Loading...</p>}
      {data && <p>{data.quote}</p>}
      {error && <p>Oops, something went wrong</p>}
      <button onClick={() => dispatch(getKanyeQuote())} disabled={pending}>
        Generate Kanye Quote
      </button>
    </div>
  );
};

export default kanye;

And here's a link to a full sample. https://stackblitz.com/edit/github-bizsur-zkcmca?file=src%2Ffeatures%2Fcounter%2Freducer.ts

Any help is highly appreciated.


Solution

  • WORKING DEMO

    First, configure wrapper:

    import {
      Action,
      AnyAction,
      combineReducers,
      configureStore,
      ThunkAction,
    } from '@reduxjs/toolkit';
    import { createWrapper, HYDRATE } from 'next-redux-wrapper';
    import { counterReducer } from '../features/counter';
    import { kanyeReducer } from '../features/kanye';
    
    const combinedReducer = combineReducers({
      counter: counterReducer,
      kanyeQuote: kanyeReducer,
    });
    
    const reducer = (state: ReturnType<typeof combinedReducer>, action: AnyAction) => {
      if (action.type === HYDRATE) {
        const nextState = {
          ...state, // use previous state
          ...action.payload, // apply delta from hydration
        };
        return nextState;
      } else {
        return combinedReducer(state, action);
      }
    };
    
    export const makeStore = () =>
      configureStore({
        reducer,
      });
    
    type Store = ReturnType<typeof makeStore>;
    
    export type AppDispatch = Store['dispatch'];
    export type RootState = ReturnType<Store['getState']>;
    export type AppThunk<ReturnType = void> = ThunkAction<
      ReturnType,
      RootState,
      unknown,
      Action<string>
    >;
    
    export const wrapper = createWrapper(makeStore, { debug: true });
    
    

    Here the new reducer function merges newly created server store and client store:

    • wrapper creates a new server side redux store with makeStore function
    • wrapper dispatches HYDRATE action. Its payload is newly created server store
    • reducer merges server store with client store.

    We're just replacing client state with server state but further reconcilation might be required if the store grows complicated.

    wrap your _app.tsx

    No need to provide Provider and store because wrapper will do it accordingly:

    import type { AppProps } from 'next/app';
    import { wrapper } from '../app/store';
    
    function MyApp({ Component, pageProps }: AppProps) {
      return <Component {...pageProps} />;
    }
    
    export default wrapper.withRedux(MyApp);
    
    

    And then you can dispatch thunk action in your page:

    import { NextPage } from 'next/types';
    import React from 'react';
    import { useAppDispatch, useAppSelector } from '../app/hooks';
    import { getKanyeQuote } from '../features/kanye';
    import { wrapper } from '../app/store';
    
    const kanye: NextPage = () => {
      const dispatch = useAppDispatch();
      const { data, pending, error } = useAppSelector((state) => state.kanyeQuote);
    
      return (
        <div>
          <h2>Generate random Kanye West quote</h2>
          {pending && <p>Loading...</p>}
          {data && <p>{data.quote}</p>}
          {error && <p>Oops, something went wrong</p>}
          <button onClick={() => dispatch(getKanyeQuote())} disabled={pending}>
            Generate Kanye Quote
          </button>
        </div>
      );
    };
    
    kanye.getInitialProps = wrapper.getInitialPageProps(
      ({ dispatch }) =>
        async () => {
          await dispatch(getKanyeQuote());
        }
    );
    
    export default kanye;