Introduction
Usually, when we want to hide our components in React we use conditional rendering, or css display: none. If we use conditional rendering, the states will be lost when it is hidden because the component will unmount. React 18 introduces <Activity> component, a new feature designed to manage the visibility, especially for elements that are frequently toggled.
function App() {
const [visible, setVisible] = useState(false)
return (
{/* Conditional Rendering*/}
{visible && <p>Content</>}
{/* or CSS Styling */}
<p style={{display: visible ? 'block': 'none'}}>
Content
</p>
)
}
How It Works?
The <Activity> component allows you to manage the visibility of its children by using the mode prop.
<Activity mode={isShowingSidebar ? 'visible' : 'hidden'}>
<Sidebar />
</Activity>
In my opinion, this is cleaner than conditional rendering, or conditional CSS styling.
- When
mode='hidden', React hides the component by applyingdisplay: "none"on CSS. - It will also destroys their Effects and clean up any subscriptions.
- But the children will still re-render at a lower priority, if there are new props.
- When the mode changes to visible again, React will re-create the Effects and restore their previous states.
Benefits
State Preservation
If we use conditional rendering, the state is lost when the component unmounts. We can fix this by moving the state to the parent component. But this breaks encapsulation. It would be better if the state is kept inside the component. The new <Activity> component enables us to do this.
DOM State Preservation
Because React applies display: "none" on CSS, the state of the DOM is preserved. Example, the user’s input is not lost when the component unmounts.
Pre-Rendering Content
The content that will become visible later, is pre-rendered. Because React renders the children at a lower priority when the mode is hidden.
Speeding Up Page Interaction On Load for SSR
React uses Selective Hydration under-the-hood. It works by hydrating your app’s HTML in chunks, allowing early interactions on some components even-though your whole code is not yet finished loading.
type Tab = 'Home' | 'HeavyComponent'
function Page() {
const [selected, setSelected] = useState<Tab>('Home')
return (
<>
<div className='tab-menu'>
<button onClick={() => setSelected('Home')}>
Home
</button>
<button onClick={() => setSelected('HeavyComponent')}>
HeavyComponent
</button>
</div>
{selected === 'Home' && <Home />}
{selected === 'HeavyComponent' && <HeavyComponent />}
</>
)
}
export default App
If we use conditional rendering, React doesn’t utilize Selective Hydration to separate our components into different chunks. It must hydrate the whole page which could lead to unresponsive tab buttons.
type Tab = 'Home' | 'HeavyComponent'
function Page() {
const [selected, setSelected] = useState<Tab>('Home')
return (
<>
<div className='tab-menu'>
<button onClick={() => setSelected('Home')}>
Home
</button>
<button onClick={() => setSelected('HeavyComponent')}>
HeavyComponent
</button>
</div>
<Activity mode={selected === 'Home' ? 'visible' : 'hidden'}>
<Home />
</Activity>
<Activity mode={selected === 'HeavyComponent' ? 'visible' : 'hidden'}>
<HeavyComponent />
</Activity>
</>
)
}
export default App
This way React can separately hydrate tab menu, home and heavy component. The tab menu could be interactable first before the heavy component finishes loading.
Demo
When Not to Use
- Permanent removal: This is obvious. If we want to remove it completely why should it linger in memory?
- When the component changes frequently and performance is critical: Although it re-renders at a lower priority, it still consumes resources.
- When the component’s effect should always run when visible, regardless of previous state: If you want to run the effects from a “fresh” state, you might prefer a full unmount and remount.