Skip to content

Commit 24618e2

Browse files
Add a conditional browser query example (#8617)
* Add conditional browser query example * Move browser query hook into its own example file * Use IndexedDB in conditional browser example * Clarify conditional browser rendering example * Polish browser rendering examples * Bridge conditional browser examples * Tighten conditional browser example intro * Use time zone for conditional browser example * Clarify conditional example transition * Lead conditional browser docs with time zone example * Address conditional browser rendering feedback * update * Clarify conditional browser control flow * Clarify conditional data fetching pattern * Clarify conditional rendering outcomes * Polish conditional browser examples * Clarify conditional query rendering * Align conditional browser guidance
1 parent 7c36f7a commit 24618e2

1 file changed

Lines changed: 162 additions & 15 deletions

File tree

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

Lines changed: 162 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -234,9 +234,153 @@ export default function SavedDraft() {
234234
235235
---
236236
237-
### Conditionally rendering in the browser {/*conditionally-rendering-in-the-browser*/}
237+
### Conditionally rendering on the server {/*conditionally-rendering-on-the-server*/}
238238
239-
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:
239+
Like other calls to [`use`](/reference/react/use), `use(browser())` can be called inside a conditional statement or after an early return. This lets a Component or custom Hook opt out of server rendering based on a condition, such as the value of a prop.
240+
241+
For example, this `useTimeZone` Hook accepts an optional default value. When provided, React renders the default value in the initial HTML and in the browser. Without a default value, the Component suspends during server rendering and shows the device's local time zone in the browser.
242+
243+
Click **Reload** to see the loading fallback before the user's time zone appears.
244+
245+
<Sandpack>
246+
247+
```js src/App.js
248+
import { Suspense } from 'react';
249+
import { useTimeZone } from './useTimeZone.js';
250+
251+
function TimeZone({label, defaultTimeZone}) {
252+
const timeZone = useTimeZone(defaultTimeZone);
253+
return <p>{label}: <strong>{timeZone}</strong></p>;
254+
}
255+
256+
export default function App() {
257+
return (
258+
<>
259+
<h1>Event details</h1>
260+
<TimeZone
261+
label="Event time zone"
262+
defaultTimeZone="America/New_York"
263+
/>
264+
<Suspense fallback={<p>Loading your time zone...</p>}>
265+
<TimeZone label="Your time zone" />
266+
</Suspense>
267+
</>
268+
);
269+
}
270+
```
271+
272+
```js src/useTimeZone.js active
273+
import { use } from 'react';
274+
import { browser } from 'react-dom';
275+
276+
export function useTimeZone(defaultTimeZone) {
277+
if (defaultTimeZone !== undefined) {
278+
return defaultTimeZone;
279+
}
280+
281+
use(browser('No default time zone was provided.'));
282+
return Intl.DateTimeFormat().resolvedOptions().timeZone;
283+
}
284+
```
285+
286+
```js src/Document.js hidden
287+
import App from './App.js';
288+
289+
export default function Document() {
290+
return (
291+
<html lang="en">
292+
<head>
293+
<title>Event details</title>
294+
<style>{`
295+
h1 { font-size: 24px; margin-top: 0; }
296+
`}</style>
297+
</head>
298+
<body>
299+
<App />
300+
</body>
301+
</html>
302+
);
303+
}
304+
```
305+
306+
```js src/index.js hidden
307+
import { hydrateRoot } from 'react-dom/client';
308+
import { renderToReadableStream } from 'react-dom/server';
309+
import Document from './Document.js';
310+
import { flushReadableStreamToFrame } from './demo-helpers.js';
311+
import './styles.css';
312+
313+
async function main(frame) {
314+
const stream = await renderToReadableStream(<Document />);
315+
await flushReadableStreamToFrame(stream, frame);
316+
317+
// Wait so both the fallback and hydrated content are visible.
318+
await new Promise(resolve => setTimeout(resolve, 1200));
319+
hydrateRoot(frame.contentDocument, <Document />);
320+
}
321+
322+
main(document.getElementById('preview'));
323+
```
324+
325+
```js src/demo-helpers.js hidden
326+
export async function flushReadableStreamToFrame(readable, frame) {
327+
const doc = frame.contentWindow.document;
328+
const decoder = new TextDecoder();
329+
const reader = readable.getReader();
330+
331+
while (true) {
332+
const {done, value} = await reader.read();
333+
if (done) {
334+
break;
335+
}
336+
doc.write(decoder.decode(value, {stream: true}));
337+
}
338+
339+
doc.write(decoder.decode());
340+
doc.close();
341+
}
342+
```
343+
344+
```html public/index.html hidden
345+
<!DOCTYPE html>
346+
<html lang="en">
347+
<head>
348+
<meta charset="UTF-8" />
349+
<title>Conditional browser rendering</title>
350+
</head>
351+
<body>
352+
<iframe id="preview" title="Rendered page"></iframe>
353+
</body>
354+
</html>
355+
```
356+
357+
```css src/styles.css hidden
358+
iframe {
359+
width: 100%;
360+
height: 240px;
361+
border: 0;
362+
}
363+
```
364+
365+
```json package.json hidden
366+
{
367+
"dependencies": {
368+
"react": "19.3.0-canary-eb8feb71-20260814",
369+
"react-dom": "19.3.0-canary-eb8feb71-20260814",
370+
"react-scripts": "latest"
371+
},
372+
"scripts": {
373+
"start": "react-scripts start",
374+
"build": "react-scripts build",
375+
"test": "react-scripts test --env=jsdom",
376+
"eject": "react-scripts eject"
377+
}
378+
}
379+
```
380+
381+
</Sandpack>
382+
383+
You can apply a similar pattern to conditionally avoid server rendering when using a Suspense-enabled data-fetching library:
240384
241385
```js {3}
242386
function useBrowserQuery(query, options) {
@@ -256,7 +400,7 @@ function ProductDetails({ productId, initialData }) {
256400
}
257401
```
258402
259-
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.
403+
With `initialData`, React renders the Component to HTML on the server. Without it, React leaves the closest [`<Suspense>`](/reference/react/Suspense) boundary's fallback in the HTML. In the browser, `useQuery` can fetch the data or read it from its client cache as usual.
260404
261405
---
262406
@@ -275,19 +419,22 @@ function SavedDraft() {
275419
return <DraftEditor initialDraft={draft} />;
276420
}
277421

278-
const { pipe } = renderToPipeableStream(
279-
<Suspense fallback={<p>Loading saved draft...</p>}>
280-
<SavedDraft />
281-
</Suspense>,
282-
{
283-
onShellReady() {
284-
pipe(response);
285-
},
286-
onBrowserBailout(error, errorInfo) {
287-
logBrowserBailout(error, errorInfo);
288-
}
422+
function App() {
423+
return (
424+
<Suspense fallback={<p>Loading saved draft...</p>}>
425+
<SavedDraft />
426+
</Suspense>
427+
);
428+
}
429+
430+
const { pipe } = renderToPipeableStream(<App />, {
431+
onShellReady() {
432+
pipe(response);
433+
},
434+
onBrowserBailout(error, errorInfo) {
435+
logBrowserBailout(error, errorInfo);
289436
}
290-
);
437+
});
291438
```
292439
293440
`onBrowserBailout` receives two arguments:

0 commit comments

Comments
 (0)