Skip to content

Commit

Permalink
6.0.0-rc.3
Browse files Browse the repository at this point in the history
- Redux Saga example
- Proper support of `getStaticProps` and `getServerSideProps`
- Removed `isServer`
- Better tests
  • Loading branch information
kirill-konshin committed Apr 8, 2020
1 parent 0d65503 commit 8087166
Show file tree
Hide file tree
Showing 36 changed files with 754 additions and 208 deletions.
70 changes: 59 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -434,27 +434,72 @@ createWrapper({

### 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.
[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.

```js
// Before this, import what you need and create a root saga as usual
Create your root saga as usual, then implement the store creator:

```typescript
import {createStore, applyMiddleware, Store} from 'redux';
import {MakeStore, 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;
}

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

// Before we returned the created store without assigning it to a variable:
// return createStore(reducer);

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

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

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

export const wrapper = createWrapper<State>(makeStore, {debug: true});
```

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

```typescript
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<Promise<AppInitialProps>>(async ({Component, ctx}) => {
// 1. Wait for all page actions to dispatch
const pageProps = {
...(Component.getInitialProps ? await Component.getInitialProps(ctx) : {}),
};

// 2. Stop the saga if on server
if (ctx.req) {
ctx.store.dispatch(END);
await (ctx.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);
```

### Usage with Redux Persist
Expand Down Expand Up @@ -581,6 +626,9 @@ Major change in the way how things are wrapped in version 6.

7. `App` should no longer wrap it's childern with `Provider`

8. `isServer` is no longer passed in context/props, use your own function or simple check `const isServer = typeof window === 'undefined'`
8. `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.
Expand Down
7 changes: 7 additions & 0 deletions packages/configs/jest-puppeteer.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
module.exports = {
server: {
command: 'yarn serve',
debug: true,
launchTimeout: 30000,
},
};
1 change: 0 additions & 1 deletion packages/configs/jest.config.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
module.exports = {
collectCoverage: true,
coveragePathIgnorePatterns: ['./node_modules', './.next'],
testPathIgnorePatterns: ['./node_modules'],
transform: {
Expand Down
6 changes: 6 additions & 0 deletions packages/configs/jest.config.puppeteer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
const config = require('./jest.config');
module.exports = {
...config,
preset: 'jest-puppeteer',
coveragePathIgnorePatterns: config.coveragePathIgnorePatterns.concat('./jest-puppeteer.config.js'),
};
2 changes: 0 additions & 2 deletions packages/demo-page/.npmrc

This file was deleted.

10 changes: 2 additions & 8 deletions packages/demo-page/jest-puppeteer.config.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,2 @@
module.exports = {
server: {
command: 'yarn serve',
debug: true,
port: 4000,
launchTimeout: 30000,
},
};
module.exports = require('next-redux-wrapper-configs/jest-puppeteer.config');
module.exports.server.port = 4000;
7 changes: 1 addition & 6 deletions packages/demo-page/jest.config.js
Original file line number Diff line number Diff line change
@@ -1,6 +1 @@
const config = require('next-redux-wrapper-configs/jest.config');
module.exports = {
...config,
preset: 'jest-puppeteer',
coveragePathIgnorePatterns: config.coveragePathIgnorePatterns.concat('./jest-puppeteer.config.js'),
};
module.exports = require('next-redux-wrapper-configs/jest.config.puppeteer');
2 changes: 2 additions & 0 deletions packages/demo-saga/jest-puppeteer.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
module.exports = require('next-redux-wrapper-configs/jest-puppeteer.config');
module.exports.server.port = 5000;
1 change: 1 addition & 0 deletions packages/demo-saga/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = require('next-redux-wrapper-configs/jest.config.puppeteer');
2 changes: 2 additions & 0 deletions packages/demo-saga/next-env.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/// <reference types="next" />
/// <reference types="next/types/global" />
51 changes: 51 additions & 0 deletions packages/demo-saga/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
{
"name": "next-redux-wrapper-demo-saga",
"private": true,
"version": "6.0.0",
"description": "Demo of redux wrapper for Next.js",
"scripts": {
"clean": "rimraf .next coverage",
"test": "jest",
"start": "next --port=5000",
"build": "next build",
"serve": "next start --port=5000"
},
"dependencies": {
"jsondiffpatch": "0.4.1",
"next-redux-wrapper": "^6.0.0",
"react": "16.13.1",
"react-dom": "16.13.1",
"react-redux": "7.2.0",
"redux": "4.0.5",
"redux-saga": "1.1.3"
},
"devDependencies": {
"@types/expect-puppeteer": "4.4.0",
"@types/jest": "25.1.4",
"@types/jest-environment-puppeteer": "4.3.1",
"@types/next-redux-saga": "3.0.2",
"@types/puppeteer": "2.0.1",
"@types/react": "16.9.31",
"@types/react-dom": "16.9.6",
"@types/react-redux": "7.1.7",
"@types/webpack-env": "1.15.1",
"jest": "25.2.4",
"jest-puppeteer": "4.4.0",
"next": "9.3.4",
"next-redux-wrapper-configs": "^6.0.0",
"puppeteer": "2.1.1",
"rimraf": "3.0.2",
"ts-jest": "25.3.0",
"typescript": "3.8.3"
},
"author": "Kirill Konshin",
"repository": {
"type": "git",
"url": "git://github.com/kirill-konshin/next-redux-wrapper.git"
},
"bugs": {
"url": "https://github.com/kirill-konshin/next-redux-wrapper/issues"
},
"homepage": "https://github.com/kirill-konshin/next-redux-wrapper",
"license": "MIT"
}
22 changes: 22 additions & 0 deletions packages/demo-saga/src/components/reducer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import {AnyAction} from 'redux';
import {HYDRATE} from 'next-redux-wrapper';
import {SAGA_ACTION_SUCCESS} from './saga';

export interface State {
page: string;
}

const initialState: State = {page: ''};

function rootReducer(state = initialState, action: AnyAction) {
switch (action.type) {
case HYDRATE:
return {...state, ...action.payload};
case SAGA_ACTION_SUCCESS:
return {...state, page: action.data};
default:
return state;
}
}

export default rootReducer;
18 changes: 18 additions & 0 deletions packages/demo-saga/src/components/saga.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import {delay, put, takeEvery} from 'redux-saga/effects';

export const SAGA_ACTION = 'SAGA_ACTION';
export const SAGA_ACTION_SUCCESS = `${SAGA_ACTION}_SUCCESS`;

function* sagaAction() {
yield delay(100);
yield put({
type: SAGA_ACTION_SUCCESS,
data: 'async text',
});
}

function* rootSaga() {
yield takeEvery(SAGA_ACTION, sagaAction);
}

export default rootSaga;
25 changes: 25 additions & 0 deletions packages/demo-saga/src/components/store.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import {createStore, applyMiddleware, Store} from 'redux';
import {MakeStore, 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: MakeStore<State> = (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<State>(makeStore, {debug: true});
32 changes: 32 additions & 0 deletions packages/demo-saga/src/pages/_app.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
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<Promise<AppInitialProps>>(async ({Component, ctx}) => {
// 1. Wait for all page actions to dispatch
const pageProps = {
...(Component.getInitialProps ? await Component.getInitialProps(ctx) : {}),
};

// 2. Stop the saga if on server
if (ctx.req) {
console.log('Saga is executing on server, we will wait');
ctx.store.dispatch(END);
await (ctx.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);
26 changes: 26 additions & 0 deletions packages/demo-saga/src/pages/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import React from 'react';
import {useSelector} from 'react-redux';
import {NextPage} from 'next';
import {State} from '../components/reducer';
import {SAGA_ACTION} from '../components/saga';

export interface ConnectedPageProps {
custom: string;
}

// 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<ConnectedPageProps> = ({custom}: ConnectedPageProps) => {
const {page} = useSelector<State, State>(state => state);
return (
<div className="index">
<pre>{JSON.stringify({page, custom}, null, 2)}</pre>
</div>
);
};

Page.getInitialProps = ({store}) => {
store.dispatch({type: SAGA_ACTION});
return {custom: 'custom'};
};

export default Page;
14 changes: 14 additions & 0 deletions packages/demo-saga/tests/index.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import config from '../jest-puppeteer.config';

const openPage = (url = '/') => page.goto(`http://localhost:${config.server.port}${url}`);

describe('Basic integration', () => {
it('shows the page', async () => {
await openPage();

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

await expect(page).toMatch('"page": "async text"');
await expect(page).toMatch('"custom": "custom"');
});
});
20 changes: 20 additions & 0 deletions packages/demo-saga/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"extends": "../configs/tsconfig.json",
"include": [
"tests",
"src/pages"
],
"compilerOptions": {
"allowJs": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"module": "esnext",
"outDir": "lib",
"isolatedModules": true,
"jsx": "preserve"
},
"exclude": [
"node_modules"
]
}
2 changes: 0 additions & 2 deletions packages/demo/.npmrc

This file was deleted.

10 changes: 2 additions & 8 deletions packages/demo/jest-puppeteer.config.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,2 @@
module.exports = {
server: {
command: 'yarn serve',
debug: true,
port: 3000,
launchTimeout: 30000,
},
};
module.exports = require('next-redux-wrapper-configs/jest-puppeteer.config');
module.exports.server.port = 3000;
7 changes: 1 addition & 6 deletions packages/demo/jest.config.js
Original file line number Diff line number Diff line change
@@ -1,6 +1 @@
const config = require('next-redux-wrapper-configs/jest.config');
module.exports = {
...config,
preset: 'jest-puppeteer',
coveragePathIgnorePatterns: config.coveragePathIgnorePatterns.concat('./jest-puppeteer.config.js'),
};
module.exports = require('next-redux-wrapper-configs/jest.config.puppeteer');
2 changes: 1 addition & 1 deletion packages/demo/src/pages/_app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ class WrappedApp extends App<AppInitialProps> {
// Call page-level getInitialProps
...(Component.getInitialProps ? await Component.getInitialProps(ctx) : {}),
// Some custom thing for all pages
pathname: ctx.pathname,
appProp: ctx.pathname,
},
};
});
Expand Down
Loading

0 comments on commit 8087166

Please sign in to comment.