From ac96c1b6735d0eceb360c5841357dbcb101aed69 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Wed, 10 Apr 2024 15:57:10 -0400 Subject: [PATCH 1/3] API docs for useDeferredValue's initialValue Updates the API docs for `useDeferredValue` to include the `initialValue` option, added in https://github.com/facebook/react/pull/27500. This feature is slated for release in React 19. --- src/content/reference/react/useDeferredValue.md | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/content/reference/react/useDeferredValue.md b/src/content/reference/react/useDeferredValue.md index f97bf0a281a..fa8576bbbd2 100644 --- a/src/content/reference/react/useDeferredValue.md +++ b/src/content/reference/react/useDeferredValue.md @@ -18,7 +18,7 @@ const deferredValue = useDeferredValue(value) ## Reference {/*reference*/} -### `useDeferredValue(value)` {/*usedeferredvalue*/} +### `useDeferredValue(value, initialValue?)` {/*usedeferredvalue*/} Call `useDeferredValue` at the top level of your component to get a deferred version of that value. @@ -37,10 +37,19 @@ function SearchPage() { #### Parameters {/*parameters*/} * `value`: The value you want to defer. It can have any type. +* **optional** `initialValue`: A value to use during the initial render of a component. If this option is omitted, `useDeferredValue` will not defer during the initial render, because there's no previous version of `value` that it can render instead. + #### Returns {/*returns*/} -During the initial render, the returned deferred value will be the same as the value you provided. During updates, React will first attempt a re-render with the old value (so it will return the old value), and then try another re-render in the background with the new value (so it will return the updated value). +Returns either `value`, the old `value` that was previously rendered to the screen, or `initialValue`, depending on the scenario: + +- During the initial render... + - If `initialValue` _is_ provided, it first returns `initialValue`, then spawns a deferred render to switch to `value`. + - If `initialValue` _is not_ provided, it returns `value`, and does not spawn a deferred render. +- During an update... + - If the update _is_ the result of a Transition, it returns the new `value`, and does not spawn a deferred render. + - If the update _is not_ the result of a Transition, it first returns the old `value`, then spawns a deferred render to switch to the new `value`. #### Caveats {/*caveats*/} From 3fab61969877e46bf3af3a6b89557c2a42b590d9 Mon Sep 17 00:00:00 2001 From: Ricky Date: Thu, 11 Apr 2024 08:58:59 -0400 Subject: [PATCH 2/3] Add docs for onCaughtError and onUncaughtError (#6742) * Add docs for onCaughtError and onUncaughtError * Updates from feedback --- src/components/Icon/IconCanary.tsx | 56 +- src/components/MDX/MDXComponents.tsx | 16 + .../reference/react-dom/client/createRoot.md | 817 ++++++++++++++++- .../reference/react-dom/client/hydrateRoot.md | 827 +++++++++++++++++- 4 files changed, 1689 insertions(+), 27 deletions(-) diff --git a/src/components/Icon/IconCanary.tsx b/src/components/Icon/IconCanary.tsx index a7782b14150..7f584fed76e 100644 --- a/src/components/Icon/IconCanary.tsx +++ b/src/components/Icon/IconCanary.tsx @@ -4,29 +4,35 @@ import {memo} from 'react'; -export const IconCanary = memo( - function IconCanary({className, title}) { - return ( - - {title && {title}} - - - - - - - ); +export const IconCanary = memo< + JSX.IntrinsicElements['svg'] & {title?: string; size?: 's' | 'md'} +>(function IconCanary( + {className, title, size} = { + className: undefined, + title: undefined, + size: 'md', } -); +) { + return ( + + {title && {title}} + + + + + + + ); +}); diff --git a/src/components/MDX/MDXComponents.tsx b/src/components/MDX/MDXComponents.tsx index 76bf86eaa0f..e42b3b2e870 100644 --- a/src/components/MDX/MDXComponents.tsx +++ b/src/components/MDX/MDXComponents.tsx @@ -33,6 +33,7 @@ import type {Toc, TocItem} from './TocContext'; import {TeamMember} from './TeamMember'; import ErrorDecoder from './ErrorDecoder'; +import {IconCanary} from '../Icon/IconCanary'; function CodeStep({children, step}: {children: any; step: number}) { return ( @@ -94,6 +95,20 @@ const Canary = ({children}: {children: React.ReactNode}) => ( {children} ); +const CanaryBadge = ({title}: {title: string}) => ( + + + Canary only + +); + const Blockquote = ({ children, ...props @@ -430,6 +445,7 @@ export const MDXComponents = { MathI, Note, Canary, + CanaryBadge, PackageImport, ReadBlogPost, Recap, diff --git a/src/content/reference/react-dom/client/createRoot.md b/src/content/reference/react-dom/client/createRoot.md index afddb4177d3..b336b6e5ed2 100644 --- a/src/content/reference/react-dom/client/createRoot.md +++ b/src/content/reference/react-dom/client/createRoot.md @@ -45,7 +45,9 @@ An app fully built with React will usually only have one `createRoot` call for i * **optional** `options`: An object with options for this React root. - * **optional** `onRecoverableError`: Callback called when React automatically recovers from errors. + * **optional** `onCaughtError`: Callback called when React catches an error in an Error Boundary. Called with the `error` caught by the Error Boundary, and an `errorInfo` object containing the `componentStack`. + * **optional** `onUncaughtError`: Callback called when an error is thrown and not caught by an Error Boundary. Called with the `error` that was thrown, and an `errorInfo` object containing the `componentStack`. + * **optional** `onRecoverableError`: Callback called when React automatically recovers from errors. Called with an `error` React throws, and an `errorInfo` object containing the `componentStack`. Some recoverable errors may include the original error cause as `error.cause`. * **optional** `identifierPrefix`: A string prefix React uses for IDs generated by [`useId`.](/reference/react/useId) Useful to avoid conflicts when using multiple roots on the same page. #### Returns {/*returns*/} @@ -342,6 +344,797 @@ export default function App({counter}) { It is uncommon to call `render` multiple times. Usually, your components will [update state](/reference/react/useState) instead. +### Show a dialog for uncaught errors {/*show-a-dialog-for-uncaught-errors*/} + + + +`onUncaughtError` is only available in the latest React Canary release. + + + +By default, React will log all uncaught errors to the console. To implement your own error reporting, you can provide the optional `onUncaughtError` root option: + +```js [[1, 6, "onUncaughtError"], [2, 6, "error", 1], [3, 6, "errorInfo"], [4, 10, "componentStack"]] +import { createRoot } from 'react-dom/client'; + +const root = createRoot( + document.getElementById('root'), + { + onUncaughtError: (error, errorInfo) => { + console.error( + 'Uncaught error', + error, + errorInfo.componentStack + ); + } + } +); +root.render(); +``` + +The onUncaughtError option is a function called with two arguments: + +1. The error that was thrown. +2. An errorInfo object that contains the componentStack of the error. + +You can use the `onUncaughtError` root option to display error dialogs: + + + +```html index.html hidden + + + + My app + + + + + +
+ + +``` + +```css src/styles.css active +label, button { display: block; margin-bottom: 20px; } +html, body { min-height: 300px; } + +#error-dialog { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + background-color: white; + padding: 15px; + opacity: 0.9; + text-wrap: wrap; + overflow: scroll; +} + +.text-red { + color: red; +} + +.-mb-20 { + margin-bottom: -20px; +} + +.mb-0 { + margin-bottom: 0; +} + +.mb-10 { + margin-bottom: 10px; +} + +pre { + text-wrap: wrap; +} + +pre.nowrap { + text-wrap: nowrap; +} + +.hidden { + display: none; +} +``` + +```js src/reportError.js hidden +function reportError({ title, error, componentStack, dismissable }) { + const errorDialog = document.getElementById("error-dialog"); + const errorTitle = document.getElementById("error-title"); + const errorMessage = document.getElementById("error-message"); + const errorBody = document.getElementById("error-body"); + const errorComponentStack = document.getElementById("error-component-stack"); + const errorStack = document.getElementById("error-stack"); + const errorClose = document.getElementById("error-close"); + const errorCause = document.getElementById("error-cause"); + const errorCauseMessage = document.getElementById("error-cause-message"); + const errorCauseStack = document.getElementById("error-cause-stack"); + const errorNotDismissible = document.getElementById("error-not-dismissible"); + + // Set the title + errorTitle.innerText = title; + + // Display error message and body + const [heading, body] = error.message.split(/\n(.*)/s); + errorMessage.innerText = heading; + if (body) { + errorBody.innerText = body; + } else { + errorBody.innerText = ''; + } + + // Display component stack + errorComponentStack.innerText = componentStack; + + // Display the call stack + // Since we already displayed the message, strip it, and the first Error: line. + errorStack.innerText = error.stack.replace(error.message, '').split(/\n(.*)/s)[1]; + + // Display the cause, if available + if (error.cause) { + errorCauseMessage.innerText = error.cause.message; + errorCauseStack.innerText = error.cause.stack; + errorCause.classList.remove('hidden'); + } else { + errorCause.classList.add('hidden'); + } + // Display the close button, if dismissible + if (dismissable) { + errorNotDismissible.classList.add('hidden'); + errorClose.classList.remove("hidden"); + } else { + errorNotDismissible.classList.remove('hidden'); + errorClose.classList.add("hidden"); + } + + // Show the dialog + errorDialog.classList.remove("hidden"); +} + +export function reportCaughtError({error, cause, componentStack}) { + reportError({ title: "Caught Error", error, componentStack, dismissable: true}); +} + +export function reportUncaughtError({error, cause, componentStack}) { + reportError({ title: "Uncaught Error", error, componentStack, dismissable: false }); +} + +export function reportRecoverableError({error, cause, componentStack}) { + reportError({ title: "Recoverable Error", error, componentStack, dismissable: true }); +} +``` + +```js src/index.js active +import { createRoot } from "react-dom/client"; +import App from "./App.js"; +import {reportUncaughtError} from "./reportError"; +import "./styles.css"; + +const container = document.getElementById("root"); +const root = createRoot(container, { + onUncaughtError: (error, errorInfo) => { + if (error.message !== 'Known error') { + reportUncaughtError({ + error, + componentStack: errorInfo.componentStack + }); + } + } +}); +root.render(); +``` + +```js src/App.js +import { useState } from 'react'; + +export default function App() { + const [throwError, setThrowError] = useState(false); + + if (throwError) { + foo.bar = 'baz'; + } + + return ( +
+ This error shows the error dialog: + +
+ ); +} +``` + +```json package.json hidden +{ + "dependencies": { + "react": "canary", + "react-dom": "canary", + "react-scripts": "^5.0.0" + }, + "main": "/index.js" +} +``` + +
+ + +### Displaying Error Boundary errors {/*displaying-error-boundary-errors*/} + + + +`onCaughtError` is only available in the latest React Canary release. + + + +By default, React will log all errors caught by an Error Boundary to `console.error`. To override this behavior, you can provide the optional `onCaughtError` root option to handle errors caught by an [Error Boundary](/reference/react/Component#catching-rendering-errors-with-an-error-boundary): + +```js [[1, 6, "onCaughtError"], [2, 6, "error", 1], [3, 6, "errorInfo"], [4, 10, "componentStack"]] +import { createRoot } from 'react-dom/client'; + +const root = createRoot( + document.getElementById('root'), + { + onCaughtError: (error, errorInfo) => { + console.error( + 'Caught error', + error, + errorInfo.componentStack + ); + } + } +); +root.render(); +``` + +The onCaughtError option is a function called with two arguments: + +1. The error that was caught by the boundary. +2. An errorInfo object that contains the componentStack of the error. + +You can use the `onCaughtError` root option to display error dialogs or filter known errors from logging: + + + +```html index.html hidden + + + + My app + + + + + +
+ + +``` + +```css src/styles.css active +label, button { display: block; margin-bottom: 20px; } +html, body { min-height: 300px; } + +#error-dialog { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + background-color: white; + padding: 15px; + opacity: 0.9; + text-wrap: wrap; + overflow: scroll; +} + +.text-red { + color: red; +} + +.-mb-20 { + margin-bottom: -20px; +} + +.mb-0 { + margin-bottom: 0; +} + +.mb-10 { + margin-bottom: 10px; +} + +pre { + text-wrap: wrap; +} + +pre.nowrap { + text-wrap: nowrap; +} + +.hidden { + display: none; +} +``` + +```js src/reportError.js hidden +function reportError({ title, error, componentStack, dismissable }) { + const errorDialog = document.getElementById("error-dialog"); + const errorTitle = document.getElementById("error-title"); + const errorMessage = document.getElementById("error-message"); + const errorBody = document.getElementById("error-body"); + const errorComponentStack = document.getElementById("error-component-stack"); + const errorStack = document.getElementById("error-stack"); + const errorClose = document.getElementById("error-close"); + const errorCause = document.getElementById("error-cause"); + const errorCauseMessage = document.getElementById("error-cause-message"); + const errorCauseStack = document.getElementById("error-cause-stack"); + const errorNotDismissible = document.getElementById("error-not-dismissible"); + + // Set the title + errorTitle.innerText = title; + + // Display error message and body + const [heading, body] = error.message.split(/\n(.*)/s); + errorMessage.innerText = heading; + if (body) { + errorBody.innerText = body; + } else { + errorBody.innerText = ''; + } + + // Display component stack + errorComponentStack.innerText = componentStack; + + // Display the call stack + // Since we already displayed the message, strip it, and the first Error: line. + errorStack.innerText = error.stack.replace(error.message, '').split(/\n(.*)/s)[1]; + + // Display the cause, if available + if (error.cause) { + errorCauseMessage.innerText = error.cause.message; + errorCauseStack.innerText = error.cause.stack; + errorCause.classList.remove('hidden'); + } else { + errorCause.classList.add('hidden'); + } + // Display the close button, if dismissible + if (dismissable) { + errorNotDismissible.classList.add('hidden'); + errorClose.classList.remove("hidden"); + } else { + errorNotDismissible.classList.remove('hidden'); + errorClose.classList.add("hidden"); + } + + // Show the dialog + errorDialog.classList.remove("hidden"); +} + +export function reportCaughtError({error, cause, componentStack}) { + reportError({ title: "Caught Error", error, componentStack, dismissable: true}); +} + +export function reportUncaughtError({error, cause, componentStack}) { + reportError({ title: "Uncaught Error", error, componentStack, dismissable: false }); +} + +export function reportRecoverableError({error, cause, componentStack}) { + reportError({ title: "Recoverable Error", error, componentStack, dismissable: true }); +} +``` + +```js src/index.js active +import { createRoot } from "react-dom/client"; +import App from "./App.js"; +import {reportCaughtError} from "./reportError"; +import "./styles.css"; + +const container = document.getElementById("root"); +const root = createRoot(container, { + onCaughtError: (error, errorInfo) => { + if (error.message !== 'Known error') { + reportCaughtError({ + error, + componentStack: errorInfo.componentStack, + }); + } + } +}); +root.render(); +``` + +```js src/App.js +import { useState } from 'react'; +import { ErrorBoundary } from "react-error-boundary"; + +export default function App() { + const [error, setError] = useState(null); + + function handleUnknown() { + setError("unknown"); + } + + function handleKnown() { + setError("known"); + } + + return ( + <> + { + setError(null); + }} + > + {error != null && } + This error will not show the error dialog: + + This error will show the error dialog: + + + + + ); +} + +function fallbackRender({ resetErrorBoundary }) { + return ( +
+

