Skip to content

Commit b84d873

Browse files
committed
Add distinct Activity state examples
1 parent 0541585 commit b84d873

1 file changed

Lines changed: 183 additions & 7 deletions

File tree

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

Lines changed: 183 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ Activity is useful when part of the UI may become hidden and visible again. Unli
6767

6868
Use an Activity boundary when preserving that work is valuable—for example, for a tab the user is likely to revisit or a panel that can prepare data in the background. A hidden boundary retains its state and DOM nodes, so it continues using memory. If the content is unlikely to become visible again, conditionally rendering it may be preferable because unmounting allows React and the browser to release its resources.
6969

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

7272
When this condition becomes false, React removes `<Sidebar>` from the tree and discards its state:
7373

@@ -105,7 +105,7 @@ export default function App() {
105105
<button
106106
aria-controls='documentation-sidebar'
107107
aria-expanded={isShowingSidebar}
108-
onClick={() => setIsShowingSidebar(s => !s)}
108+
onClick={() => setIsShowingSidebar(showing => !showing)}
109109
>
110110
{isShowingSidebar ? 'Hide' : 'Show'} sidebar
111111
</button>
@@ -119,14 +119,11 @@ function Sidebar() {
119119
const [isExpanded, setIsExpanded] = useState(false);
120120

121121
return (
122-
<nav
123-
aria-label='Documentation'
124-
id='documentation-sidebar'
125-
>
122+
<nav aria-label='Documentation' id='documentation-sidebar'>
126123
<button
127124
aria-controls='overview-sections'
128125
aria-expanded={isExpanded}
129-
onClick={() => setIsExpanded(e => !e)}
126+
onClick={() => setIsExpanded(expanded => !expanded)}
130127
>
131128
Overview
132129
<span aria-hidden='true' className='indicator'>
@@ -172,6 +169,185 @@ Changing `mode` preserves the state of the children. Removing the boundary or ch
172169

173170
---
174171

172+
### Preserving state while navigating {/*preserving-state-while-navigating*/}
173+
174+
When this condition becomes false, React removes `<VideoList>` from the tree and discards its state:
175+
176+
```js
177+
{selectedVideo === null && <VideoList />}
178+
```
179+
180+
Render the component inside an Activity boundary to preserve its state while it is hidden:
181+
182+
```js
183+
<Activity mode={selectedVideo === null ? 'visible' : 'hidden'}>
184+
<VideoList />
185+
</Activity>
186+
```
187+
188+
In this example, filter the video list, open a result, and then go back. The search text and filtered results are preserved because the list remains mounted inside the hidden Activity boundary.
189+
190+
<Sandpack>
191+
192+
```js
193+
import { Activity, useState } from 'react';
194+
195+
const videos = [
196+
{
197+
id: 1,
198+
title: 'React Keynote',
199+
description: 'The latest news from the React team.',
200+
},
201+
{
202+
id: 2,
203+
title: 'Building with Actions',
204+
description: 'Handle mutations and pending states with Actions.',
205+
},
206+
{
207+
id: 3,
208+
title: 'Animating View Transitions',
209+
description: 'Create polished transitions between screens.',
210+
},
211+
{
212+
id: 4,
213+
title: 'Understanding Server Components',
214+
description: 'Render components ahead of time on the server.',
215+
},
216+
];
217+
218+
export default function App() {
219+
const [selectedVideo, setSelectedVideo] = useState(null);
220+
221+
return (
222+
<main>
223+
<Activity
224+
mode={selectedVideo === null ? 'visible' : 'hidden'}
225+
>
226+
<VideoList onSelect={setSelectedVideo} />
227+
</Activity>
228+
229+
{selectedVideo !== null && (
230+
<VideoDetails
231+
onBack={() => setSelectedVideo(null)}
232+
video={selectedVideo}
233+
/>
234+
)}
235+
</main>
236+
);
237+
}
238+
239+
function VideoList({ onSelect }) {
240+
const [searchText, setSearchText] = useState('');
241+
const visibleVideos = videos.filter(video => {
242+
const text = `${video.title} ${video.description}`;
243+
return text.toLowerCase().includes(searchText.toLowerCase());
244+
});
245+
246+
return (
247+
<section>
248+
<h1>Videos</h1>
249+
<label htmlFor='search'>Search videos</label>
250+
<input
251+
id='search'
252+
onChange={event => setSearchText(event.target.value)}
253+
placeholder='Try "React"'
254+
type='search'
255+
value={searchText}
256+
/>
257+
258+
<ul className='videos'>
259+
{visibleVideos.map(video => (
260+
<li key={video.id}>
261+
<button onClick={() => onSelect(video)}>
262+
<strong>{video.title}</strong>
263+
<span>{video.description}</span>
264+
</button>
265+
</li>
266+
))}
267+
</ul>
268+
</section>
269+
);
270+
}
271+
272+
function VideoDetails({ video, onBack }) {
273+
return (
274+
<section>
275+
<button className='back' onClick={onBack}>
276+
Back
277+
</button>
278+
<div className='thumbnail' aria-hidden='true'>
279+
▶
280+
</div>
281+
<h1>{video.title}</h1>
282+
<p>{video.description}</p>
283+
</section>
284+
);
285+
}
286+
```
287+
288+
```css
289+
body {
290+
margin: 0;
291+
padding: 16px;
292+
font-family: system-ui;
293+
}
294+
main {
295+
max-width: 520px;
296+
}
297+
label,
298+
input {
299+
display: block;
300+
}
301+
input {
302+
box-sizing: border-box;
303+
margin: 8px 0 16px;
304+
padding: 8px;
305+
width: 100%;
306+
}
307+
.videos {
308+
display: grid;
309+
gap: 8px;
310+
list-style: none;
311+
padding: 0;
312+
}
313+
.videos button {
314+
background: #f5f5f5;
315+
border: 1px solid #ddd;
316+
border-radius: 8px;
317+
cursor: pointer;
318+
padding: 12px;
319+
text-align: left;
320+
width: 100%;
321+
}
322+
.videos strong,
323+
.videos span {
324+
display: block;
325+
}
326+
.videos span {
327+
color: #555;
328+
margin-top: 4px;
329+
}
330+
.back {
331+
margin-bottom: 12px;
332+
}
333+
.thumbnail {
334+
align-items: center;
335+
aspect-ratio: 16 / 9;
336+
background: #282c34;
337+
border-radius: 8px;
338+
color: white;
339+
display: flex;
340+
font-size: 48px;
341+
justify-content: center;
342+
}
343+
```
344+
345+
</Sandpack>
346+
347+
The search field is controlled by `VideoList` state. Hiding the list preserves that React state when the user opens a video. The next example shows a different case: state stored by the browser in an uncontrolled field.
348+
349+
---
350+
175351
### Preserving DOM state while content is hidden {/*restoring-the-dom-of-hidden-components*/}
176352

177353
An Activity boundary also preserves state held by the browser in DOM nodes. This includes an uncontrolled input's current value, scroll position, and media playback position.

0 commit comments

Comments
 (0)