From fd32cac32eeb338aae2364f13445dcd3e617790e Mon Sep 17 00:00:00 2001 From: Kirill Konshin Date: Mon, 1 Jun 2020 16:14:13 -0700 Subject: [PATCH] Prevent _app.js to be wrapped with default wrapper Fix #232 --- README.md | 27 ++++++++----------- packages/demo-page/src/pages/_app.tsx | 11 +++----- packages/demo-page/src/pages/_error.tsx | 36 ++++++++----------------- packages/wrapper/src/index.tsx | 20 +++++++++----- 4 files changed, 38 insertions(+), 56 deletions(-) diff --git a/README.md b/README.md index 645b9bf..9dbdd4a 100644 --- a/README.md +++ b/README.md @@ -129,16 +129,13 @@ export const wrapper = createWrapper(makeStore, {debug: true}); 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`: ```typescript -import React from 'react'; -import App, {AppInitialProps} from 'next/app'; +import React, {FC} from 'react'; +import {AppProps} from 'next/app'; import {wrapper} from '../components/store'; -class WrappedApp extends App { - public render() { - const {Component, pageProps} = this.props; - return ; - } -} +const WrappedApp: FC = ({Component, pageProps}) => ( + +); export default wrapper.withRedux(WrappedApp); ``` @@ -148,20 +145,18 @@ export default wrapper.withRedux(WrappedApp); ```js import React from 'react'; -import App from 'next/app'; import {wrapper} from '../components/store'; -class WrappedApp extends App { - public render() { - const {Component, pageProps} = this.props; - return ; - } -} +const MyApp = ({Component, pageProps}) => ( + +); -export default wrapper.withRedux(WrappedApp); +export default wrapper.withRedux(MyApp, {wrapDefaultGetInitialProps: true}); ``` +:warning: Next.js provides [generic `getInitialProps`](https://github.com/vercel/next.js/blob/canary/packages/next/pages/_app.tsx#L21) 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. + ## 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. diff --git a/packages/demo-page/src/pages/_app.tsx b/packages/demo-page/src/pages/_app.tsx index e8ea157..2641773 100644 --- a/packages/demo-page/src/pages/_app.tsx +++ b/packages/demo-page/src/pages/_app.tsx @@ -1,12 +1,7 @@ -import React from 'react'; -import App, {AppInitialProps} from 'next/app'; +import React, {FC} from 'react'; +import {AppProps} from 'next/app'; import {wrapper} from '../components/store'; -class WrappedApp extends App { - public render() { - const {Component, pageProps} = this.props; - return ; - } -} +const WrappedApp: FC = ({Component, pageProps}) => ; export default wrapper.withRedux(WrappedApp); diff --git a/packages/demo-page/src/pages/_error.tsx b/packages/demo-page/src/pages/_error.tsx index aa97158..d8c3002 100644 --- a/packages/demo-page/src/pages/_error.tsx +++ b/packages/demo-page/src/pages/_error.tsx @@ -1,31 +1,17 @@ -import React, {Component} from 'react'; +import React from 'react'; import Link from 'next/link'; -import {NextPageContext} from 'next'; import {connect} from 'react-redux'; import {State} from '../components/reducer'; -class ErrorPage extends Component { - public static getInitialProps = ({store, pathname}: NextPageContext) => { - console.log('2. Page.getInitialProps uses the store to dispatch things'); - store.dispatch({type: 'PAGE', payload: 'was set in error page ' + pathname}); - return {}; - }; - - render() { - const {page} = this.props; - return ( - <> -

- This is an error page, it also has access to store: {page} -

- - - ); - } -} +const ErrorPage = ({page}: any) => ( + <> +

This is an error page, {page}.

+ + +); export default connect((state: State) => state)(ErrorPage); diff --git a/packages/wrapper/src/index.tsx b/packages/wrapper/src/index.tsx index 7812362..c5c126e 100644 --- a/packages/wrapper/src/index.tsx +++ b/packages/wrapper/src/index.tsx @@ -165,10 +165,11 @@ export const createWrapper = ( const store = useRef>(initStore({makeStore, config, context})); const hydrate = useCallback(() => { - store.current.dispatch({ - type: HYDRATE, - payload: getDeserializedState(initialState, config), - } as any); + if (initialState) + store.current.dispatch({ + type: HYDRATE, + payload: getDeserializedState(initialState, config), + } as any); // ATTENTION! This code assumes that Page's getServerSideProps is executed after App.getInitialProps // so we dispatch in this order @@ -199,12 +200,17 @@ export const createWrapper = ( ...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; + let resultProps = props; + + // just some cleanup to prevent passing it as props, we need to clone props to safely delete initialState + if (initialStateFromGSPorGSSR) { + resultProps = {...props, pageProps: {...props.pageProps}}; + delete resultProps.pageProps.initialState; + } return ( - + ); };