Error Boundary

+

Something went wrong.

+ +
+ ); +} + +function Throw({error}) { + if (error === "known") { + throw new Error('Known error') + } else { + foo.bar = 'baz'; + } +} +``` + +```json package.json hidden +{ + "dependencies": { + "react": "canary", + "react-dom": "canary", + "react-scripts": "^5.0.0", + "react-error-boundary": "4.0.3" + }, + "main": "/index.js" +} +``` + +
+ +### Displaying a dialog for recoverable errors {/*displaying-a-dialog-for-recoverable-errors*/} + +React may automatically render a component a second time to attempt to recover from an error thrown in render. If successful, React will log a recoverable error to the console to notify the developer. To override this behavior, you can provide the optional `onRecoverableError` root option: + +```js [[1, 6, "onRecoverableError"], [2, 6, "error", 1], [3, 10, "error.cause"], [4, 6, "errorInfo"], [5, 11, "componentStack"]] +import { createRoot } from 'react-dom/client'; + +const root = createRoot( + document.getElementById('root'), + { + onRecoverableError: (error, errorInfo) => { + console.error( + 'Recoverable error', + error, + error.cause, + errorInfo.componentStack, + ); + } + } +); +root.render(); +``` + +The onRecoverableError option is a function called with two arguments: + +1. The error that React throws. Some errors may include the original cause as error.cause. +2. An errorInfo object that contains the componentStack of the error. + +You can use the `onRecoverableError` root option to display error dialogs: + + + +```html index.html hidden + + + + My app + + + + + +
+ + +``` + +```css src/styles.css active +label, button { display: block; margin-bottom: 20px; } +html, body { min-height: 300px; } + +#error-dialog { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + background-color: white; + padding: 15px; + opacity: 0.9; + text-wrap: wrap; + overflow: scroll; +} + +.text-red { + color: red; +} + +.-mb-20 { + margin-bottom: -20px; +} + +.mb-0 { + margin-bottom: 0; +} + +.mb-10 { + margin-bottom: 10px; +} + +pre { + text-wrap: wrap; +} + +pre.nowrap { + text-wrap: nowrap; +} + +.hidden { + display: none; +} +``` + +```js src/reportError.js hidden +function reportError({ title, error, componentStack, dismissable }) { + const errorDialog = document.getElementById("error-dialog"); + const errorTitle = document.getElementById("error-title"); + const errorMessage = document.getElementById("error-message"); + const errorBody = document.getElementById("error-body"); + const errorComponentStack = document.getElementById("error-component-stack"); + const errorStack = document.getElementById("error-stack"); + const errorClose = document.getElementById("error-close"); + const errorCause = document.getElementById("error-cause"); + const errorCauseMessage = document.getElementById("error-cause-message"); + const errorCauseStack = document.getElementById("error-cause-stack"); + const errorNotDismissible = document.getElementById("error-not-dismissible"); + + // Set the title + errorTitle.innerText = title; + + // Display error message and body + const [heading, body] = error.message.split(/\n(.*)/s); + errorMessage.innerText = heading; + if (body) { + errorBody.innerText = body; + } else { + errorBody.innerText = ''; + } + + // Display component stack + errorComponentStack.innerText = componentStack; + + // Display the call stack + // Since we already displayed the message, strip it, and the first Error: line. + errorStack.innerText = error.stack.replace(error.message, '').split(/\n(.*)/s)[1]; + + // Display the cause, if available + if (error.cause) { + errorCauseMessage.innerText = error.cause.message; + errorCauseStack.innerText = error.cause.stack; + errorCause.classList.remove('hidden'); + } else { + errorCause.classList.add('hidden'); + } + // Display the close button, if dismissible + if (dismissable) { + errorNotDismissible.classList.add('hidden'); + errorClose.classList.remove("hidden"); + } else { + errorNotDismissible.classList.remove('hidden'); + errorClose.classList.add("hidden"); + } + + // Show the dialog + errorDialog.classList.remove("hidden"); +} + +export function reportCaughtError({error, cause, componentStack}) { + reportError({ title: "Caught Error", error, componentStack, dismissable: true}); +} + +export function reportUncaughtError({error, cause, componentStack}) { + reportError({ title: "Uncaught Error", error, componentStack, dismissable: false }); +} + +export function reportRecoverableError({error, cause, componentStack}) { + reportError({ title: "Recoverable Error", error, componentStack, dismissable: true }); +} +``` + +```js src/index.js active +import { createRoot } from "react-dom/client"; +import App from "./App.js"; +import {reportRecoverableError} from "./reportError"; +import "./styles.css"; + +const container = document.getElementById("root"); +const root = createRoot(container, { + onRecoverableError: (error, errorInfo) => { + reportRecoverableError({ + error, + cause: error.cause, + componentStack: errorInfo.componentStack, + }); + } +}); +root.render(); +``` + +```js src/App.js +import { useState } from 'react'; +import { ErrorBoundary } from "react-error-boundary"; + +// 🚩 Bug: Never do this. This will force an error. +let errorThrown = false; +export default function App() { + return ( + <> + + {!errorThrown && } +

