Skip to content

Commit dcde1e1

Browse files
committed
Improve browser-only rendering example
1 parent 0d29012 commit dcde1e1

3 files changed

Lines changed: 122 additions & 51 deletions

File tree

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

Lines changed: 48 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -69,23 +69,43 @@ 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+
Press **Render the page** to see the loading fallback. React then displays a 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 />
107+
<Suspense fallback={<p>Loading draft...</p>}>
108+
<SavedDraft />
89109
</Suspense>
90110
);
91111
}
@@ -98,18 +118,21 @@ export default function Document() {
98118
return (
99119
<html lang="en">
100120
<head>
101-
<title>Article editor</title>
121+
<title>Saved draft</title>
122+
<style>{`
123+
label, textarea { display: block; }
124+
textarea { margin-top: 5px; }
125+
`}</style>
102126
</head>
103127
<body>
104-
<h1>Article editor</h1>
105128
<App />
106129
</body>
107130
</html>
108131
);
109132
}
110133
```
111134
112-
```js src/index.js
135+
```js src/index.js hidden
113136
import { hydrateRoot } from 'react-dom/client';
114137
import { renderToReadableStream } from 'react-dom/server';
115138
import Document from './Document.js';
@@ -151,7 +174,7 @@ 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>
@@ -169,8 +192,8 @@ export async function flushReadableStreamToFrame(readable, frame) {
169192
```css src/styles.css hidden
170193
iframe {
171194
width: 100%;
172-
height: 180px;
173-
border: 1px solid #aaa;
195+
height: 120px;
196+
border: 0;
174197
}
175198
```
176199
@@ -199,12 +222,13 @@ In a React Server Components app, `use(browser())` must be called from a Client
199222
```js {1}
200223
'use client';
201224

202-
import { use } from 'react';
225+
import { use, useState } from 'react';
203226
import { browser } from 'react-dom';
204227

