Giter VIP home page Giter VIP logo

next-redux-wrapper's Introduction

Redux Wrapper for Next.js

npm version Build status Coverage Status

A HOC that brings Next.js and Redux together

⚠️ 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. ⚠️

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.

⚠️ 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 <div>{tick}</div>;
};

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 <div>{tick}</div>;
};

export default Page;

⚠️ 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}) => <div>{tick}</div>;

// 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)
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 connected to Redux Store, it has to render Provider to allow child components to connect to Redux Store
const Page = ({tick}) => <div>{tick}</div>;

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

⚠️ Each time when pages that have getServerSideProps are opened by user the HYDRATE action will be dispatched. The payload of this action will contain the state at the moment of server side rendering, 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.

Page.getInitialProps

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

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

Page.getInitialProps = wrapper.getInitialPageProps(store => ({pathname, req, res}) => {
  console.log('2. Page.getInitialProps uses the store to dispatch things');
  store.dispatch({
    type: 'TICK',
    payload: 'was set in error page ' + pathname,
  });
});

export default Page;
Same code in JavaScript (without types)
import React, {Component} from 'react';
import {wrapper} from '../store';

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

Page.getInitialProps = wrapper.getInitialPageProps(store => ({pathname, req, res}) => {
  console.log('2. Page.getInitialProps uses the store to dispatch things');
  store.dispatch({
    type: 'TICK',
    payload: 'was set in error page ' + pathname,
  });
});

export default Page;

Keep in mind that req and res may not be available if getInitialProps is called on client side.

Stateless function component also can be replaced with class:

class Page extends Component {

    public static getInitialProps = wrapper.getInitialPageProps(store => () => ({ ... }));

    render() {
        // stuff
    }
}

export default Page;

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

App

⚠️ You can dispatch actions from the pages/_app too. But this mode is not compatible with Next.js 9's Auto Partial Static Export feature, see the explanation below.

The wrapper can also be attached to your _app component (located in /pages). All other components can use the connect function of react-redux.

// pages/_app.tsx

import React from 'react';
import App, {AppInitialProps} from 'next/app';
import {wrapper} from '../components/store';
import {State} from '../components/reducer';

// Since you'll be passing more stuff to Page
declare module 'next/dist/next-server/lib/utils' {
  export interface NextPageContext {
    store: Store<State>;
  }
}

class MyApp extends App<AppInitialProps> {
  public static getInitialProps = wrapper.getInitialAppProps(store => async context => {
    store.dispatch({type: 'TOE', payload: 'was set in _app'});

    return {
      pageProps: {
        // https://nextjs.org/docs/advanced-features/custom-app#caveats
        ...(await App.getInitialProps(context)).pageProps,
        // Some custom thing for all pages
        pathname: ctx.pathname,
      },
    };
  });

  public render() {
    const {Component, pageProps} = this.props;

    return <Component {...pageProps} />;
  }
}

export default wrapper.withRedux(MyApp);
Same code in JavaScript (without types)
// pages/_app.tsx

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

class MyApp extends App {
  static getInitialProps = wrapper.getInitialAppProps(store => async context => {
    store.dispatch({type: 'TOE', payload: 'was set in _app'});

    return {
      pageProps: {
        // https://nextjs.org/docs/advanced-features/custom-app#caveats
        ...(await App.getInitialProps(context)).pageProps,
        // Some custom thing for all pages
        pathname: ctx.pathname,
      },
    };
  });

  render() {
    const {Component, pageProps} = this.props;

    return <Component {...pageProps} />;
  }
}

export default wrapper.withRedux(MyApp);

Then all pages can simply be connected (the example considers page components):

// pages/xxx.tsx

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

const Page: NextPage<State> = ({foo, custom}) => (
  <div>
    <div>Prop from Redux {foo}</div>
    <div>Prop from getInitialProps {custom}</div>
  </div>
);

// No need to wrap pages if App was wrapped
Page.getInitialProps = ({store, pathname, query}: NextPageContext) => {
  store.dispatch({type: 'FOO', payload: 'foo'}); // The component can read from the store's state when rendered
  return {custom: 'custom'}; // You can pass some custom props to the component from here
};

export default connect((state: State) => state)(Page);
Same code in JavaScript (without types)
// pages/xxx.js

import React from 'react';
import {connect} from 'react-redux';

const Page = ({foo, custom}) => (
  <div>
    <div>Prop from Redux {foo}</div>
    <div>Prop from getInitialProps {custom}</div>
  </div>
);

// No need to wrap pages if App was wrapped
Page.getInitialProps = ({store, pathname, query}) => {
  store.dispatch({type: 'FOO', payload: 'foo'}); // The component can read from the store's state when rendered
  return {custom: 'custom'}; // You can pass some custom props to the component from here
};

export default connect(state => state)(Page);

App and getServerSideProps or getStaticProps at page level

You can also use getServerSideProps or getStaticProps at page level, in this case HYDRATE action will be dispatched twice: with state after App.getInitialProps and then with state after getServerSideProps or getStaticProps:

  • If you use getServerSideProps at page level then store in getServerSideProps will be executed after App.getInitialProps and will have state from it, so second HYDRATE will have full state from both
  • ⚠️ If you use getStaticProps at page level then store in getStaticProps will be executed at compile time and will NOT have state from App.getInitialProps because they are executed in different contexts and state cannot be shared. First HYDRATE actions state after App.getInitialProps and second will have state after getStaticProps (even though it was executed earlier in time).

Simplest way to ensure proper merging is to drop initial values from action.payload:

const reducer = (state: State = {app: 'init', page: 'init'}, action: AnyAction) => {
  switch (action.type) {
    case HYDRATE:
      if (action.payload.app === 'init') delete action.payload.app;
      if (action.payload.page === 'init') delete action.payload.page;
      return {...state, ...action.payload};
    case 'APP':
      return {...state, app: action.payload};
    case 'PAGE':
      return {...state, page: action.payload};
    default:
      return state;
  }
};
Same code in JavaScript (without types)
const reducer = (state = {app: 'init', page: 'init'}, action) => {
  switch (action.type) {
    case HYDRATE:
      if (action.payload.app === 'init') delete action.payload.app;
      if (action.payload.page === 'init') delete action.payload.page;
      return {...state, ...action.payload};
    case 'APP':
      return {...state, app: action.payload};
    case 'PAGE':
      return {...state, page: action.payload};
    default:
      return state;
  }
};

Assume page only dispatches PAGE action and App only APP, this makes state merging safe.

More about that in Server and Client state separation.

How it works

