|
| 1 | +--- |
| 2 | +title: browser |
| 3 | +version: canary |
| 4 | +--- |
| 5 | + |
| 6 | +<Intro> |
| 7 | + |
| 8 | +<Canary> |
| 9 | + |
| 10 | +**The `browser` API is currently only available in React’s Canary and Experimental channels.** |
| 11 | + |
| 12 | +[Learn more about React’s release channels here.](/community/versioning-policy#all-release-channels) |
| 13 | + |
| 14 | +</Canary> |
| 15 | + |
| 16 | +`browser` lets you mark a component as browser-only during server rendering. |
| 17 | + |
| 18 | +```js |
| 19 | +use(browser(reason?)) |
| 20 | +``` |
| 21 | +
|
| 22 | +</Intro> |
| 23 | +
|
| 24 | +<InlineToc /> |
| 25 | +
|
| 26 | +--- |
| 27 | +
|
| 28 | +## Reference {/*reference*/} |
| 29 | +
|
| 30 | +### `browser(reason?)` {/*browser*/} |
| 31 | +
|
| 32 | +Call `browser` inside [`use`](/reference/react/use) to mark a component as browser-only during server rendering: |
| 33 | +
|
| 34 | +```js |
| 35 | +import { use } from 'react'; |
| 36 | +import { browser } from 'react-dom'; |
| 37 | + |
| 38 | +function BrowserOnly() { |
| 39 | + use(browser('This component requires browser APIs.')); |
| 40 | + return <BrowserContent />; |
| 41 | +} |
| 42 | +``` |
| 43 | +
|
| 44 | +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. |
| 45 | +
|
| 46 | +[See more examples below.](#usage) |
| 47 | +
|
| 48 | +#### Parameters {/*parameters*/} |
| 49 | +
|
| 50 | +* **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(...)`. |
| 51 | +
|
| 52 | +#### Returns {/*returns*/} |
| 53 | +
|
| 54 | +`browser` returns an opaque 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`. |
| 55 | +
|
| 56 | +#### Caveats {/*caveats*/} |
| 57 | +
|
| 58 | +* `use(browser())` must be inside a `<Suspense>` boundary during server rendering. Without one, the server render fails. |
| 59 | +* 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). |
| 60 | +* 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. |
| 61 | +
|
| 62 | +--- |
| 63 | +
|
| 64 | +## Usage {/*usage*/} |
| 65 | +
|
| 66 | +### Rendering content only in the browser {/*rendering-content-only-in-the-browser*/} |
| 67 | +
|
| 68 | +Call `browser` inside `use` in a component that should only render in the browser: |
| 69 | +
|
| 70 | +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. |
| 71 | +
|
| 72 | +Press **Render the page**. The loading fallback appears first. After a short delay, React hydrates the page and displays the browser-only editor. |
| 73 | +
|
| 74 | +<Sandpack> |
| 75 | +
|
| 76 | +```js src/App.js active |
| 77 | +import { Suspense, use } from 'react'; |
| 78 | +import { browser } from 'react-dom'; |
| 79 | + |
| 80 | +function BrowserOnlyEditor() { |
| 81 | + use(browser('The editor requires browser APIs.')); |
| 82 | + return <label>Draft: <input /></label>; |
| 83 | +} |
| 84 | + |
| 85 | +export default function App() { |
| 86 | + return ( |
| 87 | + <Suspense fallback={<p>Loading editor...</p>}> |
| 88 | + <BrowserOnlyEditor /> |
| 89 | + </Suspense> |
| 90 | + ); |
| 91 | +} |
| 92 | +``` |
| 93 | +
|
| 94 | +```js src/Document.js hidden |
| 95 | +import App from './App.js'; |
| 96 | + |
| 97 | +export default function Document() { |
| 98 | + return ( |
| 99 | + <html lang="en"> |
| 100 | + <head> |
| 101 | + <title>Article editor</title> |
| 102 | + </head> |
| 103 | + <body> |
| 104 | + <h1>Article editor</h1> |
| 105 | + <App /> |
| 106 | + </body> |
| 107 | + </html> |
| 108 | + ); |
| 109 | +} |
| 110 | +``` |
| 111 | +
|
| 112 | +```js src/index.js |
| 113 | +import { hydrateRoot } from 'react-dom/client'; |
| 114 | +import { renderToReadableStream } from 'react-dom/server'; |
| 115 | +import Document from './Document.js'; |
| 116 | +import { flushReadableStreamToFrame } from './demo-helpers.js'; |
| 117 | +import './styles.css'; |
| 118 | + |
| 119 | +async function main(frame) { |
| 120 | + const stream = await renderToReadableStream(<Document />); |
| 121 | + await flushReadableStreamToFrame(stream, frame); |
| 122 | + |
| 123 | + // Wait so both the fallback and hydrated content are visible. |
| 124 | + await new Promise(resolve => setTimeout(resolve, 1200)); |
| 125 | + hydrateRoot(frame.contentDocument, <Document />); |
| 126 | +} |
| 127 | + |
| 128 | +const renderButton = document.getElementById('render'); |
| 129 | +renderButton.addEventListener('click', () => { |
| 130 | + renderButton.disabled = true; |
| 131 | + main(document.getElementById('preview')); |
| 132 | +}, { once: true }); |
| 133 | +``` |
| 134 | +
|
| 135 | +```js src/demo-helpers.js hidden |
| 136 | +export async function flushReadableStreamToFrame(readable, frame) { |
| 137 | + const doc = frame.contentWindow.document; |
| 138 | + const decoder = new TextDecoder(); |
| 139 | + for await (const chunk of readable) { |
| 140 | + doc.write(decoder.decode(chunk, { stream: true })); |
| 141 | + } |
| 142 | + doc.close(); |
| 143 | +} |
| 144 | +``` |
| 145 | +
|
| 146 | +```html public/index.html |
| 147 | +<!DOCTYPE html> |
| 148 | +<html lang="en"> |
| 149 | +<head> |
| 150 | + <meta charset="UTF-8" /> |
| 151 | + <title>Browser-only rendering</title> |
| 152 | +</head> |
| 153 | +<body> |
| 154 | + <button id="render">Render the page</button> |
| 155 | + <br /><br /> |
| 156 | + <iframe id="preview" title="Rendered page"></iframe> |
| 157 | +</body> |
| 158 | +</html> |
| 159 | +``` |
| 160 | +
|
| 161 | +```css src/styles.css hidden |
| 162 | +iframe { |
| 163 | + width: 100%; |
| 164 | + height: 180px; |
| 165 | + border: 1px solid #aaa; |
| 166 | +} |
| 167 | +``` |
| 168 | +
|
| 169 | +```json package.json hidden |
| 170 | +{ |
| 171 | + "dependencies": { |
| 172 | + "react": "canary", |
| 173 | + "react-dom": "canary", |
| 174 | + "react-scripts": "latest" |
| 175 | + }, |
| 176 | + "scripts": { |
| 177 | + "start": "react-scripts start", |
| 178 | + "build": "react-scripts build", |
| 179 | + "test": "react-scripts test --env=jsdom", |
| 180 | + "eject": "react-scripts eject" |
| 181 | + } |
| 182 | +} |
| 183 | +``` |
| 184 | +
|
| 185 | +</Sandpack> |
| 186 | +
|
| 187 | +<Note> |
| 188 | +
|
| 189 | +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: |
| 190 | +
|
| 191 | +```js {1} |
| 192 | +'use client'; |
| 193 | + |
| 194 | +import { use } from 'react'; |
| 195 | +import { browser } from 'react-dom'; |
| 196 | + |
| 197 | +export default function BrowserOnlyEditor() { |
| 198 | + use(browser('The editor requires browser APIs.')); |
| 199 | + return <Editor />; |
| 200 | +} |
| 201 | +``` |
| 202 | +
|
| 203 | +</Note> |
| 204 | +
|
| 205 | +--- |
| 206 | +
|
| 207 | +### Conditionally rendering in the browser {/*conditionally-rendering-in-the-browser*/} |
| 208 | +
|
| 209 | +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: |
| 210 | +
|
| 211 | +```js {3} |
| 212 | +function useBrowserQuery(query, options) { |
| 213 | + if (options.initialData === undefined) { |
| 214 | + use(browser('useBrowserQuery: No initial data was provided.')); |
| 215 | + } |
| 216 | + |
| 217 | + return useQuery(query, options); |
| 218 | +} |
| 219 | + |
| 220 | +function ProductDetails({ productId, initialData }) { |
| 221 | + const product = useBrowserQuery(`/api/products/${productId}`, { |
| 222 | + initialData, |
| 223 | + }); |
| 224 | + |
| 225 | + return <h1>{product.name}</h1>; |
| 226 | +} |
| 227 | +``` |
| 228 | +
|
| 229 | +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. |
| 230 | +
|
| 231 | +--- |
| 232 | +
|
| 233 | +### Reporting browser-only rendering on the server {/*reporting-browser-only-rendering-on-the-server*/} |
| 234 | +
|
| 235 | +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`: |
| 236 | +
|
| 237 | +```js |
| 238 | +import { Suspense, use } from 'react'; |
| 239 | +import { browser } from 'react-dom'; |
| 240 | +import { renderToPipeableStream } from 'react-dom/server'; |
| 241 | + |
| 242 | +function BrowserOnlyEditor() { |
| 243 | + use(browser(() => new Error('The editor requires a browser API.'))); |
| 244 | + return <Editor />; |
| 245 | +} |
| 246 | + |
| 247 | +const { pipe } = renderToPipeableStream( |
| 248 | + <Suspense fallback={<p>Loading editor...</p>}> |
| 249 | + <BrowserOnlyEditor /> |
| 250 | + </Suspense>, |
| 251 | + { |
| 252 | + onShellReady() { |
| 253 | + pipe(response); |
| 254 | + }, |
| 255 | + onBrowserBailout(error, errorInfo) { |
| 256 | + logBrowserBailout(error, errorInfo); |
| 257 | + } |
| 258 | + } |
| 259 | +); |
| 260 | +``` |
| 261 | +
|
| 262 | +`onBrowserBailout` receives two arguments: |
| 263 | +
|
| 264 | +1. An `Error` describing the browser-only render. If you passed a reason to `browser`, it is available as the error's `cause`. |
| 265 | +2. An `errorInfo` object with a `componentStack` showing where browser-only rendering occurred. |
| 266 | +
|
| 267 | +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. |
| 268 | +
|
| 269 | +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`. |
| 270 | +
|
| 271 | +--- |
| 272 | +
|
| 273 | +### Aborting pending server rendering for the browser {/*aborting-pending-server-rendering-for-the-browser*/} |
| 274 | +
|
| 275 | +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: |
| 276 | +
|
| 277 | +```js {1,8} |
| 278 | +import { browser } from 'react-dom'; |
| 279 | +import { renderToPipeableStream } from 'react-dom/server'; |
| 280 | + |
| 281 | +const { pipe, abort } = renderToPipeableStream(<App />, { |
| 282 | + onShellReady() { |
| 283 | + pipe(response); |
| 284 | + setTimeout(() => { |
| 285 | + abort(browser('The server render timed out.')); |
| 286 | + }, 10000); |
| 287 | + } |
| 288 | +}); |
| 289 | +``` |
| 290 | +
|
| 291 | +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`. |
| 292 | +
|
| 293 | +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). |
0 commit comments