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

2

9

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.

Bromeosin answered 20/12, 2021 at 19:23 Comment(0)
R
35

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;


Reprovable answered 23/12, 2021 at 6:52 Comment(11)
Your answer works great. But typescript is not happy about the kanye.getInitialProps, and async () => {. And this part const reducer = (state, action) => {Bromeosin
@Bromeosin I fixed the type error. Please check it out. Thanks!Reprovable
It works great. Thank you for this.Bromeosin
Is it necessary here to use getInitialProps Because if I use getStaticProps or getServerProps state getting reset automaticallyHomelike
@Homelike Hmm. Interesting. Can you elaborate it?Reprovable
@Reprovable Take a look here #70995889 I'm stuck because of this your solution works only for getInitialPropsHomelike
@Reprovable any suggestion, please?Homelike
The problem I have with this answer is that RootState is of type any. Does anyone know how to get around this and keep types in RootState?Founder
@SerhiiHolinei for that you can make this change: change this export type RootState = ReturnType<Store['getState']>; to export type RootState = ReturnType<typeof combinedReducer>;Abnormality
@Reprovable I'm not getting strong typing with useAppSelector and state there is coming as any with this solution. Expecting strong typing for intellisense to follow there.Abnormality
Thank you @AakashThakur . I solved the problem by annotating initialState in the createSliceFounder
O
0

Following the Usage guide on next-redux-wrapper repo. Within you store file will be

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

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>
>;

const makeStore = () => store;

export const wrapper = createWrapper(makeStore);

and _app.js file change following

import type { AppProps } from 'next/app';
import { wrapper } from '../app/store';

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

export default wrapper.withRedux(MyApp);

Then direct to /kanye page It should works

Omen answered 23/12, 2021 at 6:50 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.