From 3470c42b3219f038a02e5e241229face05c397c5 Mon Sep 17 00:00:00 2001 From: Kirill Konshin Date: Thu, 4 Jun 2020 13:50:43 -0700 Subject: [PATCH] Fix async in getServerSideProps Fix #235 Fix #223 --- packages/demo-page/src/pages/index.tsx | 3 +- packages/demo-page/src/pages/other.tsx | 2 +- packages/demo-page/src/pages/other2.tsx | 2 +- .../demo-saga-page/jest-puppeteer.config.js | 2 + packages/demo-saga-page/jest.config.js | 1 + packages/demo-saga-page/next-env.d.ts | 2 + packages/demo-saga-page/package.json | 53 +++++++++++++++++++ .../demo-saga-page/src/components/reducer.tsx | 22 ++++++++ .../demo-saga-page/src/components/saga.tsx | 18 +++++++ .../demo-saga-page/src/components/store.tsx | 26 +++++++++ packages/demo-saga-page/src/pages/_app.tsx | 7 +++ packages/demo-saga-page/src/pages/index.tsx | 31 +++++++++++ packages/demo-saga-page/tests/index.spec.ts | 14 +++++ packages/demo-saga-page/tsconfig.json | 20 +++++++ packages/wrapper/src/index.tsx | 4 +- 15 files changed, 203 insertions(+), 4 deletions(-) create mode 100644 packages/demo-saga-page/jest-puppeteer.config.js create mode 100644 packages/demo-saga-page/jest.config.js create mode 100644 packages/demo-saga-page/next-env.d.ts create mode 100644 packages/demo-saga-page/package.json create mode 100644 packages/demo-saga-page/src/components/reducer.tsx create mode 100644 packages/demo-saga-page/src/components/saga.tsx create mode 100644 packages/demo-saga-page/src/components/store.tsx create mode 100644 packages/demo-saga-page/src/pages/_app.tsx create mode 100644 packages/demo-saga-page/src/pages/index.tsx create mode 100644 packages/demo-saga-page/tests/index.spec.ts create mode 100644 packages/demo-saga-page/tsconfig.json diff --git a/packages/demo-page/src/pages/index.tsx b/packages/demo-page/src/pages/index.tsx index 8fe4ccb..04bc2b4 100644 --- a/packages/demo-page/src/pages/index.tsx +++ b/packages/demo-page/src/pages/index.tsx @@ -26,9 +26,10 @@ const Page: NextPage = ({custom}) => { ); }; -export const getServerSideProps = wrapper.getServerSideProps(({store, req}) => { +export const getServerSideProps = wrapper.getServerSideProps(async ({store, req}) => { console.log('2. Page.getServerSideProps uses the store to dispatch things'); store.dispatch({type: 'PAGE', payload: 'was set in index page ' + req.url}); + await new Promise(res => setTimeout(res, 1000)); return {props: {custom: 'custom'}}; }); diff --git a/packages/demo-page/src/pages/other.tsx b/packages/demo-page/src/pages/other.tsx index d68bc79..9b6abd2 100644 --- a/packages/demo-page/src/pages/other.tsx +++ b/packages/demo-page/src/pages/other.tsx @@ -5,7 +5,7 @@ import {useSelector, useDispatch} from 'react-redux'; import {State} from '../components/reducer'; import {wrapper} from '../components/store'; -export const getStaticProps = wrapper.getStaticProps(({store, previewData}) => { +export const getStaticProps = wrapper.getStaticProps(async ({store, previewData}) => { console.log('2. Page.getStaticProps uses the store to dispatch things'); store.dispatch({type: 'PAGE', payload: 'was set in other page ' + JSON.stringify({previewData})}); }); diff --git a/packages/demo-page/src/pages/other2.tsx b/packages/demo-page/src/pages/other2.tsx index 4e50795..f7c2a0e 100644 --- a/packages/demo-page/src/pages/other2.tsx +++ b/packages/demo-page/src/pages/other2.tsx @@ -5,7 +5,7 @@ import {useSelector, useDispatch} from 'react-redux'; import {State} from '../components/reducer'; import {wrapper} from '../components/store'; -export const getStaticProps = wrapper.getStaticProps(({store, previewData}) => { +export const getStaticProps = wrapper.getStaticProps(async ({store, previewData}) => { console.log('2. Page.getStaticProps uses the store to dispatch things'); store.dispatch({type: 'PAGE', payload: 'was set in other (SECOND) page ' + JSON.stringify({previewData})}); }); diff --git a/packages/demo-saga-page/jest-puppeteer.config.js b/packages/demo-saga-page/jest-puppeteer.config.js new file mode 100644 index 0000000..b116fe8 --- /dev/null +++ b/packages/demo-saga-page/jest-puppeteer.config.js @@ -0,0 +1,2 @@ +module.exports = require('next-redux-wrapper-configs/jest-puppeteer.config'); +module.exports.server.port = 5050; diff --git a/packages/demo-saga-page/jest.config.js b/packages/demo-saga-page/jest.config.js new file mode 100644 index 0000000..e311871 --- /dev/null +++ b/packages/demo-saga-page/jest.config.js @@ -0,0 +1 @@ +module.exports = require('next-redux-wrapper-configs/jest.config.puppeteer'); diff --git a/packages/demo-saga-page/next-env.d.ts b/packages/demo-saga-page/next-env.d.ts new file mode 100644 index 0000000..7b7aa2c --- /dev/null +++ b/packages/demo-saga-page/next-env.d.ts @@ -0,0 +1,2 @@ +/// +/// diff --git a/packages/demo-saga-page/package.json b/packages/demo-saga-page/package.json new file mode 100644 index 0000000..8b6d7a6 --- /dev/null +++ b/packages/demo-saga-page/package.json @@ -0,0 +1,53 @@ +{ + "name": "next-redux-wrapper-demo-saga-page", + "private": true, + "version": "6.0.0", + "description": "Demo of redux wrapper for Next.js", + "scripts": { + "clean": "rimraf .next coverage", + "test": "jest", + "start": "next --port=5050", + "build": "next build", + "serve": "next start --port=5050" + }, + "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-logger": "3.0.6", + "redux-saga": "1.1.3" + }, + "devDependencies": { + "@types/expect-puppeteer": "4.4.1", + "@types/jest": "25.2.1", + "@types/jest-environment-puppeteer": "4.3.1", + "@types/next-redux-saga": "3.0.2", + "@types/puppeteer": "2.0.1", + "@types/react": "16.9.35", + "@types/react-dom": "16.9.8", + "@types/react-redux": "7.1.8", + "@types/redux-logger": "3.0.7", + "@types/webpack-env": "1.15.2", + "jest": "26.0.1", + "jest-puppeteer": "4.4.0", + "next": "9.4.0", + "next-redux-wrapper-configs": "^6.0.0", + "puppeteer": "3.0.4", + "rimraf": "3.0.2", + "ts-jest": "25.5.1", + "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" +} diff --git a/packages/demo-saga-page/src/components/reducer.tsx b/packages/demo-saga-page/src/components/reducer.tsx new file mode 100644 index 0000000..a2cd4f0 --- /dev/null +++ b/packages/demo-saga-page/src/components/reducer.tsx @@ -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; diff --git a/packages/demo-saga-page/src/components/saga.tsx b/packages/demo-saga-page/src/components/saga.tsx new file mode 100644 index 0000000..33e8fa8 --- /dev/null +++ b/packages/demo-saga-page/src/components/saga.tsx @@ -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; diff --git a/packages/demo-saga-page/src/components/store.tsx b/packages/demo-saga-page/src/components/store.tsx new file mode 100644 index 0000000..6e76013 --- /dev/null +++ b/packages/demo-saga-page/src/components/store.tsx @@ -0,0 +1,26 @@ +import {createStore, applyMiddleware, Store} from 'redux'; +import logger from 'redux-logger'; +import createSagaMiddleware, {Task} from 'redux-saga'; +import {MakeStore, Context, createWrapper} from 'next-redux-wrapper'; +import reducer, {State} from './reducer'; +import rootSaga from './saga'; + +export interface SagaStore extends Store { + sagaTask: Task; +} + +const makeStore: MakeStore = (context: Context) => { + // 1: Create the middleware + const sagaMiddleware = createSagaMiddleware(); + + // 2: Add an extra parameter for applying middleware: + const store = createStore(reducer, applyMiddleware(sagaMiddleware, logger)); + + // 3: Run your sagas on server + (store as SagaStore).sagaTask = sagaMiddleware.run(rootSaga); + + // 4: now return the store: + return store; +}; + +export const wrapper = createWrapper(makeStore); diff --git a/packages/demo-saga-page/src/pages/_app.tsx b/packages/demo-saga-page/src/pages/_app.tsx new file mode 100644 index 0000000..2641773 --- /dev/null +++ b/packages/demo-saga-page/src/pages/_app.tsx @@ -0,0 +1,7 @@ +import React, {FC} from 'react'; +import {AppProps} from 'next/app'; +import {wrapper} from '../components/store'; + +const WrappedApp: FC = ({Component, pageProps}) => ; + +export default wrapper.withRedux(WrappedApp); diff --git a/packages/demo-saga-page/src/pages/index.tsx b/packages/demo-saga-page/src/pages/index.tsx new file mode 100644 index 0000000..f9fe983 --- /dev/null +++ b/packages/demo-saga-page/src/pages/index.tsx @@ -0,0 +1,31 @@ +import React from 'react'; +import {useSelector} from 'react-redux'; +import {NextPage} from 'next'; +import {END} from 'redux-saga'; +import {State} from '../components/reducer'; +import {SAGA_ACTION} from '../components/saga'; +import {SagaStore, wrapper} from '../components/store'; + +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 = ({custom}: ConnectedPageProps) => { + const {page} = useSelector(state => state); + return ( +
+
{JSON.stringify({page, custom}, null, 2)}
+
+ ); +}; + +export const getServerSideProps = wrapper.getServerSideProps(async ({store}) => { + store.dispatch({type: SAGA_ACTION}); + store.dispatch(END); + await (store as SagaStore).sagaTask.toPromise(); + + return {props: {custom: 'custom'}}; +}); + +export default Page; diff --git a/packages/demo-saga-page/tests/index.spec.ts b/packages/demo-saga-page/tests/index.spec.ts new file mode 100644 index 0000000..f96b0ba --- /dev/null +++ b/packages/demo-saga-page/tests/index.spec.ts @@ -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"'); + }); +}); diff --git a/packages/demo-saga-page/tsconfig.json b/packages/demo-saga-page/tsconfig.json new file mode 100644 index 0000000..e33bb37 --- /dev/null +++ b/packages/demo-saga-page/tsconfig.json @@ -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" + ] +} diff --git a/packages/wrapper/src/index.tsx b/packages/wrapper/src/index.tsx index c5c126e..452aed1 100644 --- a/packages/wrapper/src/index.tsx +++ b/packages/wrapper/src/index.tsx @@ -144,7 +144,9 @@ export const createWrapper = ( const getServerSideProps =

( callback: (context: GetServerSidePropsContext & {store: Store}) => P | void, - ): GetServerSideProps

=> getStaticProps

(callback as any) as any; // just not to repeat myself + ): GetServerSideProps

=> async (context: any) => { + return await getStaticProps(callback as any)(context); // just not to repeat myself + }; const withRedux = (Component: NextComponentType | App | any) => { const displayName = `withRedux(${Component.displayName || Component.name || 'Component'})`;