MCPcopy Index your code
hub / github.com/kirill-konshin/next-redux-wrapper

github.com/kirill-konshin/next-redux-wrapper @8.1.0

Chat with this repo
repository ↗ · DeepWiki ↗ · release 8.1.0 ↗ · + Follow
118 symbols 294 edges 58 files 0 documented · 0% 5 cross-repo links updated 2y ago9.0.0-rc.2 · 2023-02-07★ 2,67455 open issues
What it actually does AI analysis from the code graph — generated when you open this
loading…
README

Redux Wrapper for Next.js

npm version Build status Coverage Status

A HOC that brings Next.js and Redux together

:warning: The current version of this library only works with Next.js 9.3 and newer. If you are required to use Next.js 6-9 you can use version 3-5 of this library, see branches. Otherwise, consider upgrading Next.js. :warning:

Contents:

Motivation

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.

In Next.js example https://github.com/vercel/next.js/blob/canary/examples/with-redux-thunk/store.js#L23 store is being replaced on navigation. Redux will re-render components even with memoized selectors (createSelector from recompose) if store is replaced: https://codesandbox.io/s/redux-store-change-kzs8q, which may affect performance of the app by causing a huge re-render of everything, even what did not change. This library makes sure store remains the same.

Installation

npm install next-redux-wrapper react-redux --save

Note that next-redux-wrapper requires react-redux as peer dependency.

Usage

Live example: https://codesandbox.io/s/next-redux-wrapper-demo-7n2t5.

All examples are written in TypeScript. If you're using plain JavaScript just omit type declarations. These examples use vanilla Redux, if you're using Redux Toolkit, please refer to dedicated example.

Next.js has several data fetching mechanisms, this library can attach to any of them. But first you have to write some common code.

Please note that your reducer must have the HYDRATE action handler. HYDRATE action handler must properly reconciliate the hydrated state on top of the existing state (if any). This behavior was added in version 6 of this library. We'll talk about this special action later.

Create a file named store.ts:

// store.ts

import {createStore, AnyAction, Store} from 'redux';
import {createWrapper, Context, HYDRATE} from 'next-redux-wrapper';

export interface State {
  tick: string;
}

// create your reducer
const reducer = (state: State = {tick: 'init'}, action: AnyAction) => {
  switch (action.type) {
    case HYDRATE:
      // Attention! This will overwrite client state! Real apps should use proper reconciliation.
      return {...state, ...action.payload};
    case 'TICK':
      return {...state, tick: action.payload};
    default:
      return state;
  }
};

// create a makeStore function
const makeStore = (context: Context) => createStore(reducer);

// export an assembled wrapper
export const wrapper = createWrapper<Store<State>>(makeStore, {debug: true});

Same code in JavaScript (without types)

// store.js

import {createStore} from 'redux';
import {createWrapper, HYDRATE} from 'next-redux-wrapper';

// create your reducer
const reducer = (state = {tick: 'init'}, action) => {
  switch (action.type) {
    case HYDRATE:
      return {...state, ...action.payload};
    case 'TICK':
      return {...state, tick: action.payload};
    default:
      return state;
  }
};

// create a makeStore function
const makeStore = context => createStore(reducer);

// export an assembled wrapper
export const wrapper = createWrapper(makeStore, {debug: true});

wrapper.useWrappedStore

It is highly recommended to use pages/_app to wrap all pages at once, otherwise due to potential race conditions you may get Cannot update component while rendering another component:

import React, {FC} from 'react';
import {Provider} from 'react-redux';
import {AppProps} from 'next/app';
import {wrapper} from '../components/store';

const MyApp: FC<AppProps> = ({Component, ...rest}) => {
  const {store, props} = wrapper.useWrappedStore(rest);
  return (
    <Provider store={store}>
      <Component {...props.pageProps} />
    </Provider>
  );
};

Instead of wrapper.useWrappedStore you can also use legacy HOC, that can work with class-based components.

:warning: Next.js provides generic getInitialProps when using class MyApp extends App which will be picked up by wrapper, so you must not extend App as you'll be opted out of Automatic Static Optimization: https://err.sh/next.js/opt-out-auto-static-optimization. Just export a regular Functional Component as in the example above.

import React from 'react';
import {wrapper} from '../components/store';
import {AppProps} from 'next/app';

class MyApp extends React.Component<AppProps> {
  render() {
    const {Component, pageProps} = this.props;
    return <Component {...pageProps} />;
  }
}

export default wrapper.withRedux(MyApp);

State reconciliation during hydration

Each time when pages that have getStaticProps or getServerSideProps are opened by user the HYDRATE action will be dispatched. This may happen during initial page load and during regular page navigation. The payload of this action will contain the state at the moment of static generation or server side rendering, so your reducer must merge it with existing client state properly.

Simplest way is to use server and client state separation.

Another way is to use https://github.com/benjamine/jsondiffpatch to analyze diff and apply it properly:

import {HYDRATE} from 'next-redux-wrapper';

// create your reducer
const reducer = (state = {tick: 'init'}, action) => {
  switch (action.type) {
    case HYDRATE:
      const stateDiff = diff(state, action.payload) as any;
      const wasBumpedOnClient = stateDiff?.page?.[0]?.endsWith('X'); // or any other criteria
      return {
        ...state,
        ...action.payload,
        page: wasBumpedOnClient ? state.page : action.payload.page, // keep existing state or use hydrated
      };
    case 'TICK':
      return {...state, tick: action.payload};
    default:
      return state;
  }
};