This component threw an error, but recovered during a second render.

+

Since it recovered, no Error Boundary was shown, but onRecoverableError was used to show an error dialog.

+
+ + + ); +} + +function fallbackRender() { + return ( +
+

Error Boundary

+

Something went wrong.

+
+ ); +} + +function Throw({error}) { + // Simulate an external value changing during concurrent render. + errorThrown = true; + foo.bar = 'baz'; +} +``` + +```json package.json hidden +{ + "dependencies": { + "react": "canary", + "react-dom": "canary", + "react-scripts": "^5.0.0", + "react-error-boundary": "4.0.3" + }, + "main": "/index.js" +} +``` + +
+ + --- ## Troubleshooting {/*troubleshooting*/} @@ -361,6 +1154,28 @@ Until you do that, nothing is displayed. --- +### I'm getting an error: "You passed a second argument to root.render" {/*im-getting-an-error-you-passed-a-second-argument-to-root-render*/} + +A common mistake is to pass the options for `createRoot` to `root.render(...)`: + + + +Warning: You passed a second argument to root.render(...) but it only accepts one argument. + + + +To fix, pass the root options to `createRoot(...)`, not `root.render(...)`: +```js {2,5} +// 🚩 Wrong: root.render only takes one argument. +root.render(App, {onUncaughtError}); + +// ✅ Correct: pass options to createRoot. +const root = createRoot(container, {onUncaughtError}); +root.render(); +``` + +--- + ### I'm getting an error: "Target container is not a DOM element" {/*im-getting-an-error-target-container-is-not-a-dom-element*/} This error means that whatever you're passing to `createRoot` is not a DOM node. diff --git a/src/content/reference/react-dom/client/hydrateRoot.md b/src/content/reference/react-dom/client/hydrateRoot.md index c137cdda9d5..cc30ce22c1f 100644 --- a/src/content/reference/react-dom/client/hydrateRoot.md +++ b/src/content/reference/react-dom/client/hydrateRoot.md @@ -41,7 +41,9 @@ React will attach to the HTML that exists inside the `domNode`, and take over ma * **optional** `options`: An object with options for this React root. - * **optional** `onRecoverableError`: Callback called when React automatically recovers from errors. + * **optional** `onCaughtError`: Callback called when React catches an error in an Error Boundary. Called with the `error` caught by the Error Boundary, and an `errorInfo` object containing the `componentStack`. + * **optional** `onUncaughtError`: Callback called when an error is thrown and not caught by an Error Boundary. Called with the `error` that was thrown and an `errorInfo` object containing the `componentStack`. + * **optional** `onRecoverableError`: Callback called when React automatically recovers from errors. Called with the `error` React throws, and an `errorInfo` object containing the `componentStack`. Some recoverable errors may include the original error cause as `error.cause`. * **optional** `identifierPrefix`: A string prefix React uses for IDs generated by [`useId`.](/reference/react/useId) Useful to avoid conflicts when using multiple roots on the same page. Must be the same prefix as used on the server. @@ -371,3 +373,826 @@ export default function App({counter}) { It is uncommon to call [`root.render`](#root-render) on a hydrated root. Usually, you'll [update state](/reference/react/useState) inside one of the components instead. + +### Show a dialog for uncaught errors {/*show-a-dialog-for-uncaught-errors*/} + + + +`onUncaughtError` is only available in the latest React Canary release. + + + +By default, React will log all uncaught errors to the console. To implement your own error reporting, you can provide the optional `onUncaughtError` root option: + +```js [[1, 7, "onUncaughtError"], [2, 7, "error", 1], [3, 7, "errorInfo"], [4, 11, "componentStack"]] +import { hydrateRoot } from 'react-dom/client'; + +const root = hydrateRoot( + document.getElementById('root'), + , + { + onUncaughtError: (error, errorInfo) => { + console.error( + 'Uncaught error', + error, + errorInfo.componentStack + ); + } + } +); +root.render(); +``` + +The onUncaughtError option is a function called with two arguments: + +1. The error that was thrown. +2. An errorInfo object that contains the componentStack of the error. + +You can use the `onUncaughtError` root option to display error dialogs: + + + +```html index.html hidden + + + + My app + + + + + +
This error shows the error dialog:
+ + +``` + +```css src/styles.css active +label, button { display: block; margin-bottom: 20px; } +html, body { min-height: 300px; } + +#error-dialog { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + background-color: white; + padding: 15px; + opacity: 0.9; + text-wrap: wrap; + overflow: scroll; +} + +.text-red { + color: red; +} + +.-mb-20 { + margin-bottom: -20px; +} + +.mb-0 { + margin-bottom: 0; +} + +.mb-10 { + margin-bottom: 10px; +} + +pre { + text-wrap: wrap; +} + +pre.nowrap { + text-wrap: nowrap; +} + +.hidden { + display: none; +} +``` + +```js src/reportError.js hidden +function reportError({ title, error, componentStack, dismissable }) { + const errorDialog = document.getElementById("error-dialog"); + const errorTitle = document.getElementById("error-title"); + const errorMessage = document.getElementById("error-message"); + const errorBody = document.getElementById("error-body"); + const errorComponentStack = document.getElementById("error-component-stack"); + const errorStack = document.getElementById("error-stack"); + const errorClose = document.getElementById("error-close"); + const errorCause = document.getElementById("error-cause"); + const errorCauseMessage = document.getElementById("error-cause-message"); + const errorCauseStack = document.getElementById("error-cause-stack"); + const errorNotDismissible = document.getElementById("error-not-dismissible"); + + // Set the title + errorTitle.innerText = title; + + // Display error message and body + const [heading, body] = error.message.split(/\n(.*)/s); + errorMessage.innerText = heading; + if (body) { + errorBody.innerText = body; + } else { + errorBody.innerText = ''; + } + + // Display component stack + errorComponentStack.innerText = componentStack; + + // Display the call stack + // Since we already displayed the message, strip it, and the first Error: line. + errorStack.innerText = error.stack.replace(error.message, '').split(/\n(.*)/s)[1]; + + // Display the cause, if available + if (error.cause) { + errorCauseMessage.innerText = error.cause.message; + errorCauseStack.innerText = error.cause.stack; + errorCause.classList.remove('hidden'); + } else { + errorCause.classList.add('hidden'); + } + // Display the close button, if dismissible + if (dismissable) { + errorNotDismissible.classList.add('hidden'); + errorClose.classList.remove("hidden"); + } else { + errorNotDismissible.classList.remove('hidden'); + errorClose.classList.add("hidden"); + } + + // Show the dialog + errorDialog.classList.remove("hidden"); +} + +export function reportCaughtError({error, cause, componentStack}) { + reportError({ title: "Caught Error", error, componentStack, dismissable: true}); +} + +export function reportUncaughtError({error, cause, componentStack}) { + reportError({ title: "Uncaught Error", error, componentStack, dismissable: false }); +} + +export function reportRecoverableError({error, cause, componentStack}) { + reportError({ title: "Recoverable Error", error, componentStack, dismissable: true }); +} +``` + +```js src/index.js active +import { hydrateRoot } from "react-dom/client"; +import App from "./App.js"; +import {reportUncaughtError} from "./reportError"; +import "./styles.css"; +import {renderToString} from 'react-dom/server'; + +const container = document.getElementById("root"); +const root = hydrateRoot(container, , { + onUncaughtError: (error, errorInfo) => { + if (error.message !== 'Known error') { + reportUncaughtError({ + error, + componentStack: errorInfo.componentStack + }); + } + } +}); +``` + +```js src/App.js +import { useState } from 'react'; + +export default function App() { + const [throwError, setThrowError] = useState(false); + + if (throwError) { + foo.bar = 'baz'; + } + + return ( +
+ This error shows the error dialog: + +
+ ); +} +``` + +```json package.json hidden +{ + "dependencies": { + "react": "canary", + "react-dom": "canary", + "react-scripts": "^5.0.0" + }, + "main": "/index.js" +} +``` + +
+ + +### Displaying Error Boundary errors {/*displaying-error-boundary-errors*/} + + + +`onCaughtError` is only available in the latest React Canary release. + + + +By default, React will log all errors caught by an Error Boundary to `console.error`. To override this behavior, you can provide the optional `onCaughtError` root option for errors caught by an [Error Boundary](/reference/react/Component#catching-rendering-errors-with-an-error-boundary): + +```js [[1, 7, "onCaughtError"], [2, 7, "error", 1], [3, 7, "errorInfo"], [4, 11, "componentStack"]] +import { hydrateRoot } from 'react-dom/client'; + +const root = hydrateRoot( + document.getElementById('root'), + , + { + onCaughtError: (error, errorInfo) => { + console.error( + 'Caught error', + error, + errorInfo.componentStack + ); + } + } +); +root.render(); +``` + +The onCaughtError option is a function called with two arguments: + +1. The error that was caught by the boundary. +2. An errorInfo object that contains the componentStack of the error. + +You can use the `onCaughtError` root option to display error dialogs or filter known errors from logging: + + + +```html index.html hidden + + + + My app + + + + + +
This error will not show the error dialog:This error will show the error dialog:
+ + +``` + +```css src/styles.css active +label, button { display: block; margin-bottom: 20px; } +html, body { min-height: 300px; } + +#error-dialog { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + background-color: white; + padding: 15px; + opacity: 0.9; + text-wrap: wrap; + overflow: scroll; +} + +.text-red { + color: red; +} + +.-mb-20 { + margin-bottom: -20px; +} + +.mb-0 { + margin-bottom: 0; +} + +.mb-10 { + margin-bottom: 10px; +} + +pre { + text-wrap: wrap; +} + +pre.nowrap { + text-wrap: nowrap; +} + +.hidden { + display: none; +} +``` + +```js src/reportError.js hidden +function reportError({ title, error, componentStack, dismissable }) { + const errorDialog = document.getElementById("error-dialog"); + const errorTitle = document.getElementById("error-title"); + const errorMessage = document.getElementById("error-message"); + const errorBody = document.getElementById("error-body"); + const errorComponentStack = document.getElementById("error-component-stack"); + const errorStack = document.getElementById("error-stack"); + const errorClose = document.getElementById("error-close"); + const errorCause = document.getElementById("error-cause"); + const errorCauseMessage = document.getElementById("error-cause-message"); + const errorCauseStack = document.getElementById("error-cause-stack"); + const errorNotDismissible = document.getElementById("error-not-dismissible"); + + // Set the title + errorTitle.innerText = title; + + // Display error message and body + const [heading, body] = error.message.split(/\n(.*)/s); + errorMessage.innerText = heading; + if (body) { + errorBody.innerText = body; + } else { + errorBody.innerText = ''; + } + + // Display component stack + errorComponentStack.innerText = componentStack; + + // Display the call stack + // Since we already displayed the message, strip it, and the first Error: line. + errorStack.innerText = error.stack.replace(error.message, '').split(/\n(.*)/s)[1]; + + // Display the cause, if available + if (error.cause) { + errorCauseMessage.innerText = error.cause.message; + errorCauseStack.innerText = error.cause.stack; + errorCause.classList.remove('hidden'); + } else { + errorCause.classList.add('hidden'); + } + // Display the close button, if dismissible + if (dismissable) { + errorNotDismissible.classList.add('hidden'); + errorClose.classList.remove("hidden"); + } else { + errorNotDismissible.classList.remove('hidden'); + errorClose.classList.add("hidden"); + } + + // Show the dialog + errorDialog.classList.remove("hidden"); +} + +export function reportCaughtError({error, cause, componentStack}) { + reportError({ title: "Caught Error", error, componentStack, dismissable: true}); +} + +export function reportUncaughtError({error, cause, componentStack}) { + reportError({ title: "Uncaught Error", error, componentStack, dismissable: false }); +} + +export function reportRecoverableError({error, cause, componentStack}) { + reportError({ title: "Recoverable Error", error, componentStack, dismissable: true }); +} +``` + +```js src/index.js active +import { hydrateRoot } from "react-dom/client"; +import App from "./App.js"; +import {reportCaughtError} from "./reportError"; +import "./styles.css"; + +const container = document.getElementById("root"); +const root = hydrateRoot(container, , { + onCaughtError: (error, errorInfo) => { + if (error.message !== 'Known error') { + reportCaughtError({ + error, + componentStack: errorInfo.componentStack + }); + } + } +}); +``` + +```js src/App.js +import { useState } from 'react'; +import { ErrorBoundary } from "react-error-boundary"; + +export default function App() { + const [error, setError] = useState(null); + + function handleUnknown() { + setError("unknown"); + } + + function handleKnown() { + setError("known"); + } + + return ( + <> + { + setError(null); + }} + > + {error != null && } + This error will not show the error dialog: + + This error will show the error dialog: + + + + + ); +} + +function fallbackRender({ resetErrorBoundary }) { + return ( +
+

