From 4d4f9fb4956f155db22a31395ff34c0f00329041 Mon Sep 17 00:00:00 2001 From: Kirill Konshin Date: Tue, 21 Apr 2020 17:31:49 -0700 Subject: [PATCH] 6.0.0-rc.8 - Fix for static state not applied on navigation - Redux logger in demos --- packages/demo-page/package.json | 4 +- packages/demo-page/src/components/store.tsx | 5 +- packages/demo-saga/package.json | 2 + packages/demo-saga/src/components/store.tsx | 5 +- packages/demo/package.json | 4 +- packages/demo/src/components/store.tsx | 5 +- packages/demo/src/pages/static.tsx | 4 +- packages/demo/tests/index.spec.ts | 11 ++ packages/wrapper/src/index.tsx | 130 ++++++++++---------- yarn.lock | 19 +++ 10 files changed, 113 insertions(+), 76 deletions(-) diff --git a/packages/demo-page/package.json b/packages/demo-page/package.json index cce716b..b4d21e5 100644 --- a/packages/demo-page/package.json +++ b/packages/demo-page/package.json @@ -16,7 +16,8 @@ "react": "16.13.1", "react-dom": "16.13.1", "react-redux": "7.2.0", - "redux": "4.0.5" + "redux": "4.0.5", + "redux-logger": "3.0.6" }, "devDependencies": { "@types/expect-puppeteer": "4.4.0", @@ -26,6 +27,7 @@ "@types/react": "16.9.31", "@types/react-dom": "16.9.6", "@types/react-redux": "7.1.7", + "@types/redux-logger": "3.0.7", "@types/webpack-env": "1.15.1", "jest": "25.2.4", "jest-puppeteer": "4.4.0", diff --git a/packages/demo-page/src/components/store.tsx b/packages/demo-page/src/components/store.tsx index fb3d781..2c2ca1f 100644 --- a/packages/demo-page/src/components/store.tsx +++ b/packages/demo-page/src/components/store.tsx @@ -1,9 +1,10 @@ -import {createStore} from 'redux'; +import {createStore, applyMiddleware} from 'redux'; +import logger from 'redux-logger'; import {MakeStore, createWrapper, Context} from 'next-redux-wrapper'; import reducer, {State} from './reducer'; export const makeStore: MakeStore = (context: Context) => { - const store = createStore(reducer); + const store = createStore(reducer, applyMiddleware(logger)); if (module.hot) { module.hot.accept('./reducer', () => { diff --git a/packages/demo-saga/package.json b/packages/demo-saga/package.json index f4ef7d7..df9b97c 100644 --- a/packages/demo-saga/package.json +++ b/packages/demo-saga/package.json @@ -17,6 +17,7 @@ "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": { @@ -28,6 +29,7 @@ "@types/react": "16.9.31", "@types/react-dom": "16.9.6", "@types/react-redux": "7.1.7", + "@types/redux-logger": "3.0.7", "@types/webpack-env": "1.15.1", "jest": "25.2.4", "jest-puppeteer": "4.4.0", diff --git a/packages/demo-saga/src/components/store.tsx b/packages/demo-saga/src/components/store.tsx index 3e45d1b..34b8af4 100644 --- a/packages/demo-saga/src/components/store.tsx +++ b/packages/demo-saga/src/components/store.tsx @@ -1,6 +1,7 @@ import {createStore, applyMiddleware, Store} from 'redux'; -import {MakeStore, Context} from 'next-redux-wrapper'; +import logger from 'redux-logger'; import createSagaMiddleware, {Task} from 'redux-saga'; +import {MakeStore, Context} from 'next-redux-wrapper'; import reducer, {State} from './reducer'; import rootSaga from './saga'; @@ -13,7 +14,7 @@ export const makeStore: MakeStore = (context: Context) => { const sagaMiddleware = createSagaMiddleware(); // 2: Add an extra parameter for applying middleware: - const store = createStore(reducer, applyMiddleware(sagaMiddleware)); + const store = createStore(reducer, applyMiddleware(sagaMiddleware, logger)); // 3: Run your sagas on server (store as SagaStore).sagaTask = sagaMiddleware.run(rootSaga); diff --git a/packages/demo/package.json b/packages/demo/package.json index e7a3b69..aa4ff7e 100644 --- a/packages/demo/package.json +++ b/packages/demo/package.json @@ -15,7 +15,8 @@ "react": "16.13.1", "react-dom": "16.13.1", "react-redux": "7.2.0", - "redux": "4.0.5" + "redux": "4.0.5", + "redux-logger": "3.0.6" }, "devDependencies": { "@types/expect-puppeteer": "4.4.0", @@ -25,6 +26,7 @@ "@types/react": "16.9.31", "@types/react-dom": "16.9.6", "@types/react-redux": "7.1.7", + "@types/redux-logger": "3.0.7", "@types/webpack-env": "1.15.1", "jest": "25.2.4", "jest-puppeteer": "4.4.0", diff --git a/packages/demo/src/components/store.tsx b/packages/demo/src/components/store.tsx index fb3d781..2c2ca1f 100644 --- a/packages/demo/src/components/store.tsx +++ b/packages/demo/src/components/store.tsx @@ -1,9 +1,10 @@ -import {createStore} from 'redux'; +import {createStore, applyMiddleware} from 'redux'; +import logger from 'redux-logger'; import {MakeStore, createWrapper, Context} from 'next-redux-wrapper'; import reducer, {State} from './reducer'; export const makeStore: MakeStore = (context: Context) => { - const store = createStore(reducer); + const store = createStore(reducer, applyMiddleware(logger)); if (module.hot) { module.hot.accept('./reducer', () => { diff --git a/packages/demo/src/pages/static.tsx b/packages/demo/src/pages/static.tsx index 2720d3b..78288fb 100644 --- a/packages/demo/src/pages/static.tsx +++ b/packages/demo/src/pages/static.tsx @@ -10,7 +10,7 @@ interface OtherProps { appProp: string; } -const Other: NextPage = ({appProp, getStaticProp}) => { +const Static: NextPage = ({appProp, getStaticProp}) => { const {app, page} = useSelector(state => state); return (
@@ -32,4 +32,4 @@ export const getStaticProps = wrapper.getStaticProps(({store}) => { return {props: {getStaticProp: 'bar'}}; }); -export default Other; +export default Static; diff --git a/packages/demo/tests/index.spec.ts b/packages/demo/tests/index.spec.ts index 97155c5..02e545f 100644 --- a/packages/demo/tests/index.spec.ts +++ b/packages/demo/tests/index.spec.ts @@ -48,4 +48,15 @@ describe('Using App wrapper', () => { await expect(page).toMatch('"page": "static"'); // redux await expect(page).toMatch('"app": "was set in _app"'); // redux }); + + it('other page -> static', async () => { + await openPage('/'); + + await page.waitForSelector('div.index'); + + await expect(page).toClick('a', {text: 'Navigate to static'}); + + await expect(page).toMatch('"page": "static"'); // redux + await expect(page).toMatch('"app": "was set in _app"'); // redux + }); }); diff --git a/packages/wrapper/src/index.tsx b/packages/wrapper/src/index.tsx index 106d5f5..4d61dbc 100644 --- a/packages/wrapper/src/index.tsx +++ b/packages/wrapper/src/index.tsx @@ -1,7 +1,7 @@ -import * as React from 'react'; +import React, {useCallback, useEffect, useRef} from 'react'; import {Store, AnyAction, Action} from 'redux'; import {Provider} from 'react-redux'; -import {GetServerSideProps, GetStaticProps, NextComponentType, NextPageContext} from 'next'; +import {GetServerSideProps, GetStaticProps, NextComponentType, NextPage, NextPageContext} from 'next'; import App, {AppContext, AppInitialProps} from 'next/app'; import {IncomingMessage, ServerResponse} from 'http'; import {ParsedUrlQuery} from 'querystring'; @@ -24,7 +24,7 @@ export declare type MakeStore = (context: export interface InitStoreOptions { makeStore: MakeStore; context: Context; - config: Config; + config: Config; } const initStore = ({ @@ -77,7 +77,7 @@ export interface GetStaticPropsContext { export const createWrapper = ( makeStore: MakeStore, - config: Config = {}, + config: Config = {}, ) => { const makeProps = async ({ callback, @@ -147,77 +147,75 @@ export const createWrapper = ( ): GetServerSideProps

=> getStaticProps

(callback as any) as any; // just not to repeat myself const withRedux = (Component: NextComponentType | App | any) => { - const hasGetInitialProps = 'getInitialProps' in Component; - const displayName = `withRedux(${Component.displayName || Component.name || 'Component'})`; + //TODO Check if pages/_app was wrapped so there's no need to wrap a page itself + const Wrapper: NextPage = ({initialState, initialProps, ...props}, context) => { + const isFirstRender = useRef(true); - class Wrapper extends React.Component { - public store: Store; + // this happens when App has page with getServerSideProps/getStaticProps + const initialStateFromGSPorGSSR = props?.pageProps?.initialState; - public constructor(props: WrapperProps, context: AppContext) { - super(props, context); + if (config.debug) + console.log('4. WrappedApp.constructor created new store with', { + initialState, + initialStateFromGSPorGSSR, + }); - const {initialState} = props; + const store = useRef>(initStore({makeStore, config, context})); - if (config.debug) - console.log('4. WrappedApp.constructor created new store with initialState', initialState); - - //TODO Check if pages/_app was wrapped and there's no need to wrap a page itself - this.store = initStore({makeStore, config, context}); - - this.store.dispatch({ + const hydrate = useCallback(() => { + store.current.dispatch({ type: HYDRATE, payload: getDeserializedState(initialState, config), } as any); - if (props?.pageProps?.initialState) { - this.store.dispatch({ + // ATTENTION! This code assumes that Page's getServerSideProps is executed after App.getInitialProps + // so we dispatch in this order + if (initialStateFromGSPorGSSR) + store.current.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, - ), + payload: getDeserializedState(initialStateFromGSPorGSSR, config), } as any); + }, [initialStateFromGSPorGSSR, initialState]); + + // apply synchronously on first render (both server side and client side) + if (isFirstRender.current) hydrate(); + + // apply async in case props have changed, on navigation for example + useEffect(() => { + if (isFirstRender.current) { + isFirstRender.current = false; + return; } - } - - public render() { - const {initialState, initialProps, ...props} = this.props as any; - - // order is important! Next.js overwrites props from pages/_app with getStaticProps from page - // @see https://github.com/zeit/next.js/issues/11648 - if (initialProps && initialProps.pageProps) - props.pageProps = { - ...initialProps.pageProps, // this comes from wrapper in _app mode - ...props.pageProps, // this comes from gssp/gsp in _app mode - }; - - if (props.pageProps) { - // this happens when App has page with getServerSideProps - // just some cleanup here - delete props.pageProps.initialState; - } + hydrate(); + }, [hydrate]); + + // order is important! Next.js overwrites props from pages/_app with getStaticProps from page + // @see https://github.com/zeit/next.js/issues/11648 + if (initialProps && initialProps.pageProps) + props.pageProps = { + ...initialProps.pageProps, // this comes from wrapper in _app mode + ...props.pageProps, // this comes from gssp/gsp in _app mode + }; + + // just some cleanup to prevent passing it as props + if (initialStateFromGSPorGSSR) delete props.pageProps.initialState; + + return ( + + + + ); + }; - return ( - - - - ); - } - } + Wrapper.displayName = `withRedux(${Component.displayName || Component.name || 'Component'})`; + + if ('getInitialProps' in Component) + Wrapper.getInitialProps = async (context: any) => { + const callback = Component.getInitialProps; // bind? + return (context.ctx ? getInitialAppProps(callback) : getInitialPageProps(callback))(context); + }; - return hasGetInitialProps - ? class WrappedCmp extends Wrapper { - public static displayName = displayName; - public static getInitialProps = async (context: any) => { - const callback = Component.getInitialProps; // bind? - return (context.ctx ? getInitialAppProps(callback) : getInitialPageProps(callback))(context); - }; - } - : class WrappedCmp extends Wrapper { - public static displayName = displayName; - }; + return Wrapper; }; return { @@ -230,16 +228,16 @@ export const createWrapper = ( // Legacy export default (makeStore: MakeStore, config: Config = {}) => { console.warn( - '/!\\ You are using legacy implementaion. Please update your code: use createWrapper and wrapper.withRedux.', + '/!\\ You are using legacy implementaion. Please update your code: use createWrapper() and wrapper.withRedux().', ); return createWrapper(makeStore, config).withRedux; }; export type Context = NextPageContext | AppContext | GetStaticPropsContext | GetServerSidePropsContext; -export interface Config { - serializeState?: (state: any) => any; - deserializeState?: (state: any) => any; +export interface Config { + serializeState?: (state: S) => any; + deserializeState?: (state: any) => S; storeKey?: string; debug?: boolean; } diff --git a/yarn.lock b/yarn.lock index d641e8e..e55c45a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2542,6 +2542,13 @@ "@types/prop-types" "*" csstype "^2.2.0" +"@types/redux-logger@3.0.7": + version "3.0.7" + resolved "https://registry.yarnpkg.com/@types/redux-logger/-/redux-logger-3.0.7.tgz#163f6f6865c69c21d56f9356dc8d741718ec0db0" + integrity sha512-oV9qiCuowhVR/ehqUobWWkXJjohontbDGLV88Be/7T4bqMQ3kjXwkFNL7doIIqlbg3X2PC5WPziZ8/j/QHNQ4A== + dependencies: + redux "^3.6.0" + "@types/redux-promise-middleware@6.0.0": version "6.0.0" resolved "https://registry.yarnpkg.com/@types/redux-promise-middleware/-/redux-promise-middleware-6.0.0.tgz#df30dc25396d731abf8289111d2399609cb5bd47" @@ -4434,6 +4441,11 @@ dedent@^0.7.0: resolved "https://registry.yarnpkg.com/dedent/-/dedent-0.7.0.tgz#2495ddbaf6eb874abb0e1be9df22d2e5a544326c" integrity sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw= +deep-diff@^0.3.5: + version "0.3.8" + resolved "https://registry.yarnpkg.com/deep-diff/-/deep-diff-0.3.8.tgz#c01de63efb0eec9798801d40c7e0dae25b582c84" + integrity sha1-wB3mPvsO7JeYgB1Ax+Da4ltYLIQ= + deep-extend@^0.6.0: version "0.6.0" resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" @@ -9585,6 +9597,13 @@ redent@^2.0.0: indent-string "^3.0.0" strip-indent "^2.0.0" +redux-logger@3.0.6: + version "3.0.6" + resolved "https://registry.yarnpkg.com/redux-logger/-/redux-logger-3.0.6.tgz#f7555966f3098f3c88604c449cf0baf5778274bf" + integrity sha1-91VZZvMJjzyIYExEnPC69XeCdL8= + dependencies: + deep-diff "^0.3.5" + redux-promise-middleware@*, redux-promise-middleware@6.1.2: version "6.1.2" resolved "https://registry.yarnpkg.com/redux-promise-middleware/-/redux-promise-middleware-6.1.2.tgz#1c14222686934be243cbb292e348ef7d5b20d6d2"