Skip to content

Commit 7e9ff5d

Browse files
committed
Restore detailed Activity usage guidance
1 parent 6c0476a commit 7e9ff5d

1 file changed

Lines changed: 260 additions & 28 deletions

File tree

‎src/content/reference/react/Activity.md‎

Lines changed: 260 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,18 @@ boundary remains mounted.
9292

9393
## Usage {/*usage*/}
9494

95+
Activity is useful when part of the UI may become hidden and visible again. Unlike
96+
conditional rendering, hiding an Activity boundary preserves both React state and
97+
the DOM state of its children. Unlike hiding content only with CSS, Activity also
98+
cleans up the children's Effects and deprioritizes their updates while they are
99+
hidden.
100+
101+
Use an Activity boundary when preserving that work is valuable—for example, for a
102+
tab the user is likely to revisit or a panel that can prepare data in the
103+
background. If the content is unlikely to become visible again, conditionally
104+
rendering it may be preferable because unmounting allows React and the browser to
105+
release its resources.
106+
95107
### Preserving state while content is hidden {/*restoring-the-state-of-hidden-components*/}
96108

97109
When this condition becomes false, React removes `<Sidebar>` from the tree and
@@ -302,9 +314,10 @@ for DOM behavior that requires explicit cleanup.
302314

303315
### Pre-rendering content that is likely to become visible {/*pre-rendering-content-thats-likely-to-become-visible*/}
304316

305-
Content inside a hidden Activity boundary renders at a lower priority without
306-
running Effects created with `useEffect` or `useLayoutEffect`. This allows the
307-
content to begin loading code and render-time data before it becomes visible:
317+
An Activity boundary can also prepare content before the user sees it. Content
318+
inside a hidden boundary renders at a lower priority without running Effects
319+
created with `useEffect` or `useLayoutEffect`. This lets the content load code and
320+
render-time data without delaying updates to visible content:
308321

