Skip to content

Commit 4a6d4c3

Browse files
committed
Document browser with use and Suspense
1 parent 385d306 commit 4a6d4c3

4 files changed

Lines changed: 291 additions & 11 deletions

File tree

src/content/reference/react-dom/browser.md

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ version: canary
1313

1414
</Canary>
1515

16-
`browser` lets you render part of a React tree only in the browser.
16+
`browser` lets you mark a component as browser-only during server rendering.
1717

1818
```js
1919
use(browser(reason?))
@@ -29,7 +29,7 @@ use(browser(reason?))
2929
3030
### `browser(reason?)` {/*browser*/}
3131
32-
Call `browser` inside [`use`](/reference/react/use) to skip rendering a component on the server and render it in the browser instead:
32+
Call `browser` inside [`use`](/reference/react/use) to mark a component as browser-only during server rendering:
3333
3434
```js
3535
import { use } from 'react';
@@ -47,7 +47,7 @@ During server rendering, `use(browser())` stops rendering the component and leav
4747
4848
#### Parameters {/*parameters*/}
4949
50-
* **optional** `reason`: A string or function that explains why the content needs to render in the browser. If you pass a function, React calls it each time a server renderer encounters the value returned by `browser`. React does not call it in the browser. Use a function for values that are expensive to create, such as `() => new Error(...)`. The string or the function's return value becomes the `cause` of the `Error` passed to `onBrowserBailout`.
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(...)`.
5151
5252
#### Returns {/*returns*/}
5353
@@ -56,19 +56,20 @@ During server rendering, `use(browser())` stops rendering the component and leav
5656
#### Caveats {/*caveats*/}
5757
5858
* `use(browser())` must be inside a `<Suspense>` boundary during server rendering. Without one, the server render fails.
59-
* `browser` is not available in a `react-server` environment. You can use it while rendering Client Components on the server, but you cannot import it in a [React Server Component](/reference/rsc/server-components).
60-
* Calling `browser()` by itself has no effect. You can create the value at module scope and reuse it.
61-
* To skip rendering a component on the server, pass the value returned by `browser` to `use`. Do not throw it.
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.
6261
6362
---
6463
6564
## Usage {/*usage*/}
6665
6766
### Rendering content only in the browser {/*rendering-content-only-in-the-browser*/}
6867
69-
Call `use` with the value returned by `browser` to skip rendering a component on the server:
68+
Call `browser` inside `use` in a component that should only render in the browser:
7069
71-
Press **Render on the server** to see the fallback first. The demo waits briefly before hydrating and showing the browser-only editor.
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.
7273
7374
<Sandpack>
7475
@@ -150,7 +151,7 @@ export async function flushReadableStreamToFrame(readable, frame) {
150151
<title>Browser-only rendering</title>
151152
</head>
152153
<body>
153-
<button id="render">Render on the server</button>
154+
<button id="render">Render the page</button>
154155
<br /><br />
155156
<iframe id="preview" title="Rendered page"></iframe>
156157
</body>
@@ -271,7 +272,7 @@ If there is no Suspense boundary to provide a fallback, the server render fails.
271272
272273
### Aborting pending server rendering for the browser {/*aborting-pending-server-rendering-for-the-browser*/}
273274
274-
Pass the value returned by `browser` as the reason when aborting a server render. React then leaves pending Suspense boundaries in their fallback state and renders their content in the browser:
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:
275276
276277
```js {1,8}
277278
import { browser } from 'react-dom';

src/content/reference/react-dom/index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ These APIs can be used to make apps faster by pre-loading resources such as scri
3434

3535
This API controls how components render on the server:
3636

37-
* <CanaryBadge /> [`browser`](/reference/react-dom/browser) lets you render part of a React tree only in the browser.
37+
* <CanaryBadge /> [`browser`](/reference/react-dom/browser) lets you mark a component as browser-only during server rendering.
3838

3939
---
4040

