How to Show and Hide Elements Based on a Condition in React
Advertisement
🧩 Scenario
Architecture Walkthrough
The Toggle Pattern
The core pattern is a boolean state variable that controls what renders. A button toggles the boolean, React re-renders, and the JSX renders or does not render the element based on the current state. This is the foundation for every show/hide interaction in React: modals, drawers, tooltips, accordions, and notifications.
The state variable is owned by the component that needs to control the visibility. If both the trigger (button) and the controlled element are in the same component, the state lives there. If they are in different components, lift the state to the nearest common ancestor.
Unmount vs CSS Hide: The Key Decision
Conditional rendering (&& or ternary returning null) unmounts the component entirely. Its DOM node is removed, its local state is reset, and its effects and subscriptions are cleaned up. The next time it appears, it is a fresh instance. This is the right choice for most toggle scenarios: modals, confirmation dialogs, temporary panels.
CSS visibility (display: none or visibility: hidden via className) keeps the component mounted. Its DOM node exists in the document, its local state is preserved, its effects continue running, and transitions can animate in and out. Use this when the hidden element must retain state between shows (a video player that should not restart, a form that should not lose its values, an element that should transition smoothly with CSS animations).
Key Code Explained
// Pattern 1: conditional rendering — element is unmounted when hidden
import { useState } from 'react';
function TogglePanel() {
const [isOpen, setIsOpen] = useState(false);
return (
<div>
<button
onClick={() => setIsOpen((prev) => !prev)} // functional updater for correctness
aria-expanded={isOpen} // accessibility
>
{isOpen ? 'Hide Details' : 'Show Details'}
</button>
{/* Unmounted when isOpen is false — local state resets on re-open */}
{isOpen && (
<div className="panel">
<p>This panel is unmounted when hidden. Each open is a fresh render.</p>
</div>
)}
</div>
);
}
// Pattern 2: CSS hide — element stays mounted
function PersistentPanel() {
const [isOpen, setIsOpen] = useState(false);
return (
<div>
<button
onClick={() => setIsOpen((prev) => !prev)}
aria-expanded={isOpen}
>
Toggle Panel
</button>
{/* Always mounted — only visibility changes. Local state is preserved. */}
<div
className={isOpen ? 'panel panel-visible' : 'panel panel-hidden'}
aria-hidden={!isOpen}
>
<p>This panel stays mounted. Use for video players, forms, animated drawers.</p>
</div>
</div>
);
}
// Pattern 3: accordion — multiple items, one open at a time
interface AccordionItem {
id: string;
question: string;
answer: string;
}
function Accordion({ items }: { items: AccordionItem[] }) {
const [openId, setOpenId] = useState<string | null>(null);
const toggle = (id: string) =>
setOpenId((prev) => (prev === id ? null : id));
return (
<div className="accordion">
{items.map((item) => (
<div key={item.id} className="accordion-item">
<button
onClick={() => toggle(item.id)}
aria-expanded={openId === item.id}
>
{item.question}
</button>
{openId === item.id && (
<div className="accordion-body">
<p>{item.answer}</p>
</div>
)}
</div>
))}
</div>
);
}
// Pattern 4: modal toggle
function ModalExample() {
const [isModalOpen, setIsModalOpen] = useState(false);
return (
<>
<button onClick={() => setIsModalOpen(true)}>Open Modal</button>
{isModalOpen && (
<div className="modal-overlay" onClick={() => setIsModalOpen(false)}>
<div className="modal" onClick={(e) => e.stopPropagation()}>
<h2>Modal Title</h2>
<p>Modal content here.</p>
<button onClick={() => setIsModalOpen(false)}>Close</button>
</div>
</div>
)}
</>
);
}
The accordion example uses a single openId state variable rather than a boolean array. This automatically closes the previously open item when a new one is clicked, with no additional logic. The pattern setOpenId(prev => prev === id ? null : id) handles both open (set to id) and close (set to null) in one expression.
Tradeoffs
| Approach | DOM presence | Local state | Effects run | Use when |
|---|---|---|---|---|
| Conditional rendering (null) | Removed | Reset | Cleaned up | Modal, dialog, notification — fresh each time |
| CSS hide (display: none) | Present | Preserved | Continue | Video player, form, animated drawer — keep state |
What Interviewers Actually Check
- Whether you use
useState+ conditional rendering or CSS for visibility - Whether you know the difference between unmounting and CSS hiding and when to use each
- Whether you use a functional updater
(prev => !prev)for toggle state - Whether you add
aria-expandedon toggle buttons for accessibility - Whether you know that conditional rendering resets local state on re-mount
Follow-Up Questions
- How would you implement a modal that traps focus inside when open and returns focus to the trigger when closed, for accessibility?
- How would you animate a panel in and out using Framer Motion while still unmounting it after the exit animation completes?
- How does
useTransitionin React 18 help with showing/hiding heavy components without blocking the UI thread? - How would you synchronize a sidebar open state with the URL (so the page reloads with the sidebar open)?
- How would you implement a keyboard-dismissible panel (Escape key closes it) using
useEffectandaddEventListener?
Common Candidate Mistakes
- Using
display: nonefor every show/hide scenario without understanding that the component is still mounted, effects run, and data fetching continues - Using conditional rendering for a component that must preserve state between shows (like a form the user partially filled), causing all values to reset
- Writing
setIsOpen(!isOpen)instead ofsetIsOpen(prev => !prev), which reads stale state inside closures likesetTimeoutor event handlers - Not adding
aria-expandedon the toggle button, leaving screen reader users without feedback about the current state - Not stopping propagation on a modal's inner click when the overlay click closes the modal, causing the modal to close when any content inside is clicked
Interview Readiness Checklist
Before you leave this question, make sure you can answer:
- Can you implement a toggle with
useStateand&&to show/hide an element? - Can you explain the difference between unmounting (conditional rendering) and hiding (CSS)?
- Can you use a functional updater
(prev => !prev)for toggle state? - Can you implement an accordion where only one item is open at a time?
- Can you explain that conditional rendering resets all local component state on re-mount?
Summary
Show/hide in React is a boolean useState variable that controls what renders. A button toggles the boolean, React re-renders, and conditional rendering or CSS determines whether the element appears. The choice between the two is a meaningful design decision: conditional rendering unmounts the component, resets all its local state, and cleans up its effects. CSS hiding keeps the component fully mounted, preserving state and keeping effects active.
Use conditional rendering (null) for most toggle scenarios: modals, notifications, tooltips, and temporary panels that should be a clean fresh instance each time they appear. Use CSS hiding for components that must preserve internal state between shows: video players that should not restart, forms the user might partially fill, or drawers that animate in and out with CSS transitions.
The functional updater (prev => !prev) is the correct pattern for toggle state. It reads the latest state rather than the potentially stale closure value, which matters in async contexts and event listeners set up in useEffect.
Should I use CSS display:none or conditional rendering to hide elements?
It depends on what the hidden element does. Use conditional rendering (null) when the element should not exist in the DOM, should not run effects, and should be considered destroyed. Use CSS when the element must stay mounted (to preserve state, subscriptions, or animations) but should be invisible.
Advertisement
Stay Updated
Get the latest frontend challenges, interview questions and tutorials delivered to your inbox.
Advertisement