8.1.0 • Published 1 year ago

next-redux-wrapper v8.1.0

Weekly downloads
127,604
License
MIT
Repository
github
Last release
1 year ago

Redux wrapper for Next.js

Build status

:warning: This will work only with NextJS 6.x :warning:

If you're looking for a version for NextJS 5.x (the one for individual pages) use 1.x branch.

Installation

npm install next-redux-wrapper@next --save

Wrapper has to be attached your _app component (located in /pages). All other pages may use regular connect function of react-redux.

Here is the minimal setup (makeStore and reducer usually are located in other files):

// pages/_app.js
import React from "react";
import {createStore} from "redux";
import {withReduxApp} from "next-redux-wrapper";

const reducer = (state = {foo: ''}, action) => {
    switch (action.type) {
        case 'FOO':
            return {...state, foo: action.payload};
        default:
            return state
    }
};

/**
* @param {object} initialState
* @param {boolean} options.isServer indicates whether it is a server side or client side
* @param {Request} options.req NodeJS Request object (if any)
* @param {boolean} options.debug User-defined debug mode param
* @param {string} options.storeKey This key will be used to preserve store in global namespace for safe HMR 
*/
const makeStore = (initialState, options) => {
    return createStore(reducer, initialState);
};

class MyApp extends React.Component {

    static async getInitialProps({Component, ctx}) {

        // we can dispatch from here too
        ctx.store.dispatch({type: 'FOO', payload: 'foo'});

        const pageProps = Component.getInitialProps ? await Component.getInitialProps(ctx) : {};

        return {pageProps};

    }

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

}

export default withReduxApp(makeStore)(MyApp);

And then actual page components can be simply connected:

import React, {Component} from "react";
import {connect} from "react-redux";

class Page extends Component {
    static getInitialProps({store, isServer, pathname, query}) {
        store.dispatch({type: 'FOO', payload: 'foo'}); // component will be able to read from store's state when rendered
        return {custom: 'custom'}; // you can pass some custom props to component from here
    }
    render() {
        return (
            <div>
                <div>Prop from Redux {this.props.foo}</div>
                <div>Prop from getInitialProps {this.props.custom}</div>
            </div>
        )
    }
}

Page = withRedux(makeStore, (state) => ({foo: state.foo}))(Page);

export default Page;

How it works

No magic is involved, it auto-creates Redux store when getInitialProps is called by Next.js and then passes this store down to React Redux's Provider, which is used to wrap the original component, also automatically. On the client side it also takes care of using same store every time, whereas on server new store is created for each request.

The withRedux function accepts makeStore as first argument. The makeStore function will receive initial state as one argument and should return a new instance of Redux store each time when called, no memoization needed here, it is automatically done inside the wrapper.

When makeStore is invoked it is also provided with a configuration object as the second parameter, which includes:

  • isServer (boolean): true if called while on the server rather than the client
  • req (Request): The next.js getInitialProps context req parameter
  • res (Request): The next.js getInitialProps context req parameter
  • query (object): The next.js getInitialProps context query parameter

The object also includes all configuration as passed to withRedux.

Although it is possible to create server or client specific logic in both createStore function and getInitialProps method I highly don't recommend to have different behavior. This may cause errors and checksum mismatches which in turn will ruin the whole purpose of server rendering.

Keep in mind that whatever you do in _app is also affecting the NextJS error page, so if you dispatch, set something on req and checki it to prevent double dispatch.

I don't recommend to use withRedux in both top level pages and _document.js files, Next.JS does not have provide a reliable way to determine the sequence when components will be rendered. So per Next.JS recommendation it is better to have just data-agnostic things in _document and wrap top level pages with another HOC that will use withRedux.

Async actions in getInitialProps

function someAsyncAction() {
    return {
        type: 'FOO',
        payload: new Promise((res) => { res('foo'); })
    }
}

