Skip to content

Commit

Permalink
6.0.0-rc.5
Browse files Browse the repository at this point in the history
- Better support for `App.getInitialProps` with `Page.getServerSideProps` and `Page.getStaticProps`
  • Loading branch information
kirill-konshin committed Apr 9, 2020
1 parent f7db5d6 commit e67a35e
Show file tree
Hide file tree
Showing 7 changed files with 123 additions and 31 deletions.
45 changes: 39 additions & 6 deletions README.md
Expand Up @@ -4,25 +4,24 @@
![Build status](https://travis-ci.org/kirill-konshin/next-redux-wrapper.svg?branch=master)
[![Coverage Status](https://coveralls.io/repos/github/kirill-konshin/next-redux-wrapper/badge.svg?branch=master)](https://coveralls.io/github/kirill-konshin/next-redux-wrapper?branch=master)


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. Otherwise, consider upgrading Next.js. :warning:

Next.js 5 (for individual pages) is only compatible with the [1.x branch](https://github.com/kirill-konshin/next-redux-wrapper/tree/1.x). You can upgrade it following these simple [instructions](#upgrade-from-1x).

Contents:

- [Motivation](#motivation)
- [Installation](#installation)
- [Usage](#usage)
- [Configuration](#configuration)
- [getStaticProps](#getstaticprops)
- [getServerSideProps](#getserversideprops)
- [getInitialProps](#getinitialprops)
- [App](#app)
- [App and getServerSideProps or getStaticProps at page level](#app-and-getserversideprops-or-getstaticprops-at-page-level)
- [How it works](#how-it-works)
- [Configuration](#configuration)
- [Tips and Tricks](#tips-and-tricks)
- [Server and Client state separation](#server-and-client-state-separation)
- [Document](#document)
- [Error Pages](#error-pages)
- [Async actions](#async-actions)
Expand All @@ -39,10 +38,14 @@ Setting up Redux for static apps is rather simple: a single Redux store has to b

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`.
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`.

# Installation

```bash
Expand Down Expand Up @@ -284,7 +287,37 @@ Page.getInitialProps = ({store, isServer, pathname, query}: NextPageContext) =>
export default connect((state: State) => state)(Page);
```

## How it works
## 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
- :warning: 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`:

```typescript
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;
}
};
```

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

More about that in [Server and Client state separation](#server-and-client-state-separation).

# How it works

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

Expand Down
2 changes: 2 additions & 0 deletions packages/demo/src/components/reducer.tsx
Expand Up @@ -9,6 +9,8 @@ export interface State {
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};
Expand Down
6 changes: 5 additions & 1 deletion packages/demo/src/pages/index.tsx
Expand Up @@ -41,10 +41,14 @@ class Index extends React.Component<PageProps> {

<pre>{JSON.stringify({pageProp, appProp, app, page}, null, 2)}</pre>

<Link href="/other">
<Link href="/server">
<a>Navigate</a>
</Link>
{' | '}
<Link href="/static">
<a>Navigate to static</a>
</Link>
{' | '}
<Link href="/error">
<a>Navigate to error</a>
</Link>
Expand Down
Expand Up @@ -10,10 +10,10 @@ interface OtherProps {
appProp: string;
}

const Other: NextPage<OtherProps> = ({appProp, getServerSideProp}) => {
const Server: NextPage<OtherProps> = ({appProp, getServerSideProp}) => {
const {app, page} = useSelector<State, State>(state => state);
return (
<div className="other">
<div className="server">
<p>Page has access to store even though it does not dispatch anything itself</p>

<pre>{JSON.stringify({app, page, getServerSideProp, appProp}, null, 2)}</pre>
Expand All @@ -27,10 +27,9 @@ const Other: NextPage<OtherProps> = ({appProp, getServerSideProp}) => {
);
};

export const getServerSideProps = wrapper.getServerSideProps(({store}) => {
store.dispatch({type: 'PAGE', payload: 'other'});
console.log(store.getState(), 'getServerSideProps');
export const getStaticProps = wrapper.getStaticProps(({store}) => {
store.dispatch({type: 'PAGE', payload: 'server'});
return {props: {getServerSideProp: 'bar'}};
});

export default Other;
export default Server;
35 changes: 35 additions & 0 deletions packages/demo/src/pages/static.tsx
@@ -0,0 +1,35 @@
import React from 'react';
import Link from 'next/link';
import {useSelector} from 'react-redux';
import {NextPage} from 'next';
import {State} from '../components/reducer';
import {wrapper} from '../components/store';

interface OtherProps {
getStaticProp: string;
appProp: string;
}

const Other: NextPage<OtherProps> = ({appProp, getStaticProp}) => {
const {app, page} = useSelector<State, State>(state => state);
return (
<div className="static">
<p>Page has access to store even though it does not dispatch anything itself</p>

<pre>{JSON.stringify({app, page, getStaticProp, appProp}, null, 2)}</pre>

<nav>
<Link href="/">
<a>Navigate to index</a>
</Link>
</nav>
</div>
);
};

export const getStaticProps = wrapper.getStaticProps(({store}) => {
store.dispatch({type: 'PAGE', payload: 'static'});
return {props: {getStaticProp: 'bar'}};
});

export default Other;
36 changes: 24 additions & 12 deletions packages/demo/tests/index.spec.ts
Expand Up @@ -8,16 +8,16 @@ describe('Using App wrapper', () => {

await page.waitForSelector('div.index');

await expect(page).toMatch('"pageProp": "server"');
await expect(page).toMatch('"appProp": "/"');
await expect(page).toMatch('"app": "was set in _app"');
await expect(page).toMatch('"page": "server"');
await expect(page).toMatch('"pageProp": "server"'); // props
await expect(page).toMatch('"appProp": "/"'); // props
await expect(page).toMatch('"app": "was set in _app"'); // redux
await expect(page).toMatch('"page": "server"'); // redux
});

it('shows client values when page is visited after navigation', async () => {
await openPage('/other');
await openPage('/server');

await page.waitForSelector('div.other');
await page.waitForSelector('div.server');

await expect(page).toClick('a', {text: 'Navigate to index'});

Expand All @@ -27,13 +27,25 @@ describe('Using App wrapper', () => {
await expect(page).toMatch('"page": "client"');
});

it('properly combines props from _app and page', async () => {
await openPage('/other');
it("properly combines state from App.getInitialProps and page's getServerSideProps", async () => {
await openPage('/server');

await page.waitForSelector('div.server');

await expect(page).toMatch('"getServerSideProp": "bar"'); // props
await expect(page).toMatch('"appProp": "/server"'); // props
await expect(page).toMatch('"page": "server"'); // redux
await expect(page).toMatch('"app": "was set in _app"'); // redux
});

it("properly combines state from App.getInitialProps and page's getStaticProps", async () => {
await openPage('/static');

await page.waitForSelector('div.other');
await page.waitForSelector('div.static');

await expect(page).toMatch('"getServerSideProp": "bar"');
await expect(page).toMatch('"page": "other"');
await expect(page).toMatch('"appProp": "/other"');
await expect(page).toMatch('"getStaticProp": "bar"'); // props
await expect(page).toMatch('"appProp": "/static"'); // props
await expect(page).toMatch('"page": "static"'); // redux
await expect(page).toMatch('"app": "was set in _app"'); // redux
});
});
19 changes: 13 additions & 6 deletions packages/wrapper/src/index.tsx
Expand Up @@ -166,13 +166,20 @@ export const createWrapper = <S extends {} = any, A extends Action = AnyAction>(

this.store.dispatch({
type: HYDRATE,
payload: getDeserializedState(
// this happens when App has page with getServerSideProps
// ATTENTION! This code assumes that Page's getServerSideProps is executed after App.getInitialProps
props?.pageProps?.initialState ?? initialState,
config,
),
payload: getDeserializedState(initialState, config),
} as any);

if (props?.pageProps?.initialState) {
this.store.dispatch({
type: HYDRATE,
payload: getDeserializedState(
// this happens when App has page with getServerSideProps/getStaticProps
// ATTENTION! This code assumes that Page's getServerSideProps is executed after App.getInitialProps
props.pageProps.initialState,
config,
),
} as any);
}
}

public render() {
Expand Down

0 comments on commit e67a35e

Please sign in to comment.