Reputation: 2909
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.
Upvotes: 9
Views: 37079
Reputation: 8459
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:
makeStore
functionHYDRATE
action. Its payload is newly created server storeWe'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;
Upvotes: 36
Reputation: 1848
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
Upvotes: 0