function getInitialProps({store, isServer, pathname, query}) {
    
    // lets create an action using creator
    const action = someAsyncAction();
    
    // now the action has to be dispatched
    store.dispatch(action);
    
    // once the payload is available we can resume and render the app
    return action.payload.then((payload) => {
        // you can do something with payload now
        return {custom: 'custom'}; 
    });
    
}

Usage with Immutable.JS

If you want to use Immutable.JS then you have to modify your makeStore function, it should detect if object is an instance of Immutable.JS, and if not - convert it using Immutable.fromJS:

export default function makeStore(initialState = {}) {
    // Nasty duck typing, you should find a better way to detect
    if (!initialState.toJS) initialState = Immutable.fromJS(initialState);
    return createStore(reducer, initialState, applyMiddleware(thunk));
}

The reason is that initialState is transferred over the network from server to client as a plain object (it is automatically serialized on server) so it should be converted back to Immutable.JS on client side.

Here you can find better ways to detect if an object is Immutable.JS: https://stackoverflow.com/a/31919454/5125659.

Usage with Redux Persist

Honestly, I think that putting a persistence gate is not necessary because server can already send some HTML with some state, so it's better to show it right away and then wait for REHYDRATE action to happen to show additional delta coming from persistence storage. That's why we use Server Side Rendering in a first place.

But, for those who actually want to block the UI while rehydration is happening, here is the solution (still hacky though).

// lib/redux.js
import logger from 'redux-logger';
import {applyMiddleware, createStore} from 'redux';

const SET_CLIENT_STATE = 'SET_CLIENT_STATE';

export const reducer = (state, {type, payload}) => {
    if (type === SET_CLIENT_STATE) {
        return {
            ...state,
            fromClient: payload
        };
    }
    return state;
};

const makeConfiguredStore = (reducer, initialState) =>
    createStore(reducer, initialState, applyMiddleware(logger));

export const makeStore = (initialState, {isServer, req, debug, storeKey}) => {

    if (isServer) {

        initialState = initialState || {fromServer: 'foo'};

        return makeConfiguredStore(reducer, initialState);

    } else {

        // we need it only on client side
        const {persistStore, persistReducer} = require('redux-persist');
        const storage = require('redux-persist/lib/storage').default;

        const persistConfig = {
            key: 'nextjs',
            whitelist: ['fromClient'], // make sure it does not clash with server keys
            storage
        };

        const persistedReducer = persistReducer(persistConfig, reducer);
        const store = makeConfiguredStore(persistedReducer, initialState);

        store.__persistor = persistStore(store); // Nasty hack

        return store;
    }
};

export const setClientState = (clientState) => ({
    type: SET_CLIENT_STATE,
    payload: clientState
});

And then in NextJS _app page:

// pages/_app.js
import React from "react";
import withRedux from "next-redux-wrapper";
import {makeStore} from "./lib/redux";
import {PersistGate} from 'redux-persist/integration/react';

export default withRedux(makeStore, {debug: true})(class MyApp extends React.Component {

    render() {
        const {Component, pageProps, store} = this.props;
        return (
            <PersistGate persistor={store.__persistor} loading: {<div>Loading</div>}>
                <Component {...pageProps} />
            </PersistGate>
        );
    }

});

And then in NextJS page:

// pages/index.js
import React from "react";
import {connect} from "react-redux";

export default connect(
    (state) => state,
    {setClientState}
)(({fromServer, fromClient, setClientState}) => (
    <div>
        <div>fromServer: {fromServer}</div>
        <div>fromClient: {fromClient}</div>
        <div><button onClick={e => setClientState('bar')}>Set Client State</button></div>
    </div>
));

Resources

