From 0008fd898c66f1b852af183283d2fd87e3d8faac Mon Sep 17 00:00:00 2001 From: "jj@jjsweb.site" Date: Tue, 16 Nov 2021 16:21:23 -0600 Subject: [PATCH 1/4] Add error link when hydration error occurs --- errors/react-hydration-error.md | 17 +++++++++++++++++ packages/next/client/next-dev.js | 19 +++++++++++++++++++ packages/react-dev-overlay/src/client.ts | 2 +- .../src/internal/ReactDevOverlay.tsx | 2 +- .../react-dev-overlay/src/internal/bus.ts | 4 ++-- .../auto-export/pages/[post]/[cmnt].js | 4 ++++ .../auto-export/test/index.test.js | 12 ++++++++++++ 7 files changed, 56 insertions(+), 4 deletions(-) create mode 100644 errors/react-hydration-error.md diff --git a/errors/react-hydration-error.md b/errors/react-hydration-error.md new file mode 100644 index 0000000000000..7992e4ffc0a43 --- /dev/null +++ b/errors/react-hydration-error.md @@ -0,0 +1,17 @@ +# React Hydration Error + +#### Why This Error Occurred + +While rendering your application, different content was returned on the server than on the first render on the client (hydration). + +This can cause the react tree to be out of sync with the DOM and result in unexpected content/attributes being present. + +#### Possible Ways to Fix It + +Look for any differences in rendering on the server that rely on `typeof window` or `process.browser` checks that could cause a difference when rendered in the browser and delay these until after the component has mounted (after hydration). + +Ensure consistent values are being used, for example, don't render `Date.now()` directly in the component tree and instead set an initial value in `getStaticProps` which is updated after hydration `useEffect(() => {}, [])`. + +### Useful Links + +- [React Hydration Documentation](https://reactjs.org/docs/react-dom.html#hydrate) diff --git a/packages/next/client/next-dev.js b/packages/next/client/next-dev.js index 8ce7fe745877d..2caa31f55b8bb 100644 --- a/packages/next/client/next-dev.js +++ b/packages/next/client/next-dev.js @@ -26,6 +26,25 @@ const webpackHMR = initWebpackHMR() connectHMR({ assetPrefix: prefix, path: '/_next/webpack-hmr' }) +if (!window._nextSetupHydrationWarning) { + const origConsoleError = window.console.error + window.console.error = (...args) => { + const isHydrateError = args.some( + (arg) => + typeof arg === 'string' && + arg.match(/Warning:.*?did not match.*?Server:/) + ) + if (isHydrateError) { + args = [ + ...args, + `\n\nSee more info here: https://nextjs.org/docs/messages/react-hydration-error`, + ] + } + origConsoleError.apply(window.console, args) + } + window._nextSetupHydrationWarning = true +} + window.next = { version, // router is initialized later so it has to be live-binded diff --git a/packages/react-dev-overlay/src/client.ts b/packages/react-dev-overlay/src/client.ts index fa204341d73f1..b94c4e60e73d4 100644 --- a/packages/react-dev-overlay/src/client.ts +++ b/packages/react-dev-overlay/src/client.ts @@ -80,7 +80,7 @@ function onBuildError(message: string) { } function onRefresh() { - Bus.emit({ type: Bus.TYPE_REFFRESH }) + Bus.emit({ type: Bus.TYPE_REFRESH }) } export { getNodeError } from './internal/helpers/nodeStackFrames' diff --git a/packages/react-dev-overlay/src/internal/ReactDevOverlay.tsx b/packages/react-dev-overlay/src/internal/ReactDevOverlay.tsx index 7500e15983246..87888a86e2e64 100644 --- a/packages/react-dev-overlay/src/internal/ReactDevOverlay.tsx +++ b/packages/react-dev-overlay/src/internal/ReactDevOverlay.tsx @@ -22,7 +22,7 @@ function reducer(state: OverlayState, ev: Bus.BusEvent): OverlayState { case Bus.TYPE_BUILD_ERROR: { return { ...state, buildError: ev.message } } - case Bus.TYPE_REFFRESH: { + case Bus.TYPE_REFRESH: { return { ...state, buildError: null, errors: [] } } case Bus.TYPE_UNHANDLED_ERROR: diff --git a/packages/react-dev-overlay/src/internal/bus.ts b/packages/react-dev-overlay/src/internal/bus.ts index ebf32812ab695..1ffc87428b8b2 100644 --- a/packages/react-dev-overlay/src/internal/bus.ts +++ b/packages/react-dev-overlay/src/internal/bus.ts @@ -2,7 +2,7 @@ import { StackFrame } from 'stacktrace-parser' export const TYPE_BUILD_OK = 'build-ok' export const TYPE_BUILD_ERROR = 'build-error' -export const TYPE_REFFRESH = 'fast-refresh' +export const TYPE_REFRESH = 'fast-refresh' export const TYPE_UNHANDLED_ERROR = 'unhandled-error' export const TYPE_UNHANDLED_REJECTION = 'unhandled-rejection' @@ -11,7 +11,7 @@ export type BuildError = { type: typeof TYPE_BUILD_ERROR message: string } -export type FastRefresh = { type: typeof TYPE_REFFRESH } +export type FastRefresh = { type: typeof TYPE_REFRESH } export type UnhandledError = { type: typeof TYPE_UNHANDLED_ERROR reason: Error diff --git a/test/integration/auto-export/pages/[post]/[cmnt].js b/test/integration/auto-export/pages/[post]/[cmnt].js index 0982164d25db0..a8957ed20ae36 100644 --- a/test/integration/auto-export/pages/[post]/[cmnt].js +++ b/test/integration/auto-export/pages/[post]/[cmnt].js @@ -18,6 +18,10 @@ if (typeof window !== 'undefined') { export default function Page() { if (typeof window !== 'undefined') { window.pathnames.push(window.location.pathname) + + if (window.location.pathname.includes('hydrate-error')) { + return