Using next-redux-wrapper ("the wrapper"), the following things happen on a request:

  • Phase 1: getInitialProps/getStaticProps/getServerSideProps

    • The wrapper creates a server-side store (using makeStore) with an empty initial state. In doing so it also provides the Request and Response objects as options to makeStore.
    • In App mode:
      • The wrapper calls the _app's getInitialProps function and passes the previously created store.
      • Next.js takes the props returned from the _app's getInitialProps method, along with the store's state.
    • In per-page mode:
      • The wrapper calls the Page's getXXXProps function and passes the previously created store.
      • Next.js takes the props returned from the Page's getXXXProps method, along with the store's state.
  • Phase 2: SSR

    • The wrapper creates a new store using makeStore
    • The wrapper dispatches HYDRATE action with the previous store's state as payload
    • That store is passed as a property to the _app or page component.
    • Connected components may alter the store's state, but the modified state will not be transferred to the client.
  • Phase 3: Client

    • The wrapper creates a new store
    • The wrapper dispatches HYDRATE action with the state from Phase 1 as payload
    • That store is passed as a property to the _app or page component.
    • The wrapper persists the store in the client's window object, so it can be restored in case of HMR.

Note: The client's state is not persisted across requests (i.e. Phase 1 always starts with an empty state). Hence, it is reset on page reloads. Consider using Redux persist if you want to persist state between requests.

Tips and Tricks

Redux Toolkit

Since version 7.0 first-class support of @reduxjs/toolkit has been added.

Full example: https://github.com/kirill-konshin/next-redux-wrapper/blob/master/packages/demo-redux-toolkit.

import {configureStore, createSlice, ThunkAction} from '@reduxjs/toolkit';
import {Action} from 'redux';
import {createWrapper, HYDRATE} from 'next-redux-wrapper';

export const subjectSlice = createSlice({
  name: 'subject',

  initialState: {} as any,

  reducers: {
    setEnt(state, action) {
      return action.payload;
    },
  },

  extraReducers: {
    [HYDRATE]: (state, action) => {
      console.log('HYDRATE', state, action.payload);
      return {
        ...state,
        ...action.payload.subject,
      };
    },
  },
});

const makeStore = () =>
  configureStore({
    reducer: {
      [subjectSlice.name]: subjectSlice.reducer,
    },
    devTools: true,
  });

export type AppStore = ReturnType<typeof makeStore>;
export type AppState = ReturnType<AppStore['getState']>;
export type AppDispatch = AppStore['dispatch'];
export type AppThunk<ReturnType = void> = ThunkAction<ReturnType, AppState, unknown, Action>;

// Use throughout your app instead of plain `useDispatch` and `useSelector`
export const useAppDispatch = () => useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;

export const fetchSubject =
  (id: any): AppThunk =>
  async dispatch => {
    const timeoutPromise = (timeout: number) => new Promise(resolve => setTimeout(resolve, timeout));

    await timeoutPromise(200);

    dispatch(
      subjectSlice.actions.setEnt({
        [id]: {
          id,
          name: `Subject ${id}`,
        },
      }),
    );
  };

export const wrapper = createWrapper<AppStore>(makeStore);

export const selectSubject = (id: any) => (state: AppState) => state?.[subjectSlice.name]?.[id];

It is recommended to export typed State and ThunkAction:

export type AppStore = ReturnType<typeof makeStore>;
export type AppState = ReturnType<AppStore['getState']>;
export type AppThunk<ReturnType = void> = ThunkAction<ReturnType, AppState, unknown, Action>;

Server and Client state separation

Each time when pages that have getStaticProps or getServerSideProps 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 or server side rendering, so your reducer must merge it with existing client state properly.

The easiest and most stable way to make sure nothing is accidentally overwritten is to make sure that your reducer applies client side and server side actions to different substates of your state and they never clash:

export interface State {
  server: any;
  client: any;
}

const reducer = (state: State = {tick: 'init'}, action: AnyAction) => {
  switch (action.type) {
    case HYDRATE:
      return {
        ...state,
        server: {
          ...state.server,
          ...action.payload.server,
        },
      };
    case 'SERVER_ACTION':
      return {
        ...state,
        server: {
          ...state.server,
          tick: action.payload,
        },
      };
    case 'CLIENT_ACTION':
      return {
        ...state,
        client: {
          ...state.client,
          tick: action.payload,
        },
      };
    default:
      return state;
  }
};
Same code in JavaScript (without types)
const reducer = (state = {tick: 'init'}, action) => {
  switch (action.type) {
    case HYDRATE:
      return {
        ...state,
        server: {
          ...state.server,
          ...action.payload.server,
        },
      };
    case 'SERVER_ACTION':
      return {
        ...state,
        server: {
          ...state.server,
          tick: action.payload,
        },
      };
    case 'CLIENT_ACTION':
      return {
        ...state,
        client: {
          ...state.client,
          tick: action.payload,
        },
      };
    default:
      return state;
  }
};

If you prefer an isomorphic approach for some (preferably small) portions of your state, you can share them between client and server on server-rendered pages using next-redux-cookie-wrapper, an extension to next-redux-wrapper. In this case, for selected substates, the server is aware of the client's state (unless in getStaticProps) and there is no need to separate server and client state.

Also, you can use a library like https://github.com/benjamine/jsondiffpatch to analyze diff and apply it properly.

Document

I don't recommend using withRedux in pages/_document.js, Next.JS does not 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 pages/_document.

Error Pages

Error pages can also be wrapped the same way as any other pages.

Transition to an error page (pages/_error.js template) will cause pages/_app.js to be applied but it is always a full page transition (not HTML5 pushState), so client will have the store created from scratch using state from the server. So unless you persist the store on the client somehow the resulting previous client state will be ignored.

Async actions

You can use https://github.com/reduxjs/redux-thunk to dispatch async actions:

function someAsyncAction(id) {
  return async function (dispatch, getState) {
    return someApiCall(id).then(res => {
      dispatch({
        type: 'FOO',
        payload: res,
      });
    });
  };
}

// usage
await store.dispatch(someAsyncAction());

You can also install https://github.com/pburtchaell/redux-promise-middleware in order to dispatch Promises as async actions. Follow the installation guide of the library, then you'll be able to handle it like this:

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

// usage
await store.dispatch(someAsyncAction());

Custom serialization and deserialization

If you are storing complex types such as Immutable.JS or JSON objects in your state, a custom serialize and deserialize handler might be handy to serialize the redux state on the server and deserialize it again on the client. To do so, provide serializeState and deserializeState as config options to withRedux.

The reason is that state snapshot is transferred over the network from server to client as a plain object.

Example of a custom serialization of an Immutable.JS state using json-immutable:

const {serialize, deserialize} = require('json-immutable');

createWrapper({
  serializeState: state => serialize(state),
  deserializeState: state => deserialize(state),
});

Same thing using Immutable.JS:

const {fromJS} = require('immutable');

createWrapper({
  serializeState: state => state.toJS(),
  deserializeState: state => fromJS(state),
});

