-
Notifications
You must be signed in to change notification settings - Fork 7.9k
Document browser-only rendering #8582
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
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,293 @@ | ||
| --- | ||
| title: browser | ||
| version: canary | ||
| --- | ||
|
|
||
| <Intro> | ||
|
|
||
| <Canary> | ||
|
|
||
| **The `browser` API is currently only available in React’s Canary and Experimental channels.** | ||
|
|
||
| [Learn more about React’s release channels here.](/community/versioning-policy#all-release-channels) | ||
|
|
||
| </Canary> | ||
|
|
||
| `browser` lets you mark a component as browser-only during server rendering. | ||
|
|
||
| ```js | ||
| use(browser(reason?)) | ||
| ``` | ||
|
|
||
| </Intro> | ||
|
|
||
| <InlineToc /> | ||
|
|
||
| --- | ||
|
|
||
| ## Reference {/*reference*/} | ||
|
|
||
| ### `browser(reason?)` {/*browser*/} | ||
|
|
||
| Call `browser` inside [`use`](/reference/react/use) to mark a component as browser-only during server rendering: | ||
|
|
||
| ```js | ||
| import { use } from 'react'; | ||
| import { browser } from 'react-dom'; | ||
|
|
||
| function BrowserOnly() { | ||
| use(browser('This component requires browser APIs.')); | ||
| return <BrowserContent />; | ||
| } | ||
| ``` | ||
|
|
||
| During server rendering, `use(browser())` stops rendering the component and leaves the closest [`<Suspense>`](/reference/react/Suspense) boundary's fallback in its place. In the browser, `use(browser())` returns `undefined`, so the component renders normally. | ||
|
|
||
| [See more examples below.](#usage) | ||
|
|
||
| #### Parameters {/*parameters*/} | ||
|
|
||
| * **optional** `reason`: A string or function that explains why the content needs to render in the browser. The string or the function's return value becomes the `cause` of the `Error` passed to [`onBrowserBailout`](#reporting-browser-only-rendering-on-the-server). React calls a reason function each time a server renderer encounters the value returned by `browser`, but does not call it in the browser. If creating the reason is expensive, pass a function such as `() => new Error(...)`. | ||
|
|
||
| #### Returns {/*returns*/} | ||
|
|
||
| `browser` returns a value that you can pass to `use` in a component or use as the reason when [aborting a server render](#aborting-pending-server-rendering-for-the-browser). In the browser, passing this value to `use` returns `undefined`. | ||
|
|
||
| #### Caveats {/*caveats*/} | ||
|
|
||
| * `use(browser())` must be inside a `<Suspense>` boundary during server rendering. Without one, the server render fails. | ||
| * In a React Server Components app, `use(browser())` must be called from a [Client Component](/reference/rsc/use-client), not a [Server Component](/reference/rsc/server-components). | ||
| * Calling `browser()` by itself has no effect. To mark a component as browser-only, pass the value returned by `browser` to `use`. Do not throw it. | ||
|
|
||
| --- | ||
|
|
||
| ## Usage {/*usage*/} | ||
|
|
||
| ### Rendering content only in the browser {/*rendering-content-only-in-the-browser*/} | ||
|
|
||
| Call `browser` inside `use` in a component that should only render in the browser: | ||
|
|
||
| You can use this instead of checking `typeof window`, waiting for an [`Effect`](/reference/react/useEffect) to set mounted state, or using a framework option to disable server rendering. | ||
|
|
||
| Press **Render the page**. The loading fallback appears first. After a short delay, React hydrates the page and displays the browser-only editor. | ||
|
|
||
| <Sandpack> | ||
|
|
||
| ```js src/App.js active | ||
| import { Suspense, use } from 'react'; | ||
| import { browser } from 'react-dom'; | ||
|
|
||
| function BrowserOnlyEditor() { | ||
| use(browser('The editor requires browser APIs.')); | ||
| return <label>Draft: <input /></label>; | ||
| } | ||
|
|
||
| export default function App() { | ||
| return ( | ||
| <Suspense fallback={<p>Loading editor...</p>}> | ||
| <BrowserOnlyEditor /> | ||
| </Suspense> | ||
| ); | ||
| } | ||
| ``` | ||
|
|
||
| ```js src/Document.js hidden | ||
| import App from './App.js'; | ||
|
|
||
| export default function Document() { | ||
| return ( | ||
| <html lang="en"> | ||
| <head> | ||
| <title>Article editor</title> | ||
| </head> | ||
| <body> | ||
| <h1>Article editor</h1> | ||
| <App /> | ||
| </body> | ||
| </html> | ||
| ); | ||
| } | ||
| ``` | ||
|
|
||
| ```js src/index.js | ||
| import { hydrateRoot } from 'react-dom/client'; | ||
| import { renderToReadableStream } from 'react-dom/server'; | ||
| import Document from './Document.js'; | ||
| import { flushReadableStreamToFrame } from './demo-helpers.js'; | ||
| import './styles.css'; | ||
|
|
||
| async function main(frame) { | ||
| const stream = await renderToReadableStream(<Document />); | ||
| await flushReadableStreamToFrame(stream, frame); | ||
|
|
||
| // Wait so both the fallback and hydrated content are visible. | ||
| await new Promise(resolve => setTimeout(resolve, 1200)); | ||
| hydrateRoot(frame.contentDocument, <Document />); | ||
| } | ||
|
|
||
| const renderButton = document.getElementById('render'); | ||
| renderButton.addEventListener('click', () => { | ||
| renderButton.disabled = true; | ||
| main(document.getElementById('preview')); | ||
| }, { once: true }); | ||
| ``` | ||
|
|
||
| ```js src/demo-helpers.js hidden | ||
| export async function flushReadableStreamToFrame(readable, frame) { | ||
| const doc = frame.contentWindow.document; | ||
| const decoder = new TextDecoder(); | ||
| for await (const chunk of readable) { | ||
| doc.write(decoder.decode(chunk, { stream: true })); | ||
| } | ||
| doc.close(); | ||
| } | ||
| ``` | ||
|
|
||
| ```html public/index.html | ||
| <!DOCTYPE html> | ||
| <html lang="en"> | ||
| <head> | ||
| <meta charset="UTF-8" /> | ||
| <title>Browser-only rendering</title> | ||
| </head> | ||
| <body> | ||
| <button id="render">Render the page</button> | ||
| <br /><br /> | ||
| <iframe id="preview" title="Rendered page"></iframe> | ||
| </body> | ||
| </html> | ||
| ``` | ||
|
|
||
| ```css src/styles.css hidden | ||
| iframe { | ||
| width: 100%; | ||
| height: 180px; | ||
| border: 1px solid #aaa; | ||
| } | ||
| ``` | ||
|
|
||
| ```json package.json hidden | ||
| { | ||
| "dependencies": { | ||
| "react": "canary", | ||
| "react-dom": "canary", | ||
| "react-scripts": "latest" | ||
| }, | ||
| "scripts": { | ||
| "start": "react-scripts start", | ||
| "build": "react-scripts build", | ||
| "test": "react-scripts test --env=jsdom", | ||
| "eject": "react-scripts eject" | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| </Sandpack> | ||
|
|
||
| <Note> | ||
|
|
||
| In a React Server Components app, `use(browser())` must be called from a Client Component. If your framework uses Server Components by default, add the [`'use client'`](/reference/rsc/use-client) directive to that file or move the call to a child Client Component: | ||
|
|
||
| ```js {1} | ||
| 'use client'; | ||
|
|
||
| import { use } from 'react'; | ||
| import { browser } from 'react-dom'; | ||
|
|
||
| export default function BrowserOnlyEditor() { | ||
| use(browser('The editor requires browser APIs.')); | ||
| return <Editor />; | ||
| } | ||
| ``` | ||
|
|
||
| </Note> | ||
|
|
||
| --- | ||
|
|
||
| ### Conditionally rendering in the browser {/*conditionally-rendering-in-the-browser*/} | ||
|
|
||
| Like other calls to [`use`](/reference/react/use), you can call `use(browser())` conditionally or inside a custom Hook. For example, you can wrap a Suspense-enabled data-fetching library's `useQuery` and skip server rendering when initial data is missing: | ||
|
|
||
| ```js {3} | ||
| function useBrowserQuery(query, options) { | ||
| if (options.initialData === undefined) { | ||
| use(browser('useBrowserQuery: No initial data was provided.')); | ||
| } | ||
|
|
||
| return useQuery(query, options); | ||
| } | ||
|
|
||
| function ProductDetails({ productId, initialData }) { | ||
| const product = useBrowserQuery(`/api/products/${productId}`, { | ||
| initialData, | ||
| }); | ||
|
|
||
| return <h1>{product.name}</h1>; | ||
| } | ||
| ``` | ||
|
|
||
| On the server, `useBrowserQuery` calls `useQuery` only when `initialData` is available. Otherwise, the closest Suspense boundary's fallback remains in the HTML. In the browser, `use(browser())` returns `undefined`, so the query library can fetch the data or read it from its client cache. | ||
|
|
||
| --- | ||
|
|
||
| ### Reporting browser-only rendering on the server {/*reporting-browser-only-rendering-on-the-server*/} | ||
|
|
||
| Pass an `onBrowserBailout` callback to the server renderer to report browser-only rendering. When React leaves a Suspense fallback for the browser, it does not call the server renderer's `onError` callback or [`hydrateRoot`'s `onRecoverableError`](/reference/react-dom/client/hydrateRoot#error-logging-in-production) callback. This example also passes a reason, which is available as the reported error's `cause`: | ||
|
|
||
| ```js | ||
| import { Suspense, use } from 'react'; | ||
| import { browser } from 'react-dom'; | ||
| import { renderToPipeableStream } from 'react-dom/server'; | ||
|
|
||
| function BrowserOnlyEditor() { | ||
| use(browser(() => new Error('The editor requires a browser API.'))); | ||
| return <Editor />; | ||
| } | ||
|
|
||
| const { pipe } = renderToPipeableStream( | ||
| <Suspense fallback={<p>Loading editor...</p>}> | ||
| <BrowserOnlyEditor /> | ||
| </Suspense>, | ||
| { | ||
| onShellReady() { | ||
| pipe(response); | ||
| }, | ||
| onBrowserBailout(error, errorInfo) { | ||
| logBrowserBailout(error, errorInfo); | ||
| } | ||
| } | ||
| ); | ||
| ``` | ||
|
|
||
| `onBrowserBailout` receives two arguments: | ||
|
|
||
| 1. An `Error` describing the browser-only render. If you passed a reason to `browser`, it is available as the error's `cause`. | ||
| 2. An `errorInfo` object with a `componentStack` showing where browser-only rendering occurred. | ||
|
|
||
| The reason function can return any value. Return a new `Error` to give the cause its own stack without creating the `Error` in the browser. React does not serialize the reason into the HTML. | ||
|
|
||
| If there is no Suspense boundary to provide a fallback, the server render fails. React reports the failure through the renderer's usual error callbacks instead of `onBrowserBailout`. | ||
|
|
||
| --- | ||
|
|
||
| ### Aborting pending server rendering for the browser {/*aborting-pending-server-rendering-for-the-browser*/} | ||
|
|
||
| If you call a server rendering API directly, you can stop waiting for pending content and let the browser finish rendering it. Pass the value returned by `browser` as the reason when aborting the server render. React then leaves pending Suspense boundaries in their fallback state and renders their content in the browser: | ||
|
|
||
| ```js {1,8} | ||
| import { browser } from 'react-dom'; | ||
| import { renderToPipeableStream } from 'react-dom/server'; | ||
|
|
||
| const { pipe, abort } = renderToPipeableStream(<App />, { | ||
| onShellReady() { | ||
| pipe(response); | ||
| setTimeout(() => { | ||
| abort(browser('The server render timed out.')); | ||
| }, 10000); | ||
| } | ||
| }); | ||
| ``` | ||
|
|
||
| A `browser` abort reason does not trigger the server renderer's `onError` callback or `hydrateRoot`'s `onRecoverableError` callback. Instead, the server renderer reports each recovered Suspense boundary to `onBrowserBailout`. | ||
|
|
||
| For server rendering APIs that accept an [`AbortSignal`](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal), pass `browser()` as the reason to [`AbortController.abort`](https://developer.mozilla.org/en-US/docs/Web/API/AbortController/abort). | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this example need a "use client" here?
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
react-domis a "client" library in the sense that it is for browser (react-dom/client) and render-for-browser-as-html (react-dom/server). You can use this in frameworks that don't even support RSC so I don't think the examples should imply that this is in any associated with RSC even if it is true that in an RSC framework you can't use this API in Server Components.It's also not the case that you need "use client" on every file that is part of the client bundle, you only need this in files that you want to import into the server as references
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is my main concern:
use(browser())must be a client component; this page does not explicitly call it out, although it is implied.When working with a traditional client-focused framework (like Next.js Pages Router, TanStack Start),
use(browser())works great.There is a different story for server component frameworks (like Next.js App Router and Waku), where components default to being server components and do not execute code in the browser, so components that have
use(browser())and no other hooks will be treated as server components by default. Thus, they will not render and will error out unless they are client components.use(browser())does not force a client component, like having a hook in it. While"use client"is not needed in every file, I would argue that if you have code that must be in the browser, it should have a"use client"so developers know that code is going into the client and don't have hidden dependencies that could break people's apps.If this example does not use "use client", there should be some explicit callout of how to use
use(browser())in server component frameworks.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added a note: 385d306