@@ -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
97109When 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
318331of the page. If that hidden work completes, switching to the Posts tab can reveal
319332the 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
323433Only 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
350469function 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
382542import { 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
414565Transition 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