Or like this (from with-redux-wrapper example in Next.js repo):

const reducer = (state, action) => {
  if (action.type === HYDRATE) {
    const nextState = {
      ...state, // use previous state
      ...action.payload, // apply delta from hydration
    };
    if (state.count) nextState.count = state.count; // preserve count value on client side navigation
    return nextState;
  } else {
    return combinedReducer(state, action);
  }
};

Configuration

The createWrapper function accepts makeStore as its first argument. The makeStore function should return a new Redux Store instance each time it's called. No memoization is needed here, it is automatically done inside the wrapper.

createWrapper also optionally accepts a config object as a second parameter:

  • debug (optional, boolean) : enable debug logging
  • serializeState and deserializeState: custom functions for serializing and deserializing the redux state, see Custom serialization and deserialization.

When makeStore is invoked it is provided with a Next.js context, which could be NextPageContext or AppContext or getStaticProps or getServerSideProps context depending on which lifecycle function you will wrap.

Some of those contexts (getServerSideProps always, and NextPageContext, AppContext sometimes if page is rendered on server) can have request and response related properties:

  • req (IncomingMessage)
  • res (ServerResponse)

Although it is possible to create server or client specific logic in both makeStore, I highly recommend that they do not have different behavior. This may cause errors and checksum mismatches which in turn will ruin the whole purpose of server rendering.

getStaticProps

This section describes how to attach to getStaticProps lifecycle function.

Let's create a page in pages/pageName.tsx:

import React from 'react';
import {NextPage} from 'next';
import {useSelector} from 'react-redux';
import {wrapper, State} from '../store';

export const getStaticProps = wrapper.getStaticProps(store => ({preview}) => {
  console.log('2. Page.getStaticProps uses the store to dispatch things');
  store.dispatch({
    type: 'TICK',
    payload: 'was set in other page ' + preview,
  });
});

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

{tick}

;
};

export default Page;

Same code in JavaScript (without types)

import React from 'react';
import {useSelector} from 'react-redux';
import {wrapper} from '../store';

export const getStaticProps = wrapper.getStaticProps(store => ({preview}) => {
  console.log('2. Page.getStaticProps uses the store to dispatch things');
  store.dispatch({
    type: 'TICK',
    payload: 'was set in other page ' + preview,
  });
});

// you can also use `connect()` instead of hooks
const Page = () => {
  const {tick} = useSelector(state => state);
  return 

{tick}

;
};

export default Page;

:warning: Each time when pages that have getStaticProps are opened by user the HYDRATE action will be dispatched. The payload of this action will contain the state at the moment of static generation, it will not have client state, so your reducer must merge it with existing client state properly. More about this in Server and Client State Separation.

Although you can wrap individual pages (and not wrap the pages/_app) it is not recommended, see last paragraph in usage section.

getServerSideProps

This section describes how to attach to getServerSideProps lifecycle function.

Let's create a page in pages/pageName.tsx:

import React from 'react';
import {NextPage} from 'next';
import {connect} from 'react-redux';
import {wrapper, State} from '../store';

export const getServerSideProps = wrapper.getServerSideProps(store => ({req, res, ...etc}) => {
  console.log('2. Page.getServerSideProps uses the store to dispatch things');
  store.dispatch({type: 'TICK', payload: 'was set in other page'});
});

// Page itself is not connected to Redux Store, it has to render Provider to allow child components to connect to Redux Store
const Page: NextPage<State> = ({tick}) => 

{tick}

;

// you can also use Redux `useSelector` and other hooks instead of `connect()`
export default connect((state: State) => state)(Page);

Same code in JavaScript (without types)

```js import React from 'react'; import {connect} from 'react-redux'; import {wrapper} from '../store';

export const getServerSideProps = wrapper.getServerSideProps(store => ({req, res, ...etc}) => { console.log('2. Page.getServerSideProps uses the store to dispatch things'); store.dispatch({type: 'TICK', payload: 'was set in other page'}); });

// Page itself is not conn

Extension points exported contracts — how you extend this code

Core symbols most depended-on inside this repo

Shape

Function 80
Interface 28
Class 6
Method 4

Languages

TypeScript100%

Modules by API surface

packages/demo-redux-toolkit/store.ts24 symbols
packages/wrapper/src/index.tsx22 symbols
packages/wrapper/tests/testlib.tsx6 symbols
packages/demo/src/pages/index.tsx5 symbols
packages/demo/src/pages/_app.tsx3 symbols
packages/demo-saga/src/pages/_app.tsx3 symbols
packages/wrapper/tests/server.spec.tsx2 symbols
packages/demo/src/pages/static.tsx2 symbols
packages/demo/src/pages/server.tsx2 symbols
packages/demo/src/components/reducer.tsx2 symbols
packages/demo-saga/src/pages/index.tsx2 symbols
packages/demo-saga/src/components/store.tsx2 symbols

For agents

$ claude mcp add next-redux-wrapper \
  -- python -m otcore.mcp_server <graph>

⬇ download graph artifact

Ask about this repo answers extend the page