205-
export default function BrowserOnlyEditor() {
206-
use(browser('The editor requires browser APIs.'));
207-
return <Editor />;
228+
export default function SavedDraft() {
229+
use(browser('The saved draft is stored in localStorage.'));
230+
const [draft] = useState(() => localStorage.getItem('draft') ?? '');
231+
return <DraftEditor initialDraft={draft} />;
208232
}
209233
```
210234
@@ -243,18 +267,19 @@ On the server, `useBrowserQuery` calls `useQuery` only when `initialData` is ava
243267
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`:
244268
245269
```js
246-
import { Suspense, use } from 'react';
270+
import { Suspense, use, useState } from 'react';
247271
import { browser } from 'react-dom';
248272
import { renderToPipeableStream } from 'react-dom/server';
249273

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

255280
const { pipe } = renderToPipeableStream(
256-
<Suspense fallback={<p>Loading editor...</p>}>
257-
<BrowserOnlyEditor />
281+
<Suspense fallback={<p>Loading saved draft...</p>}>
282+
<SavedDraft />
258283
</Suspense>,
259284
{
260285
onShellReady() {

src/content/reference/react/Suspense.md

Lines changed: 37 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2386,23 +2386,43 @@ 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+
Press **Render the page** to see the loading fallback. React then displays a 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 />
2424+
<Suspense fallback={<p>Loading draft...</p>}>
2425+
<SavedDraft />
24062426
</Suspense>
24072427
);
24082428
}
@@ -2415,18 +2435,21 @@ export default function Document() {
24152435
return (
24162436
<html lang="en">
24172437
<head>
2418-
<title>Article editor</title>
2438+
<title>Saved draft</title>
2439+
<style>{`
2440+
label, textarea { display: block; }
2441+
textarea { margin-top: 5px; }
2442+
`}</style>
24192443
</head>
24202444
<body>
2421-
<h1>Article editor</h1>
24222445
<App />
24232446
</body>
24242447
</html>
24252448
);
24262449
}
24272450
```
24282451
2429-
```js src/index.js
2452+
```js src/index.js hidden
24302453
import { hydrateRoot } from 'react-dom/client';
24312454
import { renderToReadableStream } from 'react-dom/server';
24322455
import Document from './Document.js';
@@ -2468,7 +2491,7 @@ 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>
@@ -2486,8 +2509,8 @@ export async function flushReadableStreamToFrame(readable, frame) {
24862509
```css src/styles.css hidden
24872510
iframe {
24882511
width: 100%;
2489-
height: 180px;
2490-
border: 1px solid #aaa;
2512+
height: 120px;
2513+
border: 0;
24912514
}
24922515
```
24932516
@@ -2509,7 +2532,7 @@ iframe {
25092532
25102533
</Sandpack>
25112534
2512-
During server rendering, React includes the Suspense boundary's fallback in the HTML. In the browser, React replaces the fallback with the editor.
2535+
During server rendering, React includes the Suspense boundary's fallback in the HTML. In the browser, React replaces the fallback with the saved draft.
25132536
25142537
---
25152538

src/content/reference/react/use.md

Lines changed: 37 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1332,23 +1332,43 @@ async function getData(url) {
13321332
13331333
Pass the value returned by [`browser`](/reference/react-dom/browser) to `use` inside a component that should only render in the browser.
13341334
1335-
Press **Render the page**. The loading fallback appears first. After a short delay, React hydrates the page and displays the browser-only editor.
1335+
Press **Render the page** to see the loading fallback. React then displays a draft loaded from `localStorage`.
13361336
13371337
<Sandpack>
13381338
13391339
```js src/App.js active
1340-
import { Suspense, use } from 'react';
1340+
import { Suspense, use, useState } from 'react';
13411341
import { browser } from 'react-dom';
13421342

1343-
function BrowserOnlyEditor() {
1344-
use(browser('The editor requires browser APIs.'));
1345-
return <label>Draft: <input /></label>;
1343+
function SavedDraft() {
1344+
use(browser('The draft is stored in localStorage.'));
1345+
const [draft, setDraft] = useState(
1346+
() => localStorage.getItem('draft') ?? ''
1347+
);
1348+
1349+
function handleChange(event) {
1350+
const nextDraft = event.target.value;
1351+
setDraft(nextDraft);
1352+
localStorage.setItem('draft', nextDraft);
1353+
}
1354+
1355+
return (
1356+
<label>
1357+
Draft:
1358+
<textarea
1359+
value={draft}
1360+
onChange={handleChange}
1361+
rows={4}
1362+
cols={30}
1363+
/>
1364+
</label>
1365+
);
13461366
}
13471367

13481368
export default function App() {
13491369
return (
1350-
<Suspense fallback={<p>Loading editor...</p>}>
1351-
<BrowserOnlyEditor />
1370+
<Suspense fallback={<p>Loading draft...</p>}>
1371+
<SavedDraft />
13521372
</Suspense>
13531373
);
13541374
}
@@ -1361,18 +1381,21 @@ export default function Document() {
13611381
return (
13621382
<html lang="en">
13631383
<head>
1364-
<title>Article editor</title>
1384+
<title>Saved draft</title>
1385+
<style>{`
1386+
label, textarea { display: block; }
1387+
textarea { margin-top: 5px; }
1388+
`}</style>
13651389
</head>
13661390
<body>
1367-
<h1>Article editor</h1>
13681391
<App />
13691392
</body>
13701393
</html>
13711394
);
13721395
}
13731396
```
13741397
1375-
```js src/index.js
1398+
```js src/index.js hidden
13761399
import { hydrateRoot } from 'react-dom/client';
13771400
import { renderToReadableStream } from 'react-dom/server';
13781401
import Document from './Document.js';
@@ -1414,7 +1437,7 @@ export async function flushReadableStreamToFrame(readable, frame) {
14141437
}
14151438
```
14161439
1417-
```html public/index.html
1440+
```html public/index.html hidden
14181441
<!DOCTYPE html>
14191442
<html lang="en">
14201443
<head>
@@ -1432,8 +1455,8 @@ export async function flushReadableStreamToFrame(readable, frame) {
14321455
```css src/styles.css hidden
14331456
iframe {
14341457
width: 100%;
1435-
height: 180px;
1436-
border: 1px solid #aaa;
1458+
height: 120px;
1459+
border: 0;
14371460
}
14381461
```
14391462
@@ -1455,7 +1478,7 @@ iframe {
14551478
14561479
</Sandpack>
14571480
1458-
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.
1481+
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 saved draft renders normally.
14591482
14601483
---
14611484

0 commit comments

Comments
 (0)