Skip to content

Commit 12d692d

Browse files
Improve the browser-only rendering example (#8616)
* Improve browser-only rendering example * Clarify browser-only example action * Show browser-only fallback on reload
1 parent 0d29012 commit 12d692d

3 files changed

Lines changed: 140 additions & 75 deletions

File tree

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

Lines changed: 54 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -69,24 +69,47 @@ Call `browser` inside `use` in a component that should only render in the browse
6969
7070
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.
7171
72-
Press **Render the page**. The loading fallback appears first. After a short delay, React hydrates the page and displays the browser-only editor.
72+
Click **Reload** to see the loading fallback in the initial HTML. After hydration, React displays the draft loaded from `localStorage`.
7373
7474
<Sandpack>
7575
7676
```js src/App.js active
77-
import { Suspense, use } from 'react';
77+
import { Suspense, use, useState } from 'react';
7878
import { browser } from 'react-dom';
7979

80-
function BrowserOnlyEditor() {
81-
use(browser('The editor requires browser APIs.'));
82-
return <label>Draft: <input /></label>;
80+
function SavedDraft() {
81+
use(browser('The draft is stored in localStorage.'));
82+
const [draft, setDraft] = useState(
83+
() => localStorage.getItem('draft') ?? ''
84+
);
85+
86+
function handleChange(event) {
87+
const nextDraft = event.target.value;
88+
setDraft(nextDraft);
89+
localStorage.setItem('draft', nextDraft);
90+
}
91+
92+
return (
93+
<label>
94+
Draft:
95+
<textarea
96+
value={draft}
97+
onChange={handleChange}
98+
rows={4}
99+
cols={30}
100+
/>
101+
</label>
102+
);
83103
}
84104

85105
export default function App() {
86106
return (
87-
<Suspense fallback={<p>Loading editor...</p>}>
88-
<BrowserOnlyEditor />
89-
</Suspense>
107+
<>
108+
<h1>Saved draft</h1>
109+
<Suspense fallback={<p>Loading draft...</p>}>
110+
<SavedDraft />
111+
</Suspense>
112+
</>
90113
);
91114
}
92115
```
@@ -98,18 +121,22 @@ export default function Document() {
98121
return (
99122
<html lang="en">
100123
<head>
101-
<title>Article editor</title>
124+
<title>Saved draft</title>
125+
<style>{`
126+
h1 { font-size: 24px; margin-top: 0; }
127+
label, textarea { display: block; }
128+
textarea { margin-top: 5px; }
129+
`}</style>
102130
</head>
103131
<body>
104-
<h1>Article editor</h1>
105132
<App />
106133
</body>
107134
</html>
108135
);
109136
}
110137
```
111138
112-
```js src/index.js
139+
```js src/index.js hidden
113140
import { hydrateRoot } from 'react-dom/client';
114141
import { renderToReadableStream } from 'react-dom/server';
115142
import Document from './Document.js';
@@ -125,11 +152,7 @@ async function main(frame) {
125152
hydrateRoot(frame.contentDocument, <Document />);
126153
}
127154

128-
const renderButton = document.getElementById('render');
129-
renderButton.addEventListener('click', () => {
130-
renderButton.disabled = true;
131-
main(document.getElementById('preview'));
132-
}, { once: true });
155+
main(document.getElementById('preview'));
133156
```
134157
135158
```js src/demo-helpers.js hidden
@@ -151,16 +174,14 @@ export async function flushReadableStreamToFrame(readable, frame) {
151174
}
152175
```
153176
154-
```html public/index.html
177+
```html public/index.html hidden
155178
<!DOCTYPE html>
156179
<html lang="en">
157180
<head>
158181
<meta charset="UTF-8" />
159182
<title>Browser-only rendering</title>
160183
</head>
161184
<body>
162-
<button id="render">Render the page</button>
163-
<br /><br />
164185
<iframe id="preview" title="Rendered page"></iframe>
165186
</body>
166187
</html>
@@ -169,8 +190,8 @@ export async function flushReadableStreamToFrame(readable, frame) {
169190
```css src/styles.css hidden
170191
iframe {
171192
width: 100%;
172-
height: 180px;
173-
border: 1px solid #aaa;
193+
height: 160px;
194+
border: 0;
174195
}
175196
```
176197
@@ -199,12 +220,13 @@ In a React Server Components app, `use(browser())` must be called from a Client
199220
```js {1}
200221
'use client';
201222

202-
import { use } from 'react';
223+
import { use, useState } from 'react';
203224
import { browser } from 'react-dom';
204225

205-
export default function BrowserOnlyEditor() {
206-
use(browser('The editor requires browser APIs.'));
207-
return <Editor />;
226+
export default function SavedDraft() {
227+
use(browser('The saved draft is stored in localStorage.'));
228+
const [draft] = useState(() => localStorage.getItem('draft') ?? '');
229+
return <DraftEditor initialDraft={draft} />;
208230
}
209231
```
210232
@@ -243,18 +265,19 @@ On the server, `useBrowserQuery` calls `useQuery` only when `initialData` is ava
243265
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`:
244266
245267
```js
246-
import { Suspense, use } from 'react';
268+
import { Suspense, use, useState } from 'react';
247269
import { browser } from 'react-dom';
248270
import { renderToPipeableStream } from 'react-dom/server';
249271

250-
function BrowserOnlyEditor() {
251-
use(browser(() => new Error('The editor requires a browser API.')));
252-
return <Editor />;
272+
function SavedDraft() {
273+
use(browser(() => new Error('The saved draft is stored in localStorage.')));
274+
const [draft] = useState(() => localStorage.getItem('draft') ?? '');
275+
return <DraftEditor initialDraft={draft} />;
253276
}
254277

255278
const { pipe } = renderToPipeableStream(
256-
<Suspense fallback={<p>Loading editor...</p>}>
257-
<BrowserOnlyEditor />
279+
<Suspense fallback={<p>Loading saved draft...</p>}>
280+
<SavedDraft />
258281
</Suspense>,
259282
{
260283
onShellReady() {

src/content/reference/react/Suspense.md

Lines changed: 43 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2386,24 +2386,47 @@ The server HTML will include the loading indicator. It will be replaced by the `
23862386
23872387
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.
23882388
2389-
Press **Render the page**. The loading fallback appears first. After a short delay, React hydrates the page and displays the browser-only editor.
2389+
Click **Reload** to see the loading fallback in the initial HTML. After hydration, React displays the draft loaded from `localStorage`.
23902390
23912391
<Sandpack>
23922392
23932393
```js src/App.js active
2394-
import { Suspense, use } from 'react';
2394+
import { Suspense, use, useState } from 'react';
23952395
import { browser } from 'react-dom';
23962396

2397-
function BrowserOnlyEditor() {
2398-
use(browser('The editor requires browser APIs.'));
2399-
return <label>Draft: <input /></label>;
2397+
function SavedDraft() {
2398+
use(browser('The draft is stored in localStorage.'));
2399+
const [draft, setDraft] = useState(
2400+
() => localStorage.getItem('draft') ?? ''
2401+
);
2402+
2403+
function handleChange(event) {
2404+
const nextDraft = event.target.value;
2405+
setDraft(nextDraft);
2406+
localStorage.setItem('draft', nextDraft);
2407+
}
2408+
2409+
return (
2410+
<label>
2411+
Draft:
2412+
<textarea
2413+
value={draft}
2414+
onChange={handleChange}
2415+
rows={4}
2416+
cols={30}
2417+
/>
2418+
</label>
2419+
);
24002420
}
24012421

24022422
export default function App() {
24032423
return (
2404-
<Suspense fallback={<p>Loading editor...</p>}>
2405-
<BrowserOnlyEditor />
2406-
</Suspense>
2424+
<>
2425+
<h1>Saved draft</h1>
2426+
<Suspense fallback={<p>Loading draft...</p>}>
2427+
<SavedDraft />
2428+
</Suspense>
2429+
</>
24072430
);
24082431
}
24092432
```
@@ -2415,18 +2438,22 @@ export default function Document() {
24152438
return (
24162439
<html lang="en">
24172440
<head>
2418-
<title>Article editor</title>
2441+
<title>Saved draft</title>
2442+
<style>{`
2443+
h1 { font-size: 24px; margin-top: 0; }
2444+
label, textarea { display: block; }
2445+
textarea { margin-top: 5px; }
2446+
`}</style>
24192447
</head>
24202448
<body>
2421-
<h1>Article editor</h1>
24222449
<App />
24232450
</body>
24242451
</html>
24252452
);
24262453
}
24272454
```
24282455
2429-
```js src/index.js
2456+
```js src/index.js hidden
24302457
import { hydrateRoot } from 'react-dom/client';
24312458
import { renderToReadableStream } from 'react-dom/server';
24322459
import Document from './Document.js';
@@ -2442,11 +2469,7 @@ async function main(frame) {
24422469
hydrateRoot(frame.contentDocument, <Document />);
24432470
}
24442471

2445-
const renderButton = document.getElementById('render');
2446-
renderButton.addEventListener('click', () => {
2447-
renderButton.disabled = true;
2448-
main(document.getElementById('preview'));
2449-
}, { once: true });
2472+
main(document.getElementById('preview'));
24502473
```
24512474
24522475
```js src/demo-helpers.js hidden
@@ -2468,16 +2491,14 @@ export async function flushReadableStreamToFrame(readable, frame) {
24682491
}
24692492
```
24702493
2471-
```html public/index.html
2494+
```html public/index.html hidden
24722495
<!DOCTYPE html>
24732496
<html lang="en">
24742497
<head>
24752498
<meta charset="UTF-8" />
24762499
<title>Browser-only rendering</title>
24772500
</head>
24782501
<body>
2479-
<button id="render">Render the page</button>
2480-
<br /><br />
24812502
<iframe id="preview" title="Rendered page"></iframe>
24822503
</body>
24832504
</html>
@@ -2486,8 +2507,8 @@ export async function flushReadableStreamToFrame(readable, frame) {
24862507
```css src/styles.css hidden
24872508
iframe {
24882509
width: 100%;
2489-
height: 180px;
2490-
border: 1px solid #aaa;
2510+
height: 160px;
2511+
border: 0;
24912512
}
24922513
```
24932514
@@ -2509,7 +2530,7 @@ iframe {
25092530
25102531
</Sandpack>
25112532
2512-
During server rendering, React includes the Suspense boundary's fallback in the HTML. In the browser, React replaces the fallback with the editor.
2533+
During server rendering, React includes the Suspense boundary's fallback in the HTML. In the browser, React replaces the fallback with the saved draft.
25132534
25142535
---
25152536

0 commit comments

Comments
 (0)