Skip to content

Add error link when hydration error occurs #31519

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Nov 23, 2021
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions errors/manifest.json
Original file line number Diff line number Diff line change
@@ -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"
53 changes: 53 additions & 0 deletions errors/react-hydration-error.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# React Hydration Error

#### Why This Error Occurred

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.

#### Possible Ways to Fix It

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 <h1 className={`title ${color}`}>Hello World!</h1>
}
```
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 <h1 className={`title ${color}`}>Hello World!</h1>
}
```

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/)
19 changes: 19 additions & 0 deletions packages/next/client/next-dev.js
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion packages/react-dev-overlay/src/client.ts
Original file line number Diff line number Diff line change
@@ -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'
Original file line number Diff line number Diff line change
@@ -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:
4 changes: 2 additions & 2 deletions packages/react-dev-overlay/src/internal/bus.ts
Original file line number Diff line number Diff line change
@@ -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
5 changes: 5 additions & 0 deletions test/integration/auto-export/pages/[post]/[cmnt].js
Original file line number Diff line number Diff line change
@@ -18,6 +18,11 @@ 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 <p>hydration error</p>
}
}
// eslint-disable-next-line
return <p>{useRouter().asPath}</p>
}
12 changes: 12 additions & 0 deletions test/integration/auto-export/test/index.test.js
Original file line number Diff line number Diff line change
@@ -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)
})
})
})