hydration error

+ } } return

{useRouter().asPath}

} diff --git a/test/integration/auto-export/test/index.test.js b/test/integration/auto-export/test/index.test.js index e0eff8848d586..b38f1d4132fef 100644 --- a/test/integration/auto-export/test/index.test.js +++ b/test/integration/auto-export/test/index.test.js @@ -86,5 +86,17 @@ describe('Auto Export', () => { const caughtWarns = await browser.eval(`window.caughtWarns`) expect(caughtWarns).toEqual([]) }) + + it('should include error link when hydration error does occur', async () => { + const browser = await webdriver(appPort, '/post-1/hydrate-error') + const logs = await browser.log() + expect( + logs.some((log) => + log.message.includes( + 'See more info here: https://nextjs.org/docs/messages/react-hydration-error' + ) + ) + ).toBe(true) + }) }) }) From abcc8e878d66e1ae4ea6c069778d36cff009dc73 Mon Sep 17 00:00:00 2001 From: "jj@jjsweb.site" Date: Tue, 16 Nov 2021 16:41:03 -0600 Subject: [PATCH 2/4] add manifest entry --- errors/manifest.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/errors/manifest.json b/errors/manifest.json index 22f6415a94e2f..0b565a728d15e 100644 --- a/errors/manifest.json +++ b/errors/manifest.json @@ -4,6 +4,10 @@ "title": "Messages", "heading": true, "routes": [ + { + "title": "react-hydration-error", + "path": "/errors/react-hydration-error.md" + }, { "title": "beta-middleware", "path": "/errors/beta-middleware.md" From b44957acf9c5cebf035b7242fb7e77ed51c987c7 Mon Sep 17 00:00:00 2001 From: "jj@jjsweb.site" Date: Tue, 16 Nov 2021 16:58:45 -0600 Subject: [PATCH 3/4] lint-fix --- test/integration/auto-export/pages/[post]/[cmnt].js | 1 + 1 file changed, 1 insertion(+) diff --git a/test/integration/auto-export/pages/[post]/[cmnt].js b/test/integration/auto-export/pages/[post]/[cmnt].js index a8957ed20ae36..c3ffa5cf1ee29 100644 --- a/test/integration/auto-export/pages/[post]/[cmnt].js +++ b/test/integration/auto-export/pages/[post]/[cmnt].js @@ -23,5 +23,6 @@ export default function Page() { return

hydration error

} } + // eslint-disable-next-line return

{useRouter().asPath}

} From 6e883dcc6816dd363e21c19e8bfcb48e0b6926de Mon Sep 17 00:00:00 2001 From: Tim Neutkens Date: Fri, 19 Nov 2021 14:28:45 +0100 Subject: [PATCH 4/4] Update hydration error to explain why it happens with code and possible solutions --- errors/react-hydration-error.md | 46 +++++++++++++++++++++++++++++---- 1 file changed, 41 insertions(+), 5 deletions(-) diff --git a/errors/react-hydration-error.md b/errors/react-hydration-error.md index 7992e4ffc0a43..86c280d42a3da 100644 --- a/errors/react-hydration-error.md +++ b/errors/react-hydration-error.md @@ -2,16 +2,52 @@ #### Why This Error Occurred -While rendering your application, different content was returned on the server than on the first render on the client (hydration). +While rendering your application, there was a difference between the React tree that was pre-rendered (SSR/SSG) and the React tree that rendered during the first render in the Browser. The first render is called Hydration which is a [feature of React](https://reactjs.org/docs/react-dom.html#hydrate). -This can cause the react tree to be out of sync with the DOM and result in unexpected content/attributes being present. +This can cause the React tree to be out of sync with the DOM and result in unexpected content/attributes being present. #### Possible Ways to Fix It -Look for any differences in rendering on the server that rely on `typeof window` or `process.browser` checks that could cause a difference when rendered in the browser and delay these until after the component has mounted (after hydration). - -Ensure consistent values are being used, for example, don't render `Date.now()` directly in the component tree and instead set an initial value in `getStaticProps` which is updated after hydration `useEffect(() => {}, [])`. +In general this issue is caused by using a specific library or application code that is relying on something that could differ between pre-rendering and the browser. An example of this is using `window` in a component's rendering. + +An example: + +```jsx +function MyComponent() { + // This condition depends on `window`. During the first render of the browser the `color` variable will be different + const color = typeof window !== 'undefined' ? 'red' : 'blue + // As color is passed as a prop there is a mismatch between what was rendered server-side vs what was rendered in the first render + return