Usage with Redux Saga

[Note, this method may be unsafe - make sure you put a lot of thought into handling async sagas correctly. Race conditions happen very easily if you aren't careful.] To utilize Redux Saga, one simply has to make some changes to their makeStore function. Specifically, redux-saga needs to be initialized inside this function, rather than outside of it. (I did this at first, and got a nasty error telling me Before running a Saga, you must mount the Saga middleware on the Store using applyMiddleware). Here is how one accomplishes just that. This is just slightly modified from the setup example at the beginning of the docs. Keep in mind that this setup will opt you out of Automatic Static Optimization: https://err.sh/next.js/opt-out-auto-static-optimization.

Create your root saga as usual, then implement the store creator:

import {createStore, applyMiddleware, Store} from 'redux';
import {createWrapper, Context} from 'next-redux-wrapper';
import createSagaMiddleware, {Task} from 'redux-saga';
import reducer, {State} from './reducer';
import rootSaga from './saga';

export interface SagaStore extends Store {
  sagaTask?: Task;
}

export const makeStore = (context: Context) => {
  // 1: Create the middleware
  const sagaMiddleware = createSagaMiddleware();

  // 2: Add an extra parameter for applying middleware:
  const store = createStore(reducer, applyMiddleware(sagaMiddleware));

  // 3: Run your sagas on server
  (store as SagaStore).sagaTask = sagaMiddleware.run(rootSaga);

  // 4: now return the store:
  return store;
};

export const wrapper = createWrapper<Store<State>>(makeStore, {debug: true});
Same code in JavaScript (without types)
import {createStore, applyMiddleware} from 'redux';
import {createWrapper} from 'next-redux-wrapper';
import createSagaMiddleware from 'redux-saga';
import reducer from './reducer';
import rootSaga from './saga';

export const makeStore = context => {
  // 1: Create the middleware
  const sagaMiddleware = createSagaMiddleware();

  // 2: Add an extra parameter for applying middleware:
  const store = createStore(reducer, applyMiddleware(sagaMiddleware));

  // 3: Run your sagas on server
  store.sagaTask = sagaMiddleware.run(rootSaga);

  // 4: now return the store:
  return store;
};

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

Using pages/_app

Then in the pages/_app wait stop saga and wait for it to finish when execution is on server:

import React from 'react';
import App, {AppInitialProps} from 'next/app';
import {END} from 'redux-saga';
import {SagaStore, wrapper} from '../components/store';

class WrappedApp extends App<AppInitialProps> {
  public static getInitialProps = wrapper.getInitialAppProps(store => async context => {
    // 1. Wait for all page actions to dispatch
    const pageProps = {
      // https://nextjs.org/docs/advanced-features/custom-app#caveats
      ...(await App.getInitialProps(context)).pageProps,
    };

    // 2. Stop the saga if on server
    if (context.ctx.req) {
      store.dispatch(END);
      await (store as SagaStore).sagaTask.toPromise();
    }

    // 3. Return props
    return {pageProps};
  });

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

export default wrapper.withRedux(WrappedApp);
Same code in JavaScript (without types)
import React from 'react';
import App from 'next/app';
import {END} from 'redux-saga';
import {SagaStore, wrapper} from '../components/store';

class WrappedApp extends App {
    static getInitialProps = wrapper.getInitialAppProps(store => async context => {
        // 1. Wait for all page actions to dispatch
        const pageProps = {
            // https://nextjs.org/docs/advanced-features/custom-app#caveats
            ...(await App.getInitialProps(context)).pageProps,
        };

        // 2. Stop the saga if on server
        if (context.ctx.req) {
            store.dispatch(END);
            await store.sagaTask.toPromise();
        }

        // 3. Return props
        return {pageProps};
    });

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

export default wrapper.withRedux(WrappedApp);

Using getServerSideProps or getStaticProps

In order to use it with getServerSideProps or getStaticProps you need to await for sagas in each page's handler:

export const getServerSideProps = ReduxWrapper.getServerSideProps(async ({store, req, res, ...etc}) => {
  // regular stuff
  store.dispatch(ApplicationSlice.actions.updateConfiguration());
  // end the saga
  store.dispatch(END);
  await store.sagaTask.toPromise();
});

Usage without getInitialProps inside _app

If you don't want to opt-out of automatic pre-rendering in your Next.js app, you can manage server-called sagas on a per page basis like the official Next.js "with Redux Saga" example does. If you do go with this option, please ensure that you await any and all sagas within any Next.js page methods. If you miss it on one of pages you'll end up with inconsistent state being sent to client. So, we consider waiting in _app to be automatically safer, but obviously the main drawback is opting out of automatic static exports.

Usage with Redux Persist

If you only need to persist small portions of your state, next-redux-cookie-wrapper might be an easy alternative to Redux Persist that supports SSR.

Boilerplate: https://github.com/fazlulkarimweb/with-next-redux-wrapper-redux-persist

Honestly, I think that putting a persistence gate is not necessary because the 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 the 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}) => {
  // Usual stuff with HYDRATE handler
  if (type === SET_CLIENT_STATE) {
    return {
      ...state,
      fromClient: payload,
    };
  }
  return state;
};

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

const makeStore = () => {
  const isServer = typeof window === 'undefined';

  if (isServer) {
    return makeConfiguredStore(reducer);
  } 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);

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

    return store;
  }
};

export const wrapper = createWrapper(makeStore);

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

And then in Next.js _app page you can use bare context access to get the store (https://react-redux.js.org/api/provider#props):

// pages/_app.tsx
import React from 'react';
import App from 'next/app';
import {ReactReduxContext} from 'react-redux';
import {wrapper} from './lib/redux';
import {PersistGate} from 'redux-persist/integration/react';

export default wrapper.withRedux(
  class MyApp extends App {
    render() {
      const {Component, pageProps} = this.props;
      return (
        <ReactReduxContext.Consumer>
          {({store}) => (
            <PersistGate persistor={store.__persistor} loading={<div>Loading</div>}>
              <Component {...pageProps} />
            </PersistGate>
          )}
        </ReactReduxContext.Consumer>
      );
    }
  },
);

Or using hooks:

// pages/_app.tsx
import React from 'react';
import App from 'next/app';
import {useStore} from 'react-redux';
import {wrapper} from './lib/redux';
import {PersistGate} from 'redux-persist/integration/react';

export default wrapper.withRedux(({Component, pageProps}) => {
  const store = useStore();
  return (
    <PersistGate persistor={store.__persistor} loading={<div>Loading</div>}>
      <Component {...pageProps} />
    </PersistGate>
  );
});

And then in Next.js 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>
));