src/content/reference/react/Suspense.md

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2374,6 +2374,129 @@ The server HTML will include the loading indicator. It will be replaced by the `
23742374
23752375
---
23762376
2377+
### <CanaryBadge /> Providing a fallback for browser-only content {/*providing-a-fallback-for-browser-only-content*/}
2378+
2379+
A Suspense boundary can provide a fallback for a browser-only component. Wrap the component in `<Suspense>` and call [`use(browser())`](/reference/react/use#use-browser) inside it.
2380+
2381+
Press **Render the page**. The loading fallback appears first. After a short delay, React hydrates the page and displays the browser-only editor.
2382+
2383+
<Sandpack>
2384+
2385+
```js src/App.js active
2386+
import { Suspense, use } from 'react';
2387+
import { browser } from 'react-dom';
2388+
2389+
function BrowserOnlyEditor() {
2390+
use(browser('The editor requires browser APIs.'));
2391+
return <label>Draft: <input /></label>;
2392+
}
2393+
2394+
export default function App() {
2395+
return (
2396+
<Suspense fallback={<p>Loading editor...</p>}>
2397+
<BrowserOnlyEditor />
2398+
</Suspense>
2399+
);
2400+
}
2401+
```
2402+
2403+
```js src/Document.js hidden
2404+
import App from './App.js';
2405+
2406+
export default function Document() {
2407+
return (
2408+
<html lang="en">
2409+
<head>
2410+
<title>Article editor</title>
2411+
</head>
2412+
<body>
2413+
<h1>Article editor</h1>
2414+
<App />
2415+
</body>
2416+
</html>
2417+
);
2418+
}
2419+
```
2420+
2421+
```js src/index.js
2422+
import { hydrateRoot } from 'react-dom/client';
2423+
import { renderToReadableStream } from 'react-dom/server';
2424+
import Document from './Document.js';
2425+
import { flushReadableStreamToFrame } from './demo-helpers.js';
2426+
import './styles.css';
2427+
2428+
async function main(frame) {
2429+
const stream = await renderToReadableStream(<Document />);
2430+
await flushReadableStreamToFrame(stream, frame);
2431+
2432+
// Wait so both the fallback and hydrated content are visible.
2433+
await new Promise(resolve => setTimeout(resolve, 1200));
2434+
hydrateRoot(frame.contentDocument, <Document />);
2435+
}
2436+
2437+
const renderButton = document.getElementById('render');
2438+
renderButton.addEventListener('click', () => {
2439+
renderButton.disabled = true;
2440+
main(document.getElementById('preview'));
2441+
}, { once: true });
2442+
```
2443+
2444+
```js src/demo-helpers.js hidden
2445+
export async function flushReadableStreamToFrame(readable, frame) {
2446+
const doc = frame.contentWindow.document;
2447+
const decoder = new TextDecoder();
2448+
for await (const chunk of readable) {
2449+
doc.write(decoder.decode(chunk, { stream: true }));
2450+
}
2451+
doc.close();
2452+
}
2453+
```
2454+
2455+
```html public/index.html
2456+
<!DOCTYPE html>
2457+
<html lang="en">
2458+
<head>
2459+
<meta charset="UTF-8" />
2460+
<title>Browser-only rendering</title>
2461+
</head>
2462+
<body>
2463+
<button id="render">Render the page</button>
2464+
<br /><br />
2465+
<iframe id="preview" title="Rendered page"></iframe>
2466+
</body>
2467+
</html>
2468+
```
2469+
2470+
```css src/styles.css hidden
2471+
iframe {
2472+
width: 100%;
2473+
height: 180px;
2474+
border: 1px solid #aaa;
2475+
}
2476+
```
2477+
2478+
```json package.json hidden
2479+
{
2480+
"dependencies": {
2481+
"react": "canary",
2482+
"react-dom": "canary",
2483+
"react-scripts": "latest"
2484+
},
2485+
"scripts": {
2486+
"start": "react-scripts start",
2487+
"build": "react-scripts build",
2488+
"test": "react-scripts test --env=jsdom",
2489+
"eject": "react-scripts eject"
2490+
}
2491+
}
2492+
```
2493+
2494+
</Sandpack>
2495+
2496+
During server rendering, React includes the Suspense boundary's fallback in the HTML. In the browser, React replaces the fallback with the editor.
2497+
2498+
---
2499+
23772500
### Waiting for a stylesheet to load {/*waiting-for-a-stylesheet-to-load*/}
23782501
23792502
A stylesheet rendered with [`<link rel="stylesheet">` and a `precedence` prop](/reference/react-dom/components/link#special-rendering-behavior) blocks the Suspense boundary until the stylesheet loads, up to a timeout, so the content doesn't appear unstyled.

src/content/reference/react/use.md

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,39 @@ The resolved value of the Promise.
8080
8181
---
8282
83+
### <CanaryBadge /> `use(browser())` {/*use-browser*/}
84+
85+
Call `use` with the value returned by [`browser`](/reference/react-dom/browser) in a component that should only render in the browser:
86+
87+
```js
88+
import { use } from 'react';
89+
import { browser } from 'react-dom';
90+
91+
function BrowserOnly() {
92+
use(browser('This component requires browser APIs.'));
93+
return <BrowserContent />;
94+
}
95+
```
96+
97+
During server rendering, the component calling `use(browser())` suspends and React includes the closest [`<Suspense>`](/reference/react/Suspense) boundary's fallback in the HTML. In the browser, `use(browser())` returns `undefined`, so the component renders normally.
98+
99+
[See an example below.](#rendering-a-component-only-in-the-browser)
100+
101+
#### Parameters {/*browser-parameters*/}
102+
103+
* `browserValue`: The value returned by [`browser`](/reference/react-dom/browser).
104+
105+
#### Returns {/*browser-returns*/}
106+
107+
`use(browser())` returns `undefined` in the browser.
108+
109+
#### Caveats {/*browser-caveats*/}
110+
111+
* The component calling `use(browser())` must be inside a `<Suspense>` boundary during server rendering. Without one, server rendering fails.
112+
* 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).
113+
114+
---
115+
83116
## Usage (Context) {/*usage-context*/}
84117
85118
### Reading context with `use` {/*reading-context-with-use*/}
@@ -1293,6 +1326,129 @@ async function getData(url) {
12931326
12941327
---
12951328
1329+
### <CanaryBadge /> Rendering a component only in the browser {/*rendering-a-component-only-in-the-browser*/}
1330+
1331+
Pass the value returned by [`browser`](/reference/react-dom/browser) to `use` inside a component that should only render in the browser.
1332+
1333+
Press **Render the page**. The loading fallback appears first. After a short delay, React hydrates the page and displays the browser-only editor.
1334+
1335+
<Sandpack>
1336+
1337+
```js src/App.js active
1338+
import { Suspense, use } from 'react';
1339+
import { browser } from 'react-dom';
1340+
1341+
function BrowserOnlyEditor() {
1342+
use(browser('The editor requires browser APIs.'));
1343+
return <label>Draft: <input /></label>;
1344+
}
1345+
1346+
export default function App() {
1347+
return (
1348+
<Suspense fallback={<p>Loading editor...</p>}>
1349+
<BrowserOnlyEditor />
1350+
</Suspense>
1351+
);
1352+
}
1353+
```
1354+
1355+
```js src/Document.js hidden
1356+
import App from './App.js';
1357+
1358+
export default function Document() {
1359+
return (
1360+
<html lang="en">
1361+
<head>
1362+
<title>Article editor</title>
1363+
</head>
1364+
<body>
1365+
<h1>Article editor</h1>
1366+
<App />
1367+
</body>
1368+
</html>
1369+
);
1370+
}
1371+
```
1372+
1373+
```js src/index.js
1374+
import { hydrateRoot } from 'react-dom/client';
1375+
import { renderToReadableStream } from 'react-dom/server';
1376+
import Document from './Document.js';
1377+
import { flushReadableStreamToFrame } from './demo-helpers.js';
1378+
import './styles.css';
1379+
1380+
async function main(frame) {
1381+
const stream = await renderToReadableStream(<Document />);
1382+
await flushReadableStreamToFrame(stream, frame);
1383+
1384+
// Wait so both the fallback and hydrated content are visible.
1385+
await new Promise(resolve => setTimeout(resolve, 1200));
1386+
hydrateRoot(frame.contentDocument, <Document />);
1387+
}
1388+
1389+
const renderButton = document.getElementById('render');
1390+
renderButton.addEventListener('click', () => {
1391+
renderButton.disabled = true;
1392+
main(document.getElementById('preview'));
1393+
}, { once: true });
1394+
```
1395+
1396+
```js src/demo-helpers.js hidden
1397+
export async function flushReadableStreamToFrame(readable, frame) {
1398+
const doc = frame.contentWindow.document;
1399+
const decoder = new TextDecoder();
1400+
for await (const chunk of readable) {
1401+
doc.write(decoder.decode(chunk, { stream: true }));
1402+
}
1403+
doc.close();
1404+
}
1405+
```
1406+
1407+
```html public/index.html
1408+
<!DOCTYPE html>
1409+
<html lang="en">
1410+
<head>
1411+
<meta charset="UTF-8" />
1412+
<title>Browser-only rendering</title>
1413+
</head>
1414+
<body>
1415+
<button id="render">Render the page</button>
1416+
<br /><br />
1417+
<iframe id="preview" title="Rendered page"></iframe>
1418+
</body>
1419+
</html>
1420+
```
1421+
1422+
```css src/styles.css hidden
1423+
iframe {
1424+
width: 100%;
1425+
height: 180px;
1426+
border: 1px solid #aaa;
1427+
}
1428+
```
1429+
1430+
```json package.json hidden
1431+
{
1432+
"dependencies": {
1433+
"react": "canary",
1434+
"react-dom": "canary",
1435+
"react-scripts": "latest"
1436+
},
1437+
"scripts": {
1438+
"start": "react-scripts start",
1439+
"build": "react-scripts build",
1440+
"test": "react-scripts test --env=jsdom",
1441+
"eject": "react-scripts eject"
1442+
}
1443+
}
1444+
```
1445+
1446+
</Sandpack>
1447+
1448+
During server rendering, `use(browser())` suspends the component and React includes the closest Suspense boundary's fallback in the HTML. In the browser, `use(browser())` returns `undefined` and the editor renders normally.
1449+
1450+
---
1451+
12961452
## Troubleshooting {/*troubleshooting*/}
12971453
12981454
### I'm getting an error: "Suspense Exception: This is not a real error!" {/*suspense-exception-error*/}

0 commit comments

Comments
 (0)