Skip to content

Commit

Permalink
Fix hydration errors caused by root ErrorOverlay (vercel#53216)
Browse files Browse the repository at this point in the history
A bug was introduced in vercel#52843 that causes hydration issues -- this
fixes that by moving the previous logic into the existing `isError` path
that doesn't trigger a call to `hydrateRoot` ensuring we are only doing
this on the client tree

Fixes vercel#53110 and Fixes vercel#53006
Closes NEXT-1470
  • Loading branch information
ztanner authored and Strift committed Jul 27, 2023
1 parent d489cff commit 3bd43ba
Show file tree
Hide file tree
Showing 4 changed files with 74 additions and 88 deletions.
103 changes: 43 additions & 60 deletions packages/next/src/client/app-index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -226,8 +226,6 @@ const StrictModeIfEnabled = process.env.__NEXT_STRICT_MODE_APP
: React.Fragment

function Root({ children }: React.PropsWithChildren<{}>): React.ReactElement {
const [hadRuntimeError, setHadRuntimeError] = React.useState(false)

if (process.env.__NEXT_ANALYTICS_ID) {
// eslint-disable-next-line react-hooks/rules-of-hooks
React.useEffect(() => {
Expand All @@ -246,63 +244,6 @@ function Root({ children }: React.PropsWithChildren<{}>): React.ReactElement {
}, [])
}

if (process.env.NODE_ENV !== 'production') {
const ReactDevOverlay: typeof import('./components/react-dev-overlay/internal/ReactDevOverlay').default =
require('./components/react-dev-overlay/internal/ReactDevOverlay')
.default as typeof import('./components/react-dev-overlay/internal/ReactDevOverlay').default

const INITIAL_OVERLAY_STATE: typeof import('./components/react-dev-overlay/internal/error-overlay-reducer').INITIAL_OVERLAY_STATE =
require('./components/react-dev-overlay/internal/error-overlay-reducer').INITIAL_OVERLAY_STATE

const useWebsocket: typeof import('./components/react-dev-overlay/internal/helpers/use-websocket').useWebsocket =
require('./components/react-dev-overlay/internal/helpers/use-websocket').useWebsocket

// subscribe to hmr only if an error was captured, so that we don't have two hmr websockets active
// eslint-disable-next-line react-hooks/rules-of-hooks
const webSocketRef = useWebsocket(
process.env.__NEXT_ASSET_PREFIX || '',
hadRuntimeError
)

// eslint-disable-next-line react-hooks/rules-of-hooks
React.useEffect(() => {
const handler = (event: MessageEvent) => {
let obj
try {
obj = JSON.parse(event.data)
} catch {}

if (!obj || !('action' in obj)) {
return
}

// minimal "hot reload" support for RSC errors
if (obj.action === 'serverComponentChanges' && hadRuntimeError) {
window.location.reload()
}
}

const websocket = webSocketRef.current
if (websocket) {
websocket.addEventListener('message', handler)
}

return () =>
websocket && websocket.removeEventListener('message', handler)
}, [webSocketRef, hadRuntimeError])

// if an error is thrown while rendering an RSC stream, this will catch it in dev
// and show the error overlay
return (
<ReactDevOverlay
state={INITIAL_OVERLAY_STATE}
onReactError={() => setHadRuntimeError(true)}
>
{children}
</ReactDevOverlay>
)
}

return children as React.ReactElement
}

Expand Down Expand Up @@ -385,7 +326,49 @@ export function hydrate() {
}

if (isError) {
ReactDOMClient.createRoot(appElement as any, options).render(reactEl)
if (process.env.NODE_ENV !== 'production') {
// if an error is thrown while rendering an RSC stream, this will catch it in dev
// and show the error overlay
const ReactDevOverlay: typeof import('./components/react-dev-overlay/internal/ReactDevOverlay').default =
require('./components/react-dev-overlay/internal/ReactDevOverlay')
.default as typeof import('./components/react-dev-overlay/internal/ReactDevOverlay').default

const INITIAL_OVERLAY_STATE: typeof import('./components/react-dev-overlay/internal/error-overlay-reducer').INITIAL_OVERLAY_STATE =
require('./components/react-dev-overlay/internal/error-overlay-reducer').INITIAL_OVERLAY_STATE

const getSocketUrl: typeof import('./components/react-dev-overlay/internal/helpers/get-socket-url').getSocketUrl =
require('./components/react-dev-overlay/internal/helpers/get-socket-url')
.getSocketUrl as typeof import('./components/react-dev-overlay/internal/helpers/get-socket-url').getSocketUrl

let errorTree = (
<ReactDevOverlay state={INITIAL_OVERLAY_STATE} onReactError={() => {}}>
{reactEl}
</ReactDevOverlay>
)
const socketUrl = getSocketUrl(process.env.__NEXT_ASSET_PREFIX || '')
const socket = new window.WebSocket(`${socketUrl}/_next/webpack-hmr`)

// add minimal "hot reload" support for RSC errors
const handler = (event: MessageEvent) => {
let obj
try {
obj = JSON.parse(event.data)
} catch {}

if (!obj || !('action' in obj)) {
return
}

if (obj.action === 'serverComponentChanges') {
window.location.reload()
}
}

socket.addEventListener('message', handler)
ReactDOMClient.createRoot(appElement as any, options).render(errorTree)
} else {
ReactDOMClient.createRoot(appElement as any, options).render(reactEl)
}
} else {
React.startTransition(() =>
(ReactDOMClient as any).hydrateRoot(appElement, reactEl, options)
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
function getSocketProtocol(assetPrefix: string): string {
let protocol = window.location.protocol

try {
// assetPrefix is a url
protocol = new URL(assetPrefix).protocol
} catch (_) {}

return protocol === 'http:' ? 'ws' : 'wss'
}

export function getSocketUrl(assetPrefix: string): string {
const { hostname, port } = window.location
const protocol = getSocketProtocol(assetPrefix)
const normalizedAssetPrefix = assetPrefix.replace(/^\/+/, '')

let url = `${protocol}://${hostname}:${port}${
normalizedAssetPrefix ? `/${normalizedAssetPrefix}` : ''
}`

if (normalizedAssetPrefix.startsWith('http')) {
url = `${protocol}://${normalizedAssetPrefix.split('://')[1]}`
}

return url
}
Original file line number Diff line number Diff line change
@@ -1,32 +1,19 @@
import { useCallback, useContext, useEffect, useRef } from 'react'
import { GlobalLayoutRouterContext } from '../../../../../shared/lib/app-router-context'
import { getSocketProtocol } from './get-socket-protocol'
import { getSocketUrl } from './get-socket-url'

export function useWebsocket(
assetPrefix: string,
shouldSubscribe: boolean = true
) {
export function useWebsocket(assetPrefix: string) {
const webSocketRef = useRef<WebSocket>()

useEffect(() => {
if (webSocketRef.current || !shouldSubscribe) {
if (webSocketRef.current) {
return
}

const { hostname, port } = window.location
const protocol = getSocketProtocol(assetPrefix)
const normalizedAssetPrefix = assetPrefix.replace(/^\/+/, '')

let url = `${protocol}://${hostname}:${port}${
normalizedAssetPrefix ? `/${normalizedAssetPrefix}` : ''
}`

if (normalizedAssetPrefix.startsWith('http')) {
url = `${protocol}://${normalizedAssetPrefix.split('://')[1]}`
}
const url = getSocketUrl(assetPrefix)

webSocketRef.current = new window.WebSocket(`${url}/_next/webpack-hmr`)
}, [assetPrefix, shouldSubscribe])
}, [assetPrefix])

return webSocketRef
}
Expand Down

0 comments on commit 3bd43ba

Please sign in to comment.