Hello World!

+} +``` + +How to fix it: + +```jsx +// In order to prevent the first render from being different you can use `useEffect` which is only executed in the browser and is executed during hydration +import { useEffect, useState } from 'react' +function MyComponent() { + // The default value is 'blue', it will be used during pre-rendering and the first render in the browser (hydration) + const [color, setColor] = useState('blue') + // During hydration `useEffect` is called. `window` is available in `useEffect`. In this case because we know we're in the browser checking for window is not needed. If you need to read something from window that is fine. + // By calling `setColor` in `useEffect` a render is triggered after hydrating, this causes the "browser specific" value to be available. In this case 'red'. + useEffect(() => setColor('red'), []) + // As color is a state passed as a prop there is no mismatch between what was rendered server-side vs what was rendered in the first render. After useEffect runs the color is set to 'red' + return

Hello World!

+} +``` + +Common causes with css-in-js libraries: + +- When using Styled Components / Emotion + - When css-in-js libraries are not set up for pre-rendering (SSR/SSG) it will often lead to a hydration mismatch. In general this means the application has to follow the Next.js example for the library. For example if `pages/_document` is missing and the Babel plugin is not added. + - Possible fix for Styled Components: https://github.com/vercel/next.js/tree/canary/examples/with-styled-components + - If you want to leverage Styled Components with the new Next.js Compiler in Next.js 12 there is an [experimental flag available](https://github.com/vercel/next.js/discussions/30174#discussion-3643870) + - Possible fix for Emotion: https://github.com/vercel/next.js/tree/canary/examples/with-emotion +- When using other css-in-js libraries + - Similar to Styled Components / Emotion css-in-js libraries generally need configuration specified in their examples in the [examples directory](https://github.com/vercel/next.js/tree/canary/examples) ### Useful Links - [React Hydration Documentation](https://reactjs.org/docs/react-dom.html#hydrate) +- [Josh Comeau's article on React Hydration](https://www.joshwcomeau.com/react/the-perils-of-rehydration/)