How to Sync NextJS with Redux

How to Sync NextJS with Redux

ยท

10 min read

Introduction

With the new capabilities of NextJS, the way people are constructing their pages, are changing a little bit. As Next, brings again the performance of static pages, many applications are migrating to this, but when we want to use state management tools such as Redux or MobX, remembering now that we are in a static environment, some different approaches need to be taken in order to inject Redux stores in the components of different static pages, preserving the values in the store's state.

As each of the export components in the pages folder, will act as a static page, so now we need to inject the Redux store with a wrapper in each of the exported components, whose we want to use Redux capabilities.

How Does Redux Works with Next

For default, in a normal React app, you would inject the Redux store through your app using the following definition, in the render of your main component:

  render () {
    const {Component, pageProps, store} = this.props
    return <Container>
      <Provider store={store}>
        <Component {...pageProps} />
      </Provider>
    </Container>
  }

There are also another ways of injecting if you are using only functional components architecture, but with NextJS this doesn't work, you need to use a wrapper to ensure that all the static pages will receive the store with the most updated values.

Setting up Redux for static apps is rather simple: a single Redux store has to be created that is provided to all pages.

When Next.js static site generator or server side rendering is involved, however, things start to get complicated as another store instance is needed on the server to render Redux-connected components.

Furthermore, access to the Redux Store may also be needed during a page's getInitialProps.

This is where next-redux-wrapper comes in handy: It automatically creates the store instances for you and makes sure they all have the same state.

Moreover it allows to properly handle complex cases like App.getInitialProps (when using pages/_app) together with getStaticProps or getServerSideProps at individual page level.

Library provides uniform interface no matter in which Next.js lifecycle method you would like to use the Store.

So, the main package used to create the wrapper it's located in the following repo, and you can know more about how this works in the background:

How to Use next-redux-wrapper

Basically, you will create your Redux store, actions and reducers, regardless if you are using Thunk or Sagas, or nothing, in the same way you do in normal react apps, but with next-redux-wrapper you will need to create some additional things, the makeStore and wrapper.

// This makeStore is needed for the wrapper, for every new page that is called, a new store with the current values will be created
const makeStore: MakeStore<any> = (context: Context) => createStore(reducers);

const wrapper = createWrapper<any>(makeStore, {debug: false});

export default wrapper;

So you will create this new configuration, using the combined reducers that you are used to do.

The makeStore is an object that will be created for every new static page call, which will create a new store with the current state values, and also there is the wrapper, that will be used to wrapped Redux stores on your components.

After you have both of them, you need to override _app.tsx with the following code:

// pages/_app.js
import React from 'react'
import App from 'next/app';
import wrapper from "../store";

// For default you don't need to edit _app.tsx, but if you want to wrapper the pages with redux wrapper, you need
// to override _app.tsx with this code bellow
class MyApp extends App {
    // @ts-ignore
    static async getInitialProps({Component, ctx}) {
        return {
            pageProps: {
                // Call page-level getInitialProps
                ...(Component.getInitialProps ? await Component.getInitialProps(ctx) : {}),
            }
        };
    }

    render() {
        const {Component, pageProps} = this.props;
        return (
            <Component {...pageProps} />
        );
    }

}

export default wrapper.withRedux(MyApp);

This will create the capability of connecting all your static pages to the Redux store, because your main app component it's wrapped with next-redux-wrapper.

How to Connect a Component to Redux Store and Actions

After you have wrapped your _app.tsx component, in each of the reducers that you create, you will need to add the following action handler on the switch case:

case HYDRATE:
            // Attention! This will overwrite client state! Real apps should use proper reconciliation.
            return {...state};

In this repo example reducer, the entire code it's like this:

import {AnyAction} from "redux";
import {HYDRATE} from "next-redux-wrapper";
import ITickState from "./index";


const initialState: ITickState = {
    message: 'init'
};


export function tick(state: ITickState = initialState, action: AnyAction): ITickState {
    switch (action.type) {
        case HYDRATE:
            // Attention! This will overwrite client state! Real apps should use proper reconciliation.
            return {...state};
        case 'TICK':
            return {...state, message: action.payload};
        default:
            return state;
    }
}

This HYDRATE action, comes from the next-redux-wrapper package, and it's triggered every time a new component that connects with Redux it's opened, you can use that action to sync the values that will stay in the Store state, without loosing nothing between the static pages.

With everything done, you just need to create a component for a page, and connect with Redux in the same way you used to do, but now you need to place this following piece of code in the component.

// As the wrapper is injected in _app.tsx, for every component(page) that will interact with Redux and Thunk
// you need to place this piece of code bellow, that will get the static props from the wrapper, and inject on your
// component
export const getStaticProps = wrapper.getStaticProps(
    ({}) => {
    }
);

As the wrapper is injected in _app.tsx, for every component(page) that will interact with Redux, you need to place this piece of code bellow, that will get the static props from the wrapper, and inject on your component.

An example of an entire class component will look like:

import Link from 'next/link'
import Layout from '../components/Layout'
import {connect} from "react-redux";
import * as React from "react";
import {IStoreState} from "../store/reducers";
import {getTickState} from "../store/selectors";
import {updateTick} from "../store/tick/actions";
import {thunkAsyncFunction} from "../store";
import ITickState from "../store/tick";
import wrapper from "../store";

interface IProps {
    tick: ITickState
    updateAnnouncement: any
}

interface IState {}

interface IDispatchProps {
    onUpdateTick: (message: string) => ITickState,
    thunkAsyncFunction: () => Promise<any>;
}

type Props = IProps & IState & IDispatchProps

class App extends React.Component<Props> {

    constructor(props: Props) {
        super(props);
    }

    async componentWillUnmount(): Promise<void> {
        await this.props.thunkAsyncFunction();
    }

    render() {
        return (
            <Layout title="Home | Next.js + TypeScript Example">
                <h1>Hello Next.js ๐Ÿ‘‹</h1>
                <p>
                    <Link href="/about">
                        <a>About</a>
                    </Link>
                </p>
                <div>
                    The current tick state: {this.props.tick.message}
                </div>
            </Layout>
        );
    }
}

const mapStateToProps = (state: IStoreState): {tick: ITickState} => ({
    tick: getTickState(state)
});

const mapDispatchToProps = (dispatch: any): IDispatchProps => {
    return {
        onUpdateTick: (message: string) =>
            dispatch(updateTick(message)),
        thunkAsyncFunction: () => dispatch(thunkAsyncFunction())
    }
};

export default connect(mapStateToProps, mapDispatchToProps)(App);

// As the wrapper is injected in _app.tsx, for every component(page) that will interact with Redux and Thunk
// you need to place this piece of code bellow, that will get the static props from the wrapper, and inject on your
// component
export const getStaticProps = wrapper.getStaticProps(
    ({}) => {
    }
);

If you prefer the functional component way, you can do:

// you can also use `connect()` instead of hooks
const Page: NextPage = () => {
    const {tick} = useSelector<State, State>(state => state);
    return (
        <div>{tick}</div>
    );
};

export default Page;

export const getStaticProps = wrapper.getStaticProps(
    ({}) => {
    }
);