I have this Next.js application and I'm currently using next-redux-wrapper and react-redux with @reduxjs/toolkit.
I have this Saga which waits for an action to be dispatched before firing a XHR-Request to get some API-data. After that another action is dispatched to save the data to the store (or eventually another one to handle an error).
This works fine on the client side, but I want to prerender all the pages (and make use of next.js's Incremental Static Regeneration), to maximize performance on the one hand and to keep the API request out of the client code (as it is authenticated and regulated by a vendor).
So I followed all examples on the next-redux-wrapper-GitHub Page, but it seems like, the hydration happens, after the first action is dispatched, so in parallel the XHR-request is actually done, but it happens after the hydration is ready.
My state on the client side is the result of productRequested
{
loading: true,
product: null
}
Here are all relevant files:
store.ts
import { configureStore, Store } from '@reduxjs/toolkit';
import { toasterSlice } from 'mylocalpackage';
import { catalogSlice } from 'mylocalpackage';
import createSagaMiddleware, { Task } from 'redux-saga';
import { Context, createWrapper } from 'next-redux-wrapper';
import rootSaga from './sagas';
export interface SagaStore extends Store {
sagaTask?: Task;
}
const makeStore = (context: Context) => {
const sagaMiddleware = createSagaMiddleware();
const store = configureStore({
reducer: {
toaster: toasterSlice.reducer,
catalog: catalogSlice.reducer,
},
devTools: true,
middleware: [sagaMiddleware],
});
// See sagas.ts file, to register new sagas
(store as SagaStore).sagaTask = sagaMiddleware.run(rootSaga);
return store;
};
export type AppStore = ReturnType<typeof makeStore>;
export type AppState = ReturnType<AppStore['getState']>;
export type AppDispatch = AppStore['dispatch'];
export const wrapper = createWrapper(makeStore);
sagas.ts
import { watchFetchProduct } from 'mylocalpackage';
import { spawn, call, all } from 'redux-saga/effects';
export default function* rootSaga() {
const sagas = [watchFetchProduct];
yield all(
sagas.map((saga) =>
spawn(function* () {
while (true) {
try {
yield call(saga);
break;
} catch (e) {
console.error(e);
}
}
})
)
);
}
catalogSlice.ts
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { ProductInterface } from '../../interfaces/product.interface';
import { HYDRATE } from 'next-redux-wrapper';
/**
* The interface for catalog data provided in redux store.
*/
export interface CatalogState {
loading: boolean;
product: ProductInterface | null;
}
/**
* The initial redux state.
*/
const initialState: CatalogState = {
loading: false,
product: null,
};
/**
* The catalog redux slice.
*/
export const catalogSlice = createSlice({
name: 'catalog',
initialState,
reducers: {
productRequested: (state, action: PayloadAction<string>) => {
state.loading = true;
state.product = initialState.product;
},
productReceived(state, action: PayloadAction<ProductInterface>) {
return {
loading: false,
product: action.payload,
};
},
productRequestFailed: (state) => {
state.loading = false;
state.product = initialState.product;
},
},
extraReducers: {
[HYDRATE]: (state, action) => {
console.log('hydrate: ', action.payload.catalog);
return {
...state,
...action.payload.catalog,
};
},
},
});
const { productRequested, productReceived, productRequestFailed } = catalogSlice.actions;
export const CatalogActions = {
productRequested,
productReceived,
productRequestFailed,
};
export default catalogSlice.reducer;
catalogSaga.ts
import { call, put, takeEvery } from 'redux-saga/effects';
import { CatalogActions } from './catalogSlice';
import CatalogDataService from './catalogService';
import { PayloadAction } from '@reduxjs/toolkit';
import { AxiosResponse } from 'axios';
import { ProductInterface } from '../../interfaces/product.interface';
/**
* The watcher function to fetch single product.
*/
export function* watchFetchProduct() {
yield takeEvery(CatalogActions.productRequested, fetchProductAsync);
}
/**
* The worker function to fetch single product.
* @param action
*/
function* fetchProductAsync(action: PayloadAction<string>) {
const { payload } = action;
try {
const product: ProductInterface = yield call(() =>
CatalogDataService.getProductBySlug(payload).then((axiosResponse: AxiosResponse) => {
console.log(axiosResponse.data);
return axiosResponse.data;
})
);
yield put(CatalogActions.productReceived(product));
} catch (error) {
yield put(CatalogActions.productRequestFailed());
}
}
page
import { CatalogActions, TechnicalDataTable } from 'mylocalpackage';
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
import i18nConfig from '../../next-i18next.config';
import { useTranslation } from 'next-i18next';
import { AppState, SagaStore, wrapper } from '../../redux/store';
import { connect } from 'react-redux';
import { GetStaticProps } from 'next';
import { END } from 'next-redux-wrapper';
export const Machine = (props) => {
const { t } = useTranslation('common');
return (
<>Template, whatsoever...</>
);
};
const mapStateToProps = (state: AppState) => ({
loading: state.catalog.loading,
product: state.catalog.product,
});
const mapDispatchToProps = {
productRequested: CatalogActions.productRequested,
};
export default connect(mapStateToProps, mapDispatchToProps)(Machine);
export const getStaticProps: GetStaticProps = wrapper.getServerSideProps((store) => async ({ locale, params }) => {
const slug = params?.slug ?? '';
if (slug !== '') {
await store.dispatch(
CatalogActions.productRequested(Array.isArray(slug) && !!slug[0] ? slug[0] : (slug as string))
);
}
store.dispatch(END);
await (store as SagaStore).sagaTask.toPromise();
return {
props: {
...(await serverSideTranslations(locale, ['common'], i18nConfig)),
},
};
});
export const getStaticPaths = async () => {
return { paths: [{ params: { slug: '1' } }], fallback: 'blocking' };
};