Error Boundary

+

Something went wrong.

+ +
+ ); +} + +function Throw({error}) { + if (error === "known") { + throw new Error('Known error') + } else { + foo.bar = 'baz'; + } +} +``` + +```json package.json hidden +{ + "dependencies": { + "react": "canary", + "react-dom": "canary", + "react-scripts": "^5.0.0", + "react-error-boundary": "4.0.3" + }, + "main": "/index.js" +} +``` + +
+ +### Show a dialog for recoverable hydration mismatch errors {/*show-a-dialog-for-recoverable-hydration-mismatch-errors*/} + +When React encounters a hydration mismatch, it will automatically attempt to recover by rendering on the client. By default, React will log hydration mismatch errors to `console.error`. To override this behavior, you can provide the optional `onRecoverableError` root option: + +```js [[1, 7, "onRecoverableError"], [2, 7, "error", 1], [3, 11, "error.cause", 1], [4, 7, "errorInfo"], [5, 12, "componentStack"]] +import { hydrateRoot } from 'react-dom/client'; + +const root = hydrateRoot( + document.getElementById('root'), + , + { + onRecoverableError: (error, errorInfo) => { + console.error( + 'Caught error', + error, + error.cause, + errorInfo.componentStack + ); + } + } +); +``` + +The onRecoverableError option is a function called with two arguments: + +1. The error React throws. Some errors may include the original cause as error.cause. +2. An errorInfo object that contains the componentStack of the error. + +You can use the `onRecoverableError` root option to display error dialogs for hydration mismatches: + + + +```html index.html hidden + + + + My app + + + + + +
Server
+ + +``` + +```css src/styles.css active +label, button { display: block; margin-bottom: 20px; } +html, body { min-height: 300px; } + +#error-dialog { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + background-color: white; + padding: 15px; + opacity: 0.9; + text-wrap: wrap; + overflow: scroll; +} + +.text-red { + color: red; +} + +.-mb-20 { + margin-bottom: -20px; +} + +.mb-0 { + margin-bottom: 0; +} + +.mb-10 { + margin-bottom: 10px; +} + +pre { + text-wrap: wrap; +} + +pre.nowrap { + text-wrap: nowrap; +} + +.hidden { + display: none; +} +``` + +```js src/reportError.js hidden +function reportError({ title, error, componentStack, dismissable }) { + const errorDialog = document.getElementById("error-dialog"); + const errorTitle = document.getElementById("error-title"); + const errorMessage = document.getElementById("error-message"); + const errorBody = document.getElementById("error-body"); + const errorComponentStack = document.getElementById("error-component-stack"); + const errorStack = document.getElementById("error-stack"); + const errorClose = document.getElementById("error-close"); + const errorCause = document.getElementById("error-cause"); + const errorCauseMessage = document.getElementById("error-cause-message"); + const errorCauseStack = document.getElementById("error-cause-stack"); + const errorNotDismissible = document.getElementById("error-not-dismissible"); + + // Set the title + errorTitle.innerText = title; + + // Display error message and body + const [heading, body] = error.message.split(/\n(.*)/s); + errorMessage.innerText = heading; + if (body) { + errorBody.innerText = body; + } else { + errorBody.innerText = ''; + } + + // Display component stack + errorComponentStack.innerText = componentStack; + + // Display the call stack + // Since we already displayed the message, strip it, and the first Error: line. + errorStack.innerText = error.stack.replace(error.message, '').split(/\n(.*)/s)[1]; + + // Display the cause, if available + if (error.cause) { + errorCauseMessage.innerText = error.cause.message; + errorCauseStack.innerText = error.cause.stack; + errorCause.classList.remove('hidden'); + } else { + errorCause.classList.add('hidden'); + } + // Display the close button, if dismissible + if (dismissable) { + errorNotDismissible.classList.add('hidden'); + errorClose.classList.remove("hidden"); + } else { + errorNotDismissible.classList.remove('hidden'); + errorClose.classList.add("hidden"); + } + + // Show the dialog + errorDialog.classList.remove("hidden"); +} + +export function reportCaughtError({error, cause, componentStack}) { + reportError({ title: "Caught Error", error, componentStack, dismissable: true}); +} + +export function reportUncaughtError({error, cause, componentStack}) { + reportError({ title: "Uncaught Error", error, componentStack, dismissable: false }); +} + +export function reportRecoverableError({error, cause, componentStack}) { + reportError({ title: "Recoverable Error", error, componentStack, dismissable: true }); +} +``` + +```js src/index.js active +import { hydrateRoot } from "react-dom/client"; +import App from "./App.js"; +import {reportRecoverableError} from "./reportError"; +import "./styles.css"; + +const container = document.getElementById("root"); +const root = hydrateRoot(container, , { + onRecoverableError: (error, errorInfo) => { + reportRecoverableError({ + error, + cause: error.cause, + componentStack: errorInfo.componentStack + }); + } +}); +``` + +```js src/App.js +import { useState } from 'react'; +import { ErrorBoundary } from "react-error-boundary"; + +export default function App() { + const [error, setError] = useState(null); + + function handleUnknown() { + setError("unknown"); + } + + function handleKnown() { + setError("known"); + } + + return ( + {typeof window !== 'undefined' ? 'Client' : 'Server'} + ); +} + +function fallbackRender({ resetErrorBoundary }) { + return ( +
+

