Skip to content

Commit b119ed4

Browse files
committed
Expand Activity comparison examples
1 parent a85ac54 commit b119ed4

1 file changed

Lines changed: 288 additions & 7 deletions

File tree

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

Lines changed: 288 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -69,21 +69,25 @@ Use an Activity boundary when hidden content is likely to become visible again,
6969

7070
### Preserving component state while content is hidden {/*restoring-the-state-of-hidden-components*/}
7171

72-
With conditional rendering, React removes `<Sidebar>` from the tree when `isShowingSidebar` becomes `false` and discards its state:
72+
Conditional rendering mounts or unmounts a component as its condition changes:
7373

7474
```js
7575
{isShowingSidebar && <Sidebar />}
7676
```
7777

78-
Render the component inside an Activity boundary to preserve its state while it is hidden:
78+
Unmounting `<Sidebar>` destroys its internal state. As a result, its state resets each time it is shown.
79+
80+
To preserve the state between hides, keep `<Sidebar>` mounted inside an Activity boundary and change the boundary's `mode`:
7981

8082
```js
8183
<Activity mode={isShowingSidebar ? 'visible' : 'hidden'}>
8284
<Sidebar />
8385
</Activity>
8486
```
8587

86-
In this example, expand the sidebar, hide it, and show it again. The expanded state is preserved.
88+
When the boundary becomes visible again, `<Sidebar>` resumes with its previous state.
89+
90+
The following example has a sidebar with an expandable Overview section and a button that hides and shows the sidebar. To verify that its state is preserved, expand the Overview section, hide the sidebar, and then show it again. The Overview section remains expanded.
8791

8892
<Sandpack>
8993

@@ -177,7 +181,7 @@ Changing `mode` preserves the state of the children. Removing the boundary or ch
177181

178182
### Preserving DOM state while content is hidden {/*restoring-the-dom-of-hidden-components*/}
179183

180-
An Activity boundary also preserves state held by the browser in DOM nodes.
184+
Because Activity boundaries hide their children without removing them, the children's DOM is also preserved. This is useful for maintaining state held by the browser in DOM nodes.
181185

182186
The Contact tab in the following examples contains an uncontrolled `<textarea>`. Enter a draft, switch to the Home tab, and then return to Contact to compare the behavior with and without Activity.
183187

