Posted 2 min to read

A short example with a real use case of how to combine Redux’s server-side state with the client-side state using Redux Wrapper for Next.js.
Next.js + Redux Toolkit: State hydration between server and client
Photo by pexels.com

The problem

Imagine you have an app with a list of products stored in your Redux state that are fetched from an external API. That app uses an infinite scroll technique instead of a standard pagination to load the next portion of the data. On the server side you load the first 20 products in the Next’s getStaticProps() method and you want to load next 20 products if the user scrolls to the end of the list and append them to the previous state.

Without using a state hydration between server and client side, your Redux state would be empty since the client doesn’t see the server state to which next part of the products could be added.

The solution

To handle this case we'll use a handy package called redux-store-wrapper and its HYDRATE action.

Let's first create our slice:

// store/categorySlice.js

import { createSlice } from '@reduxjs/toolkit';
import { HYDRATE } from 'next-redux-wrapper';

const initialState = {
    products: []
}

const slice = createSlice({
    name: 'category',
    initialState,
    reducers: {
        setProducts(state, action) {
            state.products = action.payload;
        },
        addProducts(state, action) {
            state.products = [...state.products, ...action.payload];
        },
    },
    extraReducers: {
        [HYDRATE]: (state, action) => {
            return state = {
                ...state,
                ...action.payload.category
            };
        },
    },
});

export const { setProducts, addProducts } = slice.actions;
export default slice.reducer;

Next, configure the store:

// store/index.js

import { configureStore } from '@reduxjs/toolkit';
import { createWrapper } from 'next-redux-wrapper';
import categoryReducer from './categorySlice';

const makeStore = () => {
  let store = configureStore({
    reducer: {
        category: categoryReducer,
    }
  });

  return store;
}

export const storeWrapper = createWrapper(makeStore);

Then the store wrapper can be used with getStaticProps() or getServerSideProps() methods this way:

// pages/category/[slug].js

import productService from '../services/productService';
import DefaultLayout from '../layouts/DefaultLayout';
import ProductList from '../components/ProductList';
import { storeWrapper } from '../store';
import { setProducts } from '../store/categorySlice';

export default function Category() {
    const { products } = useSelector(state => state.category);

    return (
        <DefaultLayout>
            <ProductList products={products} />
        </DefaultLayout>
    )
}

export const getStaticProps = storeWrapper.getStaticProps(storeWrapper => async ({ params }) => {
    const products = await productService.getFromCollection(params.slug);

    storeWrapper.dispatch(setProducts(products));
});

In the example above, every time the page with getStaticProps() method is requested, the HYDRATE action will be dispatched:

Redux State Hydration