309322
```js
310323
<Suspense fallback={<Loading />}>
@@ -318,6 +331,103 @@ If `Posts` suspends while reading code or data, React continues rendering the re
318331
of the page. If that hidden work completes, switching to the Posts tab can reveal
319332
the content without waiting for the same work again.
320333

334+
The following example renders the Posts tab in a hidden Activity boundary when the
335+
page first loads. Wait briefly before selecting **Posts**. The list is available
336+
immediately because the hidden render started loading it in the background.
337+
338+
<Sandpack>
339+
340+
```js src/App.js active
341+
import { Activity, Suspense, use, useState } from 'react';
342+
import { getPosts } from './data.js';
343+
344+
export default function App() {
345+
const [activeTab, setActiveTab] = useState('home');
346+
347+
return (
348+
<>
349+
<div aria-label='Profile sections' role='group'>
350+
<button
351+
aria-pressed={activeTab === 'home'}
352+
onClick={() => setActiveTab('home')}
353+
>
354+
Home
355+
</button>
356+
<button
357+
aria-pressed={activeTab === 'posts'}
358+
onClick={() => setActiveTab('posts')}
359+
>
360+
Posts
361+
</button>
362+
</div>
363+
364+
<Suspense fallback={<h1>Loading posts...</h1>}>
365+
<Activity
366+
mode={activeTab === 'home' ? 'visible' : 'hidden'}
367+
>
368+
<Home />
369+
</Activity>
370+
<Activity
371+
mode={activeTab === 'posts' ? 'visible' : 'hidden'}
372+
>
373+
<Posts />
374+
</Activity>
375+
</Suspense>
376+
</>
377+
);
378+
}
379+
380+
function Home() {
381+
return <p>Welcome to my profile!</p>;
382+
}
383+
384+
function Posts() {
385+
const posts = use(getPosts());
386+
387+
return (
388+
<ul>
389+
{posts.map(post => (
390+
<li key={post.id}>{post.title}</li>
391+
))}
392+
</ul>
393+
);
394+
}
395+
```
396+
397+
```js src/data.js hidden
398+
let postsPromise;
399+
400+
export function getPosts() {
401+
if (!postsPromise) {
402+
postsPromise = loadPosts();
403+
}
404+
return postsPromise;
405+
}
406+
407+
async function loadPosts() {
408+
// Add a delay so that pre-rendering is easier to observe.
409+
await new Promise(resolve => setTimeout(resolve, 1500));
410+
411+
return Array.from({ length: 5 }, (_, index) => ({
412+
id: index,
413+
title: 'Post #' + (index + 1),
414+
}));
415+
}
416+
```
417+
418+
```css
419+
button {
420+
margin-right: 8px;
421+
}
422+
```
423+
424+
</Sandpack>
425+
426+
If the user selects Posts before the hidden render finishes, the nearest Suspense
427+
fallback appears until the data is ready. Activity improves the likely case where
428+
the background work finishes first; it does not guarantee that the content will
429+
always be ready.
430+
321431
<Note>
322432

323433
Only code and data read during rendering can load during pre-rendering. Activity
@@ -330,21 +440,30 @@ cached Promise with [`use`](/reference/react/use). See
330440

331441
</Note>
332442

333-
<DeepDive>
443+
---
444+
445+
### Improving hydration performance {/*speeding-up-interactions-during-page-load*/}
334446

335-
#### How does `<Activity>` affect server rendering and hydration? {/*speeding-up-interactions-during-page-load*/}
447+
Activity boundaries also divide server-rendered pages into units that React can
448+
hydrate independently. This is related to the selective hydration behavior of
449+
[`<Suspense>`](/reference/react/Suspense), but it does not require displaying a
450+
fallback in the initial UI.
336451

337-
React does not include initially hidden `<Activity>` content in server-rendered HTML.
338-
On the client, React hydrates the visible content first and renders the hidden
339-
content later at a lower priority.
452+
For example, without a boundary React hydrates this page as one unit:
340453

341-
For initially visible `<Activity>` boundaries, React includes the content in the
342-
server-rendered HTML but can hydrate the boundary independently from surrounding
343-
content. If the user interacts with the boundary before hydration reaches it,
344-
React prioritizes hydrating that boundary.
454+
```js
455+
function Page() {
456+
return (
457+
<>
458+
<Post />
459+
<Comments />
460+
</>
461+
);
462+
}
463+
```
345464

346-
An always-visible `<Activity>` boundary lets React hydrate that subtree
347-
independently:
465+
Wrapping `Comments` in an always-visible Activity boundary creates a separate
466+
hydration unit:
348467

349468
```js
350469
function Page() {
@@ -359,7 +478,43 @@ function Page() {
359478
}
360479
```
361480

362-
</DeepDive>
481+
The boundary is visible because the `mode` prop defaults to `'visible'`. Its
482+
server-rendered HTML remains visible, but React can hydrate it independently from
483+
the surrounding page. If the user interacts with that content before React reaches
484+
it, React prioritizes hydrating the boundary.
485+
486+
You can also use visible and hidden Activity boundaries for tabbed content:
487+
488+
```js
489+
function Page() {
490+
const [activeTab, setActiveTab] = useState('home');
491+
492+
return (
493+
<>
494+
<button onClick={() => setActiveTab('home')}>
495+
Home
496+
</button>
497+
<button onClick={() => setActiveTab('video')}>
498+
Video
499+
</button>
500+
501+
<Activity mode={activeTab === 'home' ? 'visible' : 'hidden'}>
502+
<Home />
503+
</Activity>
504+
<Activity mode={activeTab === 'video' ? 'visible' : 'hidden'}>
505+
<Video />
506+
</Activity>
507+
</>
508+
);
509+
}
510+
```
511+
512+
React does not include initially hidden `<Activity>` content in server-rendered HTML.
513+
On the client, React hydrates the visible content first and renders the hidden
514+
content later at a lower priority. Initially visible boundaries are included in the
515+
server-rendered HTML and can be hydrated independently. This allows the visible tab
516+
and the controls around it to become interactive without waiting for React to
517+
render the initially hidden tab.
363518

364519
---
365520

@@ -375,13 +530,18 @@ the boundary is hidden.
375530

376531
##### Preserved media can continue playing {/*preserved-media-can-continue-playing*/}
377532

378-
Add the corresponding cleanup to an Effect. For behavior that must stop when React
379-
hides the boundary, use [`useLayoutEffect`](/reference/react/useLayoutEffect):
533+
Unmounting a `<video>` element stops playback because the browser removes the DOM
534+
node. Hiding it with Activity preserves the node, so playback continues unless the
535+
component pauses it explicitly.
536+
537+
Add the corresponding cleanup to an Effect. For behavior that must stop at the
538+
same time React hides the boundary, use
539+
[`useLayoutEffect`](/reference/react/useLayoutEffect):
380540

381541
```js
382542
import { useLayoutEffect, useRef } from 'react';
383543

384-
function VideoPlayer({ src, captions }) {
544+
function VideoPlayer({ src }) {
385545
const ref = useRef(null);
386546

387547
useLayoutEffect(() => {
@@ -392,16 +552,7 @@ function VideoPlayer({ src, captions }) {
392552
};
393553
}, []);
394554

395-
return (
396-
<video ref={ref} controls src={src}>
397-
<track
398-
default
399-
kind='captions'
400-
src={captions}
401-
srcLang='en'
402-
/>
403-
</video>
404-
);
555+
return <video ref={ref} controls src={src} />;
405556
}
406557
```
407558

@@ -413,6 +564,87 @@ The `useLayoutEffect` cleanup runs as part of hiding the UI. A cleanup from
413564
`useEffect` can run later if, for example, a Suspense boundary suspends or a View
414565
Transition is in progress.
415566

567+
This complete example pauses the video when you switch to Home while preserving
568+
its playback position. When you return to Video, playback can continue from the
569+
same position without recreating the element or downloading it again.
570+
571+
<Sandpack>
572+
573+
```js src/App.js active
574+
import {
575+
Activity,
576+
useLayoutEffect,
577+
useRef,
578+
useState,
579+
} from 'react';
580+
581+
export default function App() {
582+
const [activeTab, setActiveTab] = useState('video');
583+
584+
return (
585+
<>
586+
<div aria-label='Media sections' role='group'>
587+
<button
588+
aria-pressed={activeTab === 'home'}
589+
onClick={() => setActiveTab('home')}
590+
>
591+
Home
592+
</button>
593+
<button
594+
aria-pressed={activeTab === 'video'}
595+
onClick={() => setActiveTab('video')}
596+
>
597+
Video
598+
</button>
599+
</div>
600+
601+
<Activity mode={activeTab === 'home' ? 'visible' : 'hidden'}>
602+
<p>Welcome home!</p>
603+
</Activity>
604+
<Activity mode={activeTab === 'video' ? 'visible' : 'hidden'}>
605+
<VideoPlayer />
606+
</Activity>
607+
</>
608+
);
609+
}
610+
611+
function VideoPlayer() {
612+
const ref = useRef(null);
613+
614+
useLayoutEffect(() => {
615+
const video = ref.current;
616+
617+
return () => {
618+
video.pause();
619+
};
620+
}, []);
621+
622+
return (
623+
<video
624+
aria-label='Big Buck Bunny video'
625+
controls
626+
playsInline
627+
ref={ref}
628+
src='https://archive.org/download/BigBuckBunny_124/Content/big_buck_bunny_720p_surround.mp4'
629+
/>
630+
);
631+
}
632+
```
633+
634+
```css
635+
button {
636+
margin-right: 8px;
637+
}
638+
video {
639+
aspect-ratio: 16 / 9;
640+
margin-top: 12px;
641+
max-width: 100%;
642+
width: 400px;
643+
}
644+
```
645+
646+
</Sandpack>
647+
416648
</Pitfall>
417649

418650
---

0 commit comments

Comments
 (0)