Skip to content

Commit

Permalink
recover from hydration errors without refresh
Browse files Browse the repository at this point in the history
  • Loading branch information
ForsakenHarmony committed Mar 31, 2023
1 parent 4f4a0ed commit 480e245
Show file tree
Hide file tree
Showing 4 changed files with 74 additions and 16 deletions.
3 changes: 0 additions & 3 deletions packages/next-swc/crates/next-core/js/src/dev/hmr-client.ts
Expand Up @@ -16,8 +16,6 @@ import type {
TurbopackGlobals,
} from '@vercel/turbopack-dev-runtime/types'

import stripAnsi from '@vercel/turbopack-next/compiled/strip-ansi'

import {
onBeforeRefresh,
onBuildOk,
Expand Down Expand Up @@ -464,7 +462,6 @@ function handleIssues(msg: ServerMessage): boolean {

for (const issue of msg.issues) {
if (CRITICAL.includes(issue.severity)) {
console.error(stripAnsi(issue.formatted))
hasCriticalIssues = true
}
}
Expand Down
11 changes: 9 additions & 2 deletions packages/next-swc/crates/next-core/js/src/entry/fallback.tsx
Expand Up @@ -29,6 +29,13 @@ initializeHMR({
})

const el = document.getElementById('__next')!
el.innerText = ''

createRoot(el).render(<ReactDevOverlay />)
const innerHtml = {
__html: el.innerHTML,
}

createRoot(el).render(
<ReactDevOverlay>
<div dangerouslySetInnerHTML={innerHtml}></div>
</ReactDevOverlay>
)
@@ -1,12 +1,16 @@
import React from 'react'

type ErrorBoundaryProps = {
error: Error | null
onError: (error: Error, componentStack: string | null) => void
fallback: React.ReactNode | null
children?: React.ReactNode
}

type ErrorBoundaryState = { error: Error | null }
type ErrorBoundaryState = {
error: Error | null
lastPropsError: Error | null
}

class ErrorBoundary extends React.PureComponent<
ErrorBoundaryProps,
Expand All @@ -16,7 +20,10 @@ class ErrorBoundary extends React.PureComponent<
return { error }
}

state = { error: null }
state = {
error: null,
lastPropsError: null,
}

componentDidCatch(
error: Error,
Expand All @@ -27,6 +34,33 @@ class ErrorBoundary extends React.PureComponent<
this.props.onError(error, errorInfo?.componentStack ?? null)
}

// I don't like this, but it works to clear the error boundary
//
// props.error is only set 1 render after state.error, so we can't treat it as the source of truth all the time.
// to get around this we store the error from props in state and only accept updates if the stored error from props
// matches the error in state.
//
// this is definitely not a controlled component and it only works if the error matches exactly
static getDerivedStateFromProps(
props: ErrorBoundaryProps,
state: ErrorBoundaryState
): ErrorBoundaryState | null {
if (state.lastPropsError === props.error) {
return null
}

let error = state.error

if (state.error === state.lastPropsError) {
error = props.error
}

return {
error,
lastPropsError: props.error,
}
}

render() {
const { error } = this.state

Expand Down
Expand Up @@ -20,6 +20,7 @@ type RefreshState =
// executed yet.
type: 'pending'
errors: SupportedErrorEvent[]
reactError: Error | null
}

type OverlayState = {
Expand Down Expand Up @@ -52,20 +53,16 @@ function pushErrorFilterDuplicates(
function reducer(state: OverlayState, ev: Bus.BusEvent): OverlayState {
switch (ev.type) {
case Bus.TYPE_BUILD_OK: {
if (state.reactError != null) {
console.warn(
'[Fast Refresh] performing full reload because your application had an unrecoverable error'
)
window.location.reload()
}

return { ...state }
}
case Bus.TYPE_TURBOPACK_ISSUES: {
return { ...state, issues: ev.issues }
}
case Bus.TYPE_BEFORE_REFRESH: {
return { ...state, refreshState: { type: 'pending', errors: [] } }
return {
...state,
refreshState: { type: 'pending', errors: [], reactError: null },
}
}
case Bus.TYPE_REFRESH: {
return {
Expand All @@ -80,6 +77,10 @@ function reducer(state: OverlayState, ev: Bus.BusEvent): OverlayState {
state.refreshState.type === 'pending'
? state.refreshState.errors
: [],
reactError:
state.refreshState.type === 'pending'
? state.refreshState.reactError
: null,
refreshState: { type: 'idle' },
}
}
Expand Down Expand Up @@ -114,7 +115,25 @@ function reducer(state: OverlayState, ev: Bus.BusEvent): OverlayState {
}
}
case Bus.TYPE_REACT_ERROR: {
return { ...state, reactError: ev.error }
switch (state.refreshState.type) {
case 'idle': {
return {
...state,
reactError: ev.error,
}
}
case 'pending': {
return {
...state,
refreshState: {
...state.refreshState,
reactError: ev.error,
},
}
}
default:
return state
}
}
default: {
return state
Expand Down Expand Up @@ -189,6 +208,7 @@ export default function ReactDevOverlay({
return (
<React.Fragment>
<ErrorBoundary
error={state.reactError}
onError={onComponentError}
fallback={
// When the overlay is global for the application and it wraps a component rendering `<html>`
Expand Down

0 comments on commit 480e245

Please sign in to comment.