Error Boundary

+

Something went wrong.

+ +
+ ); +} + +function Throw({error}) { + if (error === "known") { + throw new Error('Known error') + } else { + foo.bar = 'baz'; + } +} +``` + +```json package.json hidden +{ + "dependencies": { + "react": "canary", + "react-dom": "canary", + "react-scripts": "^5.0.0", + "react-error-boundary": "4.0.3" + }, + "main": "/index.js" +} +``` + +
+ +## Troubleshooting {/*troubleshooting*/} + + +### I'm getting an error: "You passed a second argument to root.render" {/*im-getting-an-error-you-passed-a-second-argument-to-root-render*/} + +A common mistake is to pass the options for `hydrateRoot` to `root.render(...)`: + + + +Warning: You passed a second argument to root.render(...) but it only accepts one argument. + + + +To fix, pass the root options to `hydrateRoot(...)`, not `root.render(...)`: +```js {2,5} +// 🚩 Wrong: root.render only takes one argument. +root.render(App, {onUncaughtError}); + +// ✅ Correct: pass options to createRoot. +const root = hydrateRoot(container, , {onUncaughtError}); +``` From a6de255cfb832ed97e1743ebb14c5f26760cf207 Mon Sep 17 00:00:00 2001 From: Rick Hanlon Date: Thu, 11 Apr 2024 09:54:44 -0400 Subject: [PATCH 3/3] Add canary info, simplify a bit --- src/content/reference/react/useDeferredValue.md | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/content/reference/react/useDeferredValue.md b/src/content/reference/react/useDeferredValue.md index fa8576bbbd2..6d055f1f984 100644 --- a/src/content/reference/react/useDeferredValue.md +++ b/src/content/reference/react/useDeferredValue.md @@ -37,22 +37,23 @@ function SearchPage() { #### Parameters {/*parameters*/} * `value`: The value you want to defer. It can have any type. -* **optional** `initialValue`: A value to use during the initial render of a component. If this option is omitted, `useDeferredValue` will not defer during the initial render, because there's no previous version of `value` that it can render instead. +* **optional** `initialValue`: A value to use during the initial render of a component. If this option is omitted, `useDeferredValue` will not defer during the initial render, because there's no previous version of `value` that it can render instead. #### Returns {/*returns*/} -Returns either `value`, the old `value` that was previously rendered to the screen, or `initialValue`, depending on the scenario: +- `currentValue`: During the initial render, the returned deferred value will be the same as the value you provided. During updates, React will first attempt a re-render with the old value (so it will return the old value), and then try another re-render in the background with the new value (so it will return the updated value). -- During the initial render... - - If `initialValue` _is_ provided, it first returns `initialValue`, then spawns a deferred render to switch to `value`. - - If `initialValue` _is not_ provided, it returns `value`, and does not spawn a deferred render. -- During an update... - - If the update _is_ the result of a Transition, it returns the new `value`, and does not spawn a deferred render. - - If the update _is not_ the result of a Transition, it first returns the old `value`, then spawns a deferred render to switch to the new `value`. + + +In the latest React Canary versions, `useDeferredValue` returns the `initialValue` on initial render, and schedules a re-render in the background with the `value` returned. + + #### Caveats {/*caveats*/} +- When an update is inside a Transition, `useDeferredValue` always returns the new `value` and does not spawn a deferred render, since the update is already deferred. + - The values you pass to `useDeferredValue` should either be primitive values (like strings and numbers) or objects created outside of rendering. If you create a new object during rendering and immediately pass it to `useDeferredValue`, it will be different on every render, causing unnecessary background re-renders. - When `useDeferredValue` receives a different value (compared with [`Object.is`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is)), in addition to the current render (when it still uses the previous value), it schedules a re-render in the background with the new value. The background re-render is interruptible: if there's another update to the `value`, React will restart the background re-render from scratch. For example, if the user is typing into an input faster than a chart receiving its deferred value can re-render, the chart will only re-render after the user stops typing.