Upgrade from 6.x to 7.x

  1. Signature of createWrapper has changed: instead of createWrapper<State> you should use createWrapper<Store<State>>, all types will be automatically inferred from Store.

  2. GetServerSidePropsContext and GetStaticPropsContext are no longer exported from next-redux-wrapper, you should use GetServerSideProps, GetServerSidePropsContext, GetStaticProps and GetStaticPropsContext directly from next.

  3. All signatures like ({store, req, res, ...}) => { ... } were changed to store => ({req, res, ...}) => { ... } in order to keep Next.js internals free of modifications and for better typings support.

  4. In version 7.x you have to manually wrap all getInitialProps with proper wrappers: wrapper.getInitialPageProps and wrapper.getInitialAppProps.

  5. window.NEXT_REDUX_WRAPPER_STORE has been removed as it was causing issues with hot reloading

Upgrade from 5.x to 6.x

Major change in the way how things are wrapped in version 6.

  1. Default export withRedux is marked deprecated, you should create a wrapper const wrapper = createWrapper(makeStore, {debug: true}) and then use wrapper.withRedux(MyApp).

  2. Your makeStore function no longer gets initialState, it only receives the context: makeStore(context: Context). Context could be NextPageContext or AppContext or getStaticProps or getServerSideProps context depending on which lifecycle function you will wrap. Instead, you need to handle the HYDRATE action in the reducer. 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.

  3. App should no longer wrap its children with Provider, it is now done internally.

  4. isServer is not passed in context/props, use your own function or simple check const isServer = typeof window === 'undefined' or !!context.req or !!context.ctx.req.

  5. store is not passed to wrapped component props.

  6. WrappedAppProps was renamed to WrapperProps.

Upgrade from 1.x to 2.x

If your project was using Next.js 5 and Next Redux Wrapper 1.x these instructions will help you to upgrade to 2.x.

  1. Upgrade Next.js and Wrapper

    $ npm install next@6 --save-dev
    $ npm install next-redux-wrapper@latest --save
  2. Replace all usages of import withRedux from "next-redux-wrapper"; and withRedux(...)(WrappedComponent) in all your pages with plain React Redux connect HOC:

    import {connect} from "react-redux";
    
    export default connect(...)(WrappedComponent);

    You also may have to reformat your wrapper object-based config to simple React Redux config.

  3. Create the pages/_app.js file with the following minimal code:

    // pages/_app.js
    import React from 'react'
    import {Provider} from 'react-redux';
    import App from 'next/app';
    import {wrapper} from '../store';
    
    class MyApp extends App {
        static async getInitialProps = (context) => ({
            pageProps: {
                // https://nextjs.org/docs/advanced-features/custom-app#caveats
                ...(await App.getInitialProps(context)).pageProps,
            }
        });
    
        render() {
            const {Component, pageProps} = this.props;
            return (
                <Component {...pageProps} />
            );
        }
    
    }
    
    export default wrapper.withRedux(MyApp);
  4. Follow Next.js 6 upgrade instructions for all your components (props.router instead of props.url and so on)

That's it. Your project should now work the same as before.

Resources

next-redux-wrapper's People

Contributors

1ilsang avatar bjoluc avatar bobmk2 avatar brycejacobs avatar cullylarson avatar iamsolankiamit avatar jason-ivy avatar kiraind avatar kirill-konshin avatar montoias avatar mpppk avatar nabeelvalley avatar nikandlv avatar noahtallen avatar odykyi avatar olistic avatar pikachews avatar popenkomaksim avatar qvil avatar remolueoend avatar robbieaverill avatar robert-j-webb avatar rwieruch avatar stevegeek avatar sushant00 avatar tobiastornros avatar wi-ski avatar ytliusvn avatar zhengyutay avatar zrod avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

next-redux-wrapper's Issues

Maintaining State / Redux Store Across Pages

New to next.js, dumb questions:

  1. How do I maintain state (Redux store) when I come back to a page?

  2. How do I maintain shared state across pages?

  3. So then if I just have a page level store, how is that really different from just using inherent React page state?

With an SPA - part of the point of Redux was to have a global store that any page could access and can reuse any collected data. Would this have to be stored in a server side session (likely backed by Redis) - and is that a bad idea? Would you then have the notion of 2 stores (1 client/1 server).

Great if you can help with my confusion.

Populating store with getInitialProps

While I'm able to get data and pass them as props to the component I'm unable to populate the store.
I'm successfully hitting my action and reducer, I see the data in the terminal when the page loads but the store just stays with the default value.

In my component

  static async getInitialProps ({ store, isServer}) {
    const res = await store.dispatch(getMovies(isServer));
    return res.data;
  }

Action

export const getMovies = (isServer) => dispatch => (
      axios.get('https://api.themoviedb.org/xxxxxx')
          .then(response => dispatch({ type: constants.GET_MOVIES, data: response.data }))
);

Reducer

const movies = (state = initialState, action) => {
  switch (action.type) {
    case constants.GET_MOVIES:
      return {...state, foo: action.data};
    default:
      return {foo: 'cucumber'}
  }
};

I was thinking it had something to do with here, but trying to pass in state has had no luck

export default withRedux(initStore, (state) => ({foo: state.foo}), mapDispatchToProps)(Movies)

@types/next-redux-wrapper error

node_modules/@types/next-redux-wrapper/index.d.ts(52,27): error TS2314: Generic type 'MapStateToPropsParam' requires 3 type argument(s).
tsconfig.json: "noImplicitAny": true,

Seems its only accepts 2 params
mapStateToProps?: MapStateToPropsParam<TStateProps, TOwnProps>;

State set getInitialProps not available in children

Let's start with a little of background:

I have a page component which fetches some data inside getInitialProps and dispatches actions to the redux store.
Somewhere inside this page component I have a component which is connected to the store by using withRedux HOC.

Now the issue:
When rendering on the server, the child component doesn't see the state set by parent in getInitialProps, it receives the default state. Shouldn't the store be the same? So if a parent component dispatches actions which set some state in the store, child components have access to this new state from the store?

How to get working with react-responsive

So I got the package working. However, I'm finding that it doesn't play nice with react-responsive

Normally, once the store is initiated and sent to the client, the browser reducer will update itself to the proper window size. However, that doesn't happen when used in this project. Any suggestions?

react-responsive repo

Wrapped component loses defined props.

Take the following example (that I needed when testing with Jest):

import React from 'react';
import { render } from 'enzyme';
import App from '../pages/list/_single.js'; // 👈 next-redux-wrapper

describe('List errors', () => {
  test('errors correctly', () => {
    const app = render(
      <App error="__not_found__" />
    );

    // App never sees the `error` property
    expect(app.find('h1').text()).toContain('went wrong');
  });
});

This is because the next-redux-wrapper is swallowing properties here.

You can see it in this replicated test:

I tried changing a couple of lines, in particular:

let { store, isServer, initialState, initialProps, ...rest } = props;
// ...
return React.createElement(
  Provider,
  {store: store},
  React.createElement(ConnectedCmp, { ...initialProps, ...rest })
);

And then ran the same test, the bar value passes through correct:

Would you be happy for a PR for this change?

store.getState is not a function

Getting this error and unable to figure out why. Following the instructions as closely as possible:

store.getState is not a function
TypeError: store.getState is not a function
    at Object.runComponentSelector [as run] (http://localhost:3000/_next/1505232471916/commons.js:85141:46)
    at Connect.initSelector (http://localhost:3000/_next/1505232471916/commons.js:85293:23)
    at new Connect (http://localhost:3000/_next/1505232471916/commons.js:85234:15)
    at http://localhost:3000/_next/1505232471916/commons.js:29191:18
    at measureLifeCyclePerf (http://localhost:3000/_next/1505232471916/commons.js:28972:12)
    at ReactCompositeComponentWrapper._constructComponentWithoutOwner (http://localhost:3000/_next/1505232471916/commons.js:29190:16)
    at ReactCompositeComponentWrapper._constructComponent (http://localhost:3000/_next/1505232471916/commons.js:29176:21)
    at ReactCompositeComponentWrapper.mountComponent (http://localhost:3000/_next/1505232471916/commons.js:29084:21)
    at mountComponent (http://localhost:3000/_next/1505232471916/commons.js:2295:35)
    at Object._ReactReconciler2.default.mountComponent (http://localhost:3000/_next/1505232471916/commons.js:14921:35)

Preserve default props

If I use your packages, it will remove default props url (default props from NextJS itself) in my component.

How can I fix that?

Experiencing unmount on hot loading

Not sure it is react-redux or this, but I have tried across different versions (4 to 5) of react-redux and it occurs consistently, so I believe it would be better to post here (if you think it is not related to this lib, feel free to close please!)

The issue is simple - whenever hot loading happens via Next.js, components from root get unmounted and re-mounted. I can confirm this happens in the official example too, which is also using your library. Every time I save, I can see componentDidMount() and componentWillUnmount() gets called.

Custom serialize

Hi that is not an issue. I'm working with parse and need to custom encode the data before it is sent to the client so i will be able to decode it and get the original data in createStore function, i don't know if this can be achieved with next-redux-wrapper if so please how can i do it.

Difference between client and server state

My client state only consists of the default arguments passed to each individual reducer. Cannot get it to match server state. What am I messing up here? Let me know if you need to see any more files.

store/index.js

import { createStore, applyMiddleware, compose } from 'redux'
import { composeWithDevTools } from 'redux-devtools-extension'
import thunk from 'redux-thunk'
import rootReducer from '../reducers'
import { createLogger } from 'redux-logger'
import withRedux from 'next-redux-wrapper'

const middleware = [thunk]

if (process.env.NODE_ENV === 'development') {
  // middleware.push(createLogger())
}

function initStore() {
  return createStore(rootReducer, composeWithDevTools(applyMiddleware(...middleware)))
}

export default initStore

pages/index.js

import React from 'react'
import defaultPage from '../hocs/defaultPage'
import NotAuthenticated from '~/NotAuthenticated'

function Index ({ isAuthenticated, children }) {
  if (isAuthenticated) {
    return (
      <div>
        { children }
      </div>
    )
  } else {
    return ( 
      <div>
        <NotAuthenticated/>
      </div>
    )
  }
}

export default defaultPage(Index)

hocs/defaultPage.js

import React from 'react'
import { getUserFromCookie, getUserFromLocalStorage } from '../utils/auth'
import initStore from '../store'
import saveUserAgent from '!/saveUserAgent'
import saveCurrentPage from '!/saveCurrentPage'
import withRedux from 'next-redux-wrapper'
import getMuiTheme from 'material-ui/styles/getMuiTheme'
import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider'
import PropTypes from 'prop-types'
import NProgress from 'nprogress'
import Router from 'next/router'
import DesktopNavbar from '~/DesktopNavbar'
import Filters from '~/Filters'
import WidgetArea from '~/WidgetArea'
import saveWidgetConfig from '!/saveWidgetConfig' // only dispatch if diff in future
import styled, { ThemeProvider } from 'styled-components'
import defaultTheme from '../themes/default.theme'
import fetchUserData from '../actions/actionCreators/fetchUserData';
import _ from 'lodash'

const Container = styled.div`
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
`;

Router.onRouteChangeStart = (url) => {
  NProgress.start()
}
Router.onRouteChangeComplete = () => NProgress.done()
Router.onRouteChangeError = () => NProgress.done()

function wrapInAuthAndMaterialUI (Page) {
  class DefaultPage extends React.Component {
    static async getInitialProps (ctx) {
      const loggedUser = process.browser ? getUserFromLocalStorage() : getUserFromCookie(ctx.req);
      const pageProps = Page.getInitialProps && Page.getInitialProps(ctx);
      const { store, isServer, pathname } = ctx;
      const initPageProps = {
        ...pageProps,
        loggedUser,
        currentUrl: pathname,
        isAuthenticated: !!loggedUser,
        userAgent: store.getState().userAgent
      };
      return new Promise((resolve) => {
        store.dispatch(fetchUserData())
          .then(() => {
            resolve(initPageProps)
          })
      })
    }
    
    constructor (props) {
      super(props)
      this.logout = this.logout.bind(this)
    }

    getChildContext() {
      return { muiTheme: getMuiTheme({ userAgent: this.props.userAgent }) };
    }

    logout (eve) {
      if (eve.key === 'logout') {
        Router.push(`/?logout=${eve.newValue}`)
      }
    }

    componentDidMount() {
      window.addEventListener('storage', this.logout, false)
    }

    componentWillUnmount() {
      window.removeEventListener('storage', this.logout, false)
    }

    render () {
      // console.log('hocprops', this.props)
      return (
        <ThemeProvider theme={defaultTheme}>
          <MuiThemeProvider muiTheme={ getMuiTheme({ userAgent: this.props.userAgent }) }>
            <Container>
              <Page { ...this.props }>
                <DesktopNavbar/>
                <Filters/>
                <WidgetArea/>
              </Page>
            </Container>
          </MuiThemeProvider>
        </ThemeProvider>
      )
    }
  }
  DefaultPage.childContextTypes = {
    muiTheme: PropTypes.object,
  }
  
  return withRedux(initStore)(DefaultPage)
}

export default wrapInAuthAndMaterialUI

Using a HOC breaks hot reloading

I am using next-redux-wrapper in a HOC that handles a few tasks that each page needs (like authentication).

It works great, except it breaks hot-reloading by losing component state on each reload. I set up a basic demo that shows how it breaks:

https://github.com/jasondonnette/next-js-component-state-bug

Has anyone dealt with a similar situation like this? Is there a better pattern I can use for keeping logic that needs to happen on every load (like authentication) in one spot?

Feature Request: Make mapped props available in getInitialProps

I wish there was a clean way to have bound action creators in getInitialProps. Right now, when passing in mapDispatchToProps to withRedux, these actions have to be created and dispatched imperatively with the store object, e.g.

  static async getInitialProps({ store }) {
   store.dispatch(plusCount());
  }

Preferably, that'd look something like

  static async getInitialProps({ props }) {
    props.plusCount();
  }
const mapDispatchToProps = dispatch =>
  bindActionCreators({ plusCount }, dispatch);

export default withRedux(makeStore, null, mapDispatchToProps)(MyPage);

Unfortunately this pattern requires calling this function again inside getInitialProps, breaking the no-use-before-define rule.

  static async getInitialProps({ store }) {
    const props = mapDispatchToProps(store.dispatch);
    props.plusCount();
  }

Given how weird that seems, and that those actions aren't often called outside of the getInitialProps method, it seems better to write a custom function that's more descriptive, and use an actions object in getInitialProps.

const getInitialActions = store =>
  bindActionCreators({ getMediaItems, plusCount }, store.dispatch);
  static async getInitialProps({ store }) {
    const actions = getInitialActions(store);
    actions.plusCount();
  }

It'd be nice if next-redux-wrapper would pick up mapStateToProps and mapDispatchToProps and expose those props to getInitialProps.

If that seems a little awkward and potentially hazardous, exposing an actions object when mapDispatchToProps is supplied is a less ambitious solution.

How to use Immutable.js as state?

Sorry. Question again.
I'm trying to use Immutable.js for the state.
Here is my makeStore function

import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import reducer from './reducer';
import Immutable from 'immutable';
export default function makeStore(initialState = Immutable.fromJS({})) {
    return createStore(reducer, initialState, applyMiddleware(thunk));
}
and then I use this to 'wrap' the Page component 
Page = withRedux(makeStore, (state) => ({
    foo: state.get('foo'),
    custom: 'custom value',
}), actions)(Page);

got error : state.get is not a function
can you suggest a way to make redux work with Immutable.js
thank you.

Client-side store memoization not working with hot module reloading

When navigating to a different page in our app we see the following warning:
bildschirmfoto 2017-05-22 um 15 20 19

We believe that this is caused by using both withRedux and connect. The store is memoized in the module scope here but then HMR reloads the module, which creates another store instance:

[HMR] Updated modules:
[HMR]  - ./~/next-redux-wrapper/src/index.js
...

We initially thought that this might be related to using a _document.js template with next.js, especially after seeing this comment, but removing _document.js didn't solve our issue.

where is the argument 'options' gone ?

thank you for your amazing work, and i'm puzzled with the argument 'options', how can i use it in the 'makeStore' function.

/**

  • @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);
    };

Usage with apollo

Hi,

Thanks for your work in this repo. It is a very intuitive library. I am trying to incorporate it with apollo but I am having a bit of trouble. The example in the next.js repo seems to do its own memoization of the store creation and explicit creation on the server side so I feel like it is difficult to incorporate with this library. Could you point me in the right direction on how I could do it and help out? Here is the example apollo HOC in the next.js repo.

It seems there is only one extra step - waiting for Apollo to return with data while in getInitialProps.

      // Run all GraphQL queries in the component tree
      // and extract the resulting data
      if (!process.browser) {
        const apollo = initApollo()
        const redux = initRedux(apollo)
        // Provide the `url` prop data in case a GraphQL query uses it
        const url = {query: ctx.query, pathname: ctx.pathname}

        try {
          // Run all GraphQL queries
          await getDataFromTree(
            // No need to use the Redux Provider
            // because Apollo sets up the store for us
            <ApolloProvider client={apollo} store={redux}>
              <ComposedComponent url={url} {...composedInitialProps} />
            </ApolloProvider>
          )
        } catch (error) {
          // Prevent Apollo Client GraphQL errors from crashing SSR.
          // Handle them in components via the data.error prop:
          // http://dev.apollodata.com/react/api-queries.html#graphql-query-data-error
        }
        // getDataFromTree does not call componentWillUnmount
        // head side effect therefore need to be cleared manually
        Head.rewind()

        // Extract query data from the store
        const state = redux.getState()

        // No need to include other initial Redux state because when it
        // initialises on the client-side it'll create it again anyway
        serverState = {
          apollo: { // Only include the Apollo data state
            data: state.apollo.data
          }
        }
      }

Thanks!

Redux store not updating with SSR?

I've got a component where the store updates fine on the client side with the following code:

  componentDidMount() {
    const { loadQuoteIfNeededConnect, id } = this.props
    loadQuoteIfNeededConnect(id)
  }

If I comment that out and attempt to use the code below, the store does not update:

  static async getInitialProps({ store, query: { id } }) {
    // this fetches the data, but store is not updated
    await store.dispatch(loadQuoteIfNeeded(id))
    return { id }
  }

I've got a console.log directly above the return statement in my reducer, and I can see in my terminal that the data is in fact being fetched and returned properly with the code from getInitialProps(), but the store is not updating.

Any ideas?

How to dispatch an action from an event function?

The following line in getInitialProps() dispatched an action to the reducer:

store.dispatch(action);

However, what if I want to do that in some other function, say, onChange()? The following would obviously return an error saying store is undefined:

onChange({ store, e }) {
    store.dispatch({ type: 'USERNAME', payload: e.target.value });
 }

So how do I access the store from outside of getInitialProps()?

I am trying to call the dispatch method from within my onSubmit() like so:

onSubmit(e) {
    e.preventDefault();
    axios.post('/api/authentication/login', { username: this.state.username, password: this.state.password })
      .then(function (response) {
        console.log(response);
        console.log('First: ', response.data.firstName);
				// dispatch user data to the store
				this.props.testclick(response.data.email);
      })
      .catch(function (error) {
        console.log(error);
      });
  }

And my mapDispatchToProps() is defined:

const mapDispatchToProps = (dispatch) => {
	return {
		testclick: (currUser) => dispatch({ type: 'FOO', payload: currUser })
	}
}

However, when I run it, the browser throws an error on this line saying this is undefined:

this.props.testclick(response.data.email);

Any workaround?

How to use redux middleware

I would like to use redux devtools as a redux middleWare. But it doesn't work. Does this library allow using redux middleware ?

This is my makeStore function

const reduxDevtools =
  typeof window !== 'undefined' &&
  window.__REDUX_DEVTOOLS_EXTENSION__ &&
  window.__REDUX_DEVTOOLS_EXTENSION__()

const makeStore = (initialState) => {
    return createStore(reducer, initialState, reduxDevtools);
};

Client kept initialState

Hello,

In the example, I found something strange (correct me if I'm wrong) about the state in client after calling getInitialProps in _document from the server.

We dispatch an async action on server to edit tack

// _document.js
static async getInitialProps(ctx) {

        const props = await Document.getInitialProps(ctx);
        const {store, isServer, pathname, query} = ctx;

        console.log(MyDocument.name, '- 2. Cmp.getInitialProps uses the store to dispatch things, pathname', pathname, 'query', query);

        if (isServer) {

            return new Promise((res) => {
                setTimeout(() => {
                    store.dispatch({type: 'TACK', payload: 'server'}); // <--- here
                    res(props);
                }, 200);
            });

        }

        store.dispatch({type: 'TACK', payload: 'client'});

        return props;

    }

So in the tack should be equal to server but it's equal to init.

// index.js
...
render () {
  return  (
    <div>Redux tack: {this.props.tack} (_document)</div>
  )
}

Thanks for your help.

Tight coupling between page component and action creator(s)

When we create a page component, that requests some data from REST API, we usually end up with code like this:

// In component dependencies definition:
import { getSomethingAsynchronously } from './actions/something';
// In component definition:
static getInitialProps({ store }) {
  return store.dispatch(getSomethingAsynchronously());
}

It works fine, but since getInitialProps doesn't have access to component props defined in mapDispatchToProps on previous higher level, we have to create unnecessary connection between the component and action creators it uses. I'd like to know if it's possible to solve this little problem without creating one more component layer like that:

import React from 'react';
import { getSomethingAsynchronously } from './actions/something';
import ComponentWeWantToDisplay from './components/ComponentWeWantToDisplay';

export default class Wrapper extends React.Component {
  static getInitialProps({ store }) {
    return store.dispatch(getSomethingAsynchronously());
  }
  render() {
    return <ComponentWeWantToDisplay {...this.props} />;
  }
}

Package quality issues

Hope you to fix this soon.

Code

https://github.com/kirill-konshin/next-redux-wrapper/blob/master/src/index.js#L81

Cmp.getInitialProps(ctx)

https://github.com/kirill-konshin/next-redux-wrapper/blob/master/src/index.js#L68-L82

     ctx = ctx || {}

      console.log(Cmp.name, '- 1. WrappedCmp.getInitialProps wrapper', (ctx.req && ctx.req._store ? 'takes the req store' : 'creates the store'))

      ctx.isServer = !!ctx.req
      ctx.store = initStore(createStore, ctx.req)

      return Promise
        .all([
          ctx.isServer,
          ctx.store,
          ctx.req,
          Cmp.getInitialProps ? Cmp.getInitialProps(ctx) : {}
        ])

Modules

Use files attribute of package.json to include only the source code.

screen shot 2017-04-18 at 1 03 01

'window is not defined'

Constantly snagging window is not defined with latest release.

ReferenceError: window is not defined
at initStore (/**/node_modules/next-redux-wrapper/src/index.js:33:10)

WithRedux has to be used only for top level pages, all other components has to be wrapped in connect - Breaking change introduced in minor version

Minor version update 1.3.1 breaks node.js rendering of WithRedux.
This is critical issue, avoid latest release.

Error: WithRedux has to be used only for top level pages, all other components has to be wrapped in connect at initStore (/var/app/current/node_modules/next-redux-wrapper/src/index.js:33:15) at WrappedCmp (/var/app/current/node_modules/next-redux-wrapper/src/index.js:95:19) at ReactCompositeComponentWrapper._constructComponentWithoutOwner (/var/app/current/node_modules/react-dom/lib/ReactCompositeComponent.js:308:14) at ReactCompositeComponentWrapper._constructComponent (/var/app/current/node_modules/react-dom/lib/ReactCompositeComponent.js:284:19) at ReactCompositeComponentWrapper.mountComponent (/var/app/current/node_modules/react-dom/lib/ReactCompositeComponent.js:187:21) at Object.mountComponent (/var/app/current/node_modules/react-dom/lib/ReactReconciler.js:45:35) at ReactCompositeComponentWrapper.performInitialMount (/var/app/current/node_modules/react-dom/lib/ReactCompositeComponent.js:370:34) at ReactCompositeComponentWrapper.mountComponent (/var/app/current/node_modules/react-dom/lib/ReactCompositeComponent.js:257:21) at Object.mountComponent (/var/app/current/node_modules/react-dom/lib/ReactReconciler.js:45:35) at ReactCompositeComponentWrapper.performInitialMount (/var/app/current/node_modules/react-dom/lib/ReactCompositeComponent.js:370:34) at ReactCompositeComponentWrapper.mountComponent (/var/app/current/node_modules/react-dom/lib/ReactCompositeComponent.js:257:21) at Object.mountComponent (/var/app/current/node_modules/react-dom/lib/ReactReconciler.js:45:35) at ReactCompositeComponentWrapper.performInitialMount (/var/app/current/node_modules/react-dom/lib/ReactCompositeComponent.js:370:34) at ReactCompositeComponentWrapper.mountComponent (/var/app/current/node_modules/react-dom/lib/ReactCompositeComponent.js:257:21) at Object.mountComponent (/var/app/current/node_modules/react-dom/lib/ReactReconciler.js:45:35) at ReactCompositeComponentWrapper.performInitialMount (/var/app/current/node_modules/react-dom/lib/ReactCompositeComponent.js:370:34)

Pass isServer to makeStore function

@kirill-konshin Nice library!

It would be useful to have the isServer bool passed into the store create method. There are often times I load certain middleware on the client and another certain set of middleware on the server.

Right now, you can get away with looking to see if there is a window object, but seems it should be easy enough to pass the isServer bool instead to have a clean way of deducing the environment.

Let me know if you want me to whip the PR, shouldn't take but a second.

Article about how to use next-redux-wrapper with Redux Saga

Hey @kirill-konshin

Recently I used NextJs and wanted to introduce Redux with Redux Saga. Your library helped me a lot and I just wanted to write about it, because I found it pretty hard to find any advice or solutions around that topic.

Maybe you want to review the article or even more consider linking to it. Thanks again for your library 👍 You can close this issue any time, because it's no real issue 😄

redux state will become to the initial State after page load finished.

thanks for your awesome library.

I have a problem, when refresh the browser, getInitialProps fetch data and dispatch an action ,then the server will return a correct page, but when the client loads all js file, redux state has become the initial value.

demo repository https://github.com/joyran/next.js-with-redux-demo

// pages/index.js
// init state
const initialState = {
  name: null,
  bio: null
};

const initStore = () => {
  return createStore(user, initialState, composeWithDevTools(applyMiddleware(thunkMiddleware)));
};

const Index = () => {
  return (
    <User />
  );
};

Index.getInitialProps = async ({ store, isServer }) => {
  const res = await fetch('https://api.github.com/users/tj');
  const data = await res.json();

  store.dispatch(readUserSuccessByServer(data));
};

export default withRedux(initStore, null)(Index);
// reducers
import fetch from 'isomorphic-fetch';
import { createActions, handleActions } from 'redux-actions';

// ------------------------
// ACTIONS
// ------------------------
export const {
  readUserSuccess,
  readUserSuccessByServer
} = createActions(
  'READ_USER_SUCCESS',
  'READ_USER_SUCCESS_BY_SERVER'
);

export const readUser = () => dispatch => {
  fetch('https://api.github.com/users/a')
    .then(res => res.json())
    .then(res => dispatch(readUserSuccess(res)))
    .catch((err) => {
      console.error(err.message);
    });
};

// ------------------------
// REDUCERS
// ------------------------
export const user = handleActions({
  READ_USER_SUCCESS: (state, action) => ({
    ...state,
    name: action.payload.name,
    bio: action.payload.bio
  }),

  READ_USER_SUCCESS_BY_SERVER: (state, action) => ({
    ...state,
    name: action.payload.name,
    bio: action.payload.bio
  })
}, {});

console log

warning.js?9dca99b:35 Warning: React attempted to reuse markup in a container but the checksum was invalid. This generally means that you are using server rendering and the markup generated on the server was not what the client was expecting. React injected new markup to compensate which works but you have lost many of the benefits of server rendering. Instead, figure out why the markup being generated is different on the client or server:
 (client) h1 data-reactid="3"></h1><h3 data-reacti
 (server) h1 data-reactid="3">TJ Holowaychuk</h1><

Question: Nested components

Hello, In the README it says;

Wrapper has to be attached your page components (located in /pages). For safety it is recommended to wrap all pages, no matter if they use Redux or not, so that you should not care about it anymore in all child components.

Although this is fine, my pages within nextjs become rather large, with many nested components. If my page is the one receiving props, I see two issues here:

  1. Trying to access the data in multi-nested components becomes messy. They have to be passed down as props over and over.

  2. If one item in the store changes which a page relies on, the entire page is re-rendered even though 95% of the other components don't care about it.

In a client side SPA, I'd subscribe only the components which rely on specific data, so only they'd re-render. Am I missing something here?

Persisting store state on page reload

I've seen a couple of seemingly related questions but reading through them I didn't get an answer.
I want my state (or more specifically parts of it, related to user auth) to be persisted across page reloads.

With client-only app I would normally use https://www.npmjs.com/package/redux-storage, and follow these steps:

  1. create the store with the empty initial sate
  2. use the redux-storage load method, wait for store to be populated
  3. render an app

Then whenever the auth changes the token (or null) is placed in the store and persisted in localStorage.

Now with Next.js there are two issues:

  1. that would throw away the state sent on the initial SSR
  2. I do not control the app initialization so I cannot defer it before the saved state is loaded from the localStorage

I can work around these problems if using cookies but it's kinda ugly.
Can you think of any better solution?


If it helps here's the user story for a contrived app:

  1. the user can login and log out from the app
  2. The login is always an XHR from the client-side to the API server. The auth API returns a JWT token which is saved in the store
  3. the app consists of a single page that contains the login form (for the logged out user) or the user name decoded from the token
  4. I need the auth to be persisted on page refresh
  5. Ideally on page refresh I'd like the server side to be given the same persisted storage so that the page is properly pre-rendered based on the user auth state

Example using _document

Would be great to get an example explaining how to use this package with the _document component from next.

:-)

Propagate request in makeStore call

Hi! I think it would be nice to have the opportunity to give the request to the store creation function.
We would then be able to distinguish between client and server initialization when creating the store and we could also use some data from server (through the request) to populate the initial state.
I also use to give an api client (superagent) as an extra param to the redux thunk middleware to have it in all my thunk action to perform my request to my api server. When, the request is made from the server, I'd like to forward the cookie from the initial request. This change would allow me to do this.

It would be as easy as changing line 17 of src/index.js

        req._store = makeStore(initialState);

into

        req._store = makeStore(initialState, req);

What do you think ? If you agree, I can submit a PR :)

Unable to work with Redux

i tried to implement next.js with redux.but it is not working. why is that? this is what i tried so far...

import React, { Component } from 'react';
import withRedux from 'next-redux-wrapper'
import Header from '../components/layout/header';
import Navbar from '../components/layout/navbar';

import { createStore, bindActionCreators } from 'redux';

const exampleInitialState = {
  count: 0
}

const addCounter = (value) => dispatch => {
  return dispatch({
      type: 'ADD',
      payload: value
    })
}

const reducer = (state = exampleInitialState, action) => {
  switch (action.type) {
    case "ADD":
      const newCount = state.count + action.payload;
      return Object.assign({}, state, { count: newCount });
    default:
      return state;
  }
}

const store = () => createStore(reducer, exampleInitialState);


class Home extends Component {

  addCount() {
    this.props.addCount();
    console.log(store().getState());
  }

  render() {
    return (
      <div>
        <Header />
        <Navbar />

        <div className="columns top-container">
          <div className="column">
            <h1>Count: {this.props.count}</h1>
            <button className="button is-primary" onClick={this.addCount.bind(this)}>Click Me</button>
          </div>
        </div>

        <style jsx>{`
          .top-container {
            background: radial-gradient(ellipse at center, rgba(87,73,214,1) 0%, rgba(70,59,173,1) 100%);
            color: #FFF;
            padding: 15px;
            height: 500px;
            max-height: 500px;
          }
      `}</style>
      <style global jsx>{`
        body, button, input, select, textarea {
          font-family: 'Fira Sans', sans-serif;
        }
    `}</style>
      </div>
    )
  }
}

const mapStateToProps = (state) => {
  return {
    count: state.count
  }
}

const mapDispatchToProps = (dispatch) => {
  return {
    addCount : () => bindActionCreators(addCounter(1), dispatch)
  }
}

export default withRedux(store, mapStateToProps, mapDispatchToProps)(Home);

withRedux on child components works, but they render server-side before the page's getInitialProps is done dispatching actions

So we have the following hierarchy:

<Page>
  <Layout>
    <UserMenu />
  </Layout>
</Page> 

Both Page and UserMenu are wrapped with withRedux. In Page.getInitialProps we do a context.store.dispatch({type: 'SET_USER', payload: {...userData}}). After this, the state should contain the user data and not be empty anymore. But it seems that UserMenu is already rendered before this, supposedly synchronous, dispatch call has finished.

When inspecting the server-side rendered markup, it appears that UserMenu saw an empty state.

We're trying to understand this issue a bit better. Is there any way to make child components work with withRedux so that the order/hierarchy of components is respected and rendering is delayed until the store was populated on the top level?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.