create-fdn-app@sprucelabs/react-heartwood-components@courselit/appnext-packagespowersports_expowavelib-exampleenotarylog-clientpops-app-sstvpops-kids-sstvpops-plus-webspotto-locationsspotto-searchspotto-walkershelf-app-agent-assist@brudi/brudi-toolbox-nextfdy2-sales-funneldemo-dynamicxr3-client@busy-bee/ui-wwwstadtenergielodext@beepsoft/ocho@beepsoft/ocho-2@beepsoft/ocho-3@beepsoft/ocho-6@infinitebrahmanuniverse/nolb-next-r@plateer/create-x2bee-appreact-vrw@everything-registry/sub-chunk-2268ydlmwebydl-mobilewavelibweb-backoffice-neww3-reduxweb-whitelabelwith-reduxwith-redux-wrapperwith-firebase-authenticationwowgo-state-nextwowgo-state-reactticketmelon-webtesting-reopo-paysvitalsio-admin-coresylksoft-admin-with-reduxzettadaten_boiler_palleteproject-nextqr-pagesqr-pages-testreact-sprucebotsohosme-core-fesnapesrv-stories-ui@mooglee/corewww-enterprise@gameworkers/chapter-map-componentproving-groundsrcostexam@nextdapp/wallet@nteract/play@nteract/web@vibe-reporting/uieventjuicer-site-components@types/next-redux-wrapper@sprucelabs/react-sprucebot@strawbees/learning-ui@kstory8715/next-common@slonum/uidumdum-next-boilerplate-2022dungeoncrawlerescala-core@targecy/sdk@sprucelabs/spruce-next-helpers@tverse/ui@kengoldfarb/react-sprucebotfrontend-servicefullstack-nextjs-app-template@zalastax/nolb-next-rgeo-web-cadastreheq-devinternal-service-portalnext-landingnext-js-tsnext-saga-persist-boilerplatenextjs-firstnextjs-secondjie-webjoyteka_quest_constructorbonde-public@courselit/state-managementbrand-ingestion-prototypebrax-react-pagemyinvisalignorgzapp-common-utility-modulecryptic-reduxcustom-server-typescriptoneto3kstone-foundation-ui-coremnra-scriptsbed-and-breakfast-web-platform@betox/sx-core
9.0.0-rc.1

1 year ago

9.0.0-rc.2

1 year ago

8.1.0

1 year ago

8.0.0

2 years ago

8.0.0-rc.1

2 years ago

7.0.5

3 years ago

7.0.4

3 years ago

7.0.3

3 years ago

7.0.2

3 years ago

7.0.0

3 years ago

7.0.1

3 years ago

7.0.0-rc.2

3 years ago

7.0.0-rc.1

3 years ago

6.0.2

4 years ago

6.0.1

4 years ago

6.0.0

4 years ago

6.0.0-rc.8

4 years ago

6.0.0-rc.7

4 years ago

6.0.0-rc.6

4 years ago

6.0.0-rc.5

4 years ago

6.0.0-rc.4

4 years ago

6.0.0-rc.3

4 years ago

6.0.0-rc.2

4 years ago

6.0.0-rc.1

4 years ago

5.0.0

4 years ago

4.0.1

5 years ago

4.0.0

5 years ago

3.0.0

5 years ago

3.0.0-alpha.3

5 years ago

3.0.0-alpha.2

5 years ago

3.0.0-alpha.1

5 years ago

3.0.0-alpha.0

5 years ago

2.1.0

5 years ago

2.0.0

6 years ago

2.0.0-beta.6

6 years ago

2.0.0-beta.5

6 years ago

2.0.0-beta.4

6 years ago

2.0.0-beta.3

6 years ago

2.0.0-beta.2

6 years ago

2.0.0-beta.1

6 years ago

1.3.5

6 years ago

1.3.4

7 years ago

1.3.3

7 years ago

1.3.2

7 years ago

1.3.1

7 years ago

1.3.0

7 years ago

1.2.0

7 years ago

1.1.3

7 years ago

1.1.2

7 years ago

1.1.1

7 years ago

1.1.0

7 years ago

1.0.0

7 years ago