@@ -349,7 +353,80 @@ b {
349353

350354
</Recipes>
351355

352-
Activity also preserves other browser-managed state, such as scroll position and media playback position. In this example, play the video, hide it, and then show it again. When you play it again, the video continues from the same position.
356+
Activity also preserves other browser-managed state, such as scroll position and media playback position. The following examples use the same video player to compare removing and hiding a `<video>` element. Play the video, hide it, show it again, and then select **Play**.
357+
358+
<Recipes titleText="The difference between removing and hiding a video" titleId="examples-preserving-media-state">
359+
360+
#### Removing the video resets its playback position {/*removing-the-video-resets-its-playback-position*/}
361+
362+
This version conditionally renders the video player. Hiding it removes the `<video>` element from the DOM, so playback starts from the beginning when you show it again.
363+
364+
<Sandpack>
365+
366+
```js src/App.js active
367+
import { useState } from 'react';
368+
import VideoPlayer from './VideoPlayer.js';
369+
370+
export default function App() {
371+
const [isShowingVideo, setIsShowingVideo] = useState(true);
372+
373+
return (
374+
<>
375+
<button onClick={() => setIsShowingVideo(showing => !showing)}>
376+
{isShowingVideo ? 'Hide' : 'Show'} video
377+
</button>
378+
{isShowingVideo && <VideoPlayer />}
379+
</>
380+
);
381+
}
382+
```
383+
384+
```js src/VideoPlayer.js hidden
385+
import { useLayoutEffect, useRef } from 'react';
386+
387+
export default function VideoPlayer() {
388+
const ref = useRef(null);
389+
390+
useLayoutEffect(() => {
391+
const video = ref.current;
392+
return () => video.pause();
393+
}, []);
394+
395+
return (
396+
<div className="video-player">
397+
<button onClick={() => ref.current.play()}>Play</button>
398+
<button onClick={() => ref.current.pause()}>Pause</button>
399+
<video ref={ref} width="250">
400+
<source
401+
src="https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4"
402+
type="video/mp4"
403+
/>
404+
</video>
405+
</div>
406+
);
407+
}
408+
```
409+
410+
```css
411+
.video-player {
412+
margin-top: 10px;
413+
}
414+
.video-player button {
415+
margin-right: 10px;
416+
}
417+
.video-player video {
418+
display: block;
419+
margin-top: 10px;
420+
}
421+
```
422+
423+
</Sandpack>
424+
425+
<Solution />
426+
427+
#### Hiding the video preserves its playback position {/*hiding-the-video-preserves-its-playback-position*/}
428+
429+
This version renders the video player inside an Activity boundary. Hiding it preserves the `<video>` element, so playback continues from the same position when you show it and select **Play** again.
353430

354431
<Sandpack>
355432

@@ -414,7 +491,11 @@ export default function VideoPlayer() {
414491

415492
</Sandpack>
416493

417-
The ref provides access to the `<video>` DOM node; it does not store the playback position. The `useLayoutEffect` cleanup pauses the video when the boundary becomes hidden. Activity preserves the DOM node and its playback position, but React detaches the ref while the boundary is hidden and reconnects it when the boundary becomes visible. See [Troubleshooting](#my-hidden-component-keeps-playing-audio-or-video) for more about cleaning up browser-managed behavior.
494+
<Solution />
495+
496+
</Recipes>
497+
498+
The ref provides access to the `<video>` DOM node. It does not store the playback position. The `useLayoutEffect` cleanup pauses the video when the player is removed or its Activity boundary becomes hidden. Activity preserves the DOM node and its playback position, but React detaches the ref while the boundary is hidden and reconnects it when the boundary becomes visible. See [Troubleshooting](#my-hidden-component-keeps-playing-audio-or-video) for more about cleaning up browser-managed behavior.
418499

419500
---
420501

@@ -430,7 +511,207 @@ An Activity boundary can also prepare content before the user sees it. Content i
430511
</Suspense>
431512
```
432513

433-
If `Posts` suspends while reading code or data, React continues rendering the rest of the page. If the hidden work finishes before the user selects Posts, React can reveal it without showing the Suspense fallback. Otherwise, the nearest fallback appears until the code or data is ready.
514+
If `Posts` suspends while reading code or data, React continues rendering the rest of the page while the hidden work proceeds.
515+
516+
The following examples compare rendering the Posts tab on demand with pre-rendering it in a hidden Activity boundary. Both versions read the same cached Promise with [`use`](/reference/react/use).
517+
518+
<Recipes titleText="The difference between rendering and pre-rendering a tab" titleId="examples-pre-rendering">
519+
520+
#### Rendering the tab on demand shows a fallback {/*rendering-the-tab-on-demand-shows-a-fallback*/}
521+
522+
This version does not mount `<Posts>` until its tab is active. Select **Posts** to see the Suspense fallback while the data loads.
523+
524+
<Sandpack>
525+
526+
```js src/App.js active
527+
import { Suspense, useState } from 'react';
528+
import Home from './Home.js';
529+
import Posts from './Posts.js';
530+
531+
export default function App() {
532+
const [activeTab, setActiveTab] = useState('home');
533+
534+
return (
535+
<>
536+
<div aria-label="Profile sections" role="group">
537+
<button
538+
aria-pressed={activeTab === 'home'}
539+
onClick={() => setActiveTab('home')}
540+
>
541+
Home
542+
</button>
543+
<button
544+
aria-pressed={activeTab === 'posts'}
545+
onClick={() => setActiveTab('posts')}
546+
>
547+
Posts
548+
</button>
549+
</div>
550+
551+
<Suspense fallback={<h1>Loading posts...</h1>}>
552+
{activeTab === 'home' && <Home />}
553+
{activeTab === 'posts' && <Posts />}
554+
</Suspense>
555+
</>
556+
);
557+
}
558+
```
559+
560+
```js src/Posts.js
561+
import { use } from 'react';
562+
import { getPosts } from './data.js';
563+
564+
export default function Posts() {
565+
const posts = use(getPosts());
566+
567+
return (
568+
<ul>
569+
{posts.map(post => (
570+
<li key={post.id}>{post.title}</li>
571+
))}
572+
</ul>
573+
);
574+
}
575+
```
576+
577+
```js src/Home.js hidden
578+
export default function Home() {
579+
return <p>Welcome to my profile!</p>;
580+
}
581+
```
582+
583+
```js src/data.js hidden
584+
let postsPromise;
585+
586+
export function getPosts() {
587+
if (!postsPromise) {
588+
postsPromise = loadPosts();
589+
}
590+
return postsPromise;
591+
}
592+
593+
async function loadPosts() {
594+
// Add a delay so that loading is easier to observe.
595+
await new Promise(resolve => setTimeout(resolve, 1500));
596+
597+
return Array.from({ length: 5 }, (_, index) => ({
598+
id: index,
599+
title: 'Post #' + (index + 1),
600+
}));
601+
}
602+
```
603+
604+
```css
605+
button {
606+
margin-right: 8px;
607+
}
608+
```
609+
610+
</Sandpack>
611+
612+
<Solution />
613+
614+
#### Pre-rendering the tab can avoid the fallback {/*pre-rendering-the-tab-can-avoid-the-fallback*/}
615+
616+
This version renders both tabs inside Activity boundaries. React begins rendering `<Posts>` while its boundary is hidden. Wait briefly before selecting **Posts**. The list appears immediately because the hidden render started loading it in the background.
617+
618+
<Sandpack>
619+
620+
```js src/App.js active
621+
import { Activity, Suspense, useState } from 'react';
622+
import Home from './Home.js';
623+
import Posts from './Posts.js';
624+
625+
export default function App() {
626+
const [activeTab, setActiveTab] = useState('home');
627+
628+
return (
629+
<>
630+
<div aria-label="Profile sections" role="group">
631+
<button
632+
aria-pressed={activeTab === 'home'}
633+
onClick={() => setActiveTab('home')}
634+
>
635+
Home
636+
</button>
637+
<button
638+
aria-pressed={activeTab === 'posts'}
639+
onClick={() => setActiveTab('posts')}
640+
>
641+
Posts
642+
</button>
643+
</div>
644+
645+
<Suspense fallback={<h1>Loading posts...</h1>}>
646+
<Activity mode={activeTab === 'home' ? 'visible' : 'hidden'}>
647+
<Home />
648+
</Activity>
649+
<Activity mode={activeTab === 'posts' ? 'visible' : 'hidden'}>
650+
<Posts />
651+
</Activity>
652+
</Suspense>
653+
</>
654+
);
655+
}
656+
```
657+
658+
```js src/Posts.js
659+
import { use } from 'react';
660+
import { getPosts } from './data.js';
661+
662+
export default function Posts() {
663+
const posts = use(getPosts());
664+
665+
return (
666+
<ul>
667+
{posts.map(post => (
668+
<li key={post.id}>{post.title}</li>
669+
))}
670+
</ul>
671+
);
672+
}
673+
```
674+
675+
```js src/Home.js hidden
676+
export default function Home() {
677+
return <p>Welcome to my profile!</p>;
678+
}
679+
```
680+
681+
```js src/data.js hidden
682+
let postsPromise;
683+
684+
export function getPosts() {
685+
if (!postsPromise) {
686+
postsPromise = loadPosts();
687+
}
688+
return postsPromise;
689+
}
690+
691+
async function loadPosts() {
692+
// Add a delay so that pre-rendering is easier to observe.
693+
await new Promise(resolve => setTimeout(resolve, 1500));
694+
695+
return Array.from({ length: 5 }, (_, index) => ({
696+
id: index,
697+
title: 'Post #' + (index + 1),
698+
}));
699+
}
700+
```
701+
702+
```css
703+
button {
704+
margin-right: 8px;
705+
}
706+
```
707+
708+
</Sandpack>
709+
710+
<Solution />
711+
712+
</Recipes>
713+
714+
If the user selects Posts before the hidden render finishes, the Suspense fallback appears until the data is ready. Pre-rendering can reduce the loading time, but it does not guarantee that the content will always be ready.
434715

435716
<Note>
436717

0 commit comments

Comments
 (0)