skip to content

In React, what actually changes if you write `{isOpen && <Panel />}` instead of always rendering `<Panel hidden={!isOpen} />`, and how do you choose between them?

level: seniorimportance: should knowfreq 44%

answer

  1. one removes, the other only hides
  2. what happens to the state on close
  3. cleanups run, effects keep running
  4. fresh on reopen or preserved
  5. exit animation needs it to survive

basics

~20 s

The conditional version removes Panel from the tree, so it unmounts: its state and refs are discarded and its effect cleanups run, and reopening remounts it fresh. The hidden version keeps it mounted, preserving state and DOM but paying render and memory cost while invisible.

solid answer

~50 s

`{isOpen && <Panel />}` is a **mount/unmount** toggle. When the condition goes false, React removes the subtree: component state and refs are thrown away, effect cleanups run, the DOM nodes are destroyed, and reopening runs the whole mount path again — so scroll position, focus, uncontrolled input values and any in-flight work are lost. `<Panel hidden={!isOpen} />` keeps it **mounted**: state survives, the DOM nodes stay, and its effects, timers and subscriptions keep running while it is invisible. I choose on what the panel costs and what it holds. Heavy, rarely-opened, or stale-on-reopen content should unmount, so the memory and the subscriptions go away. Content with expensive-to-rebuild state — a half-filled form, a loaded editor, a virtualised list's scroll position — or anything that needs an exit animation should stay mounted. And if I want the fresh-start behaviour explicitly, unmounting is the honest way to get it rather than resetting state by hand.

code

jsx · 9 lines
jsx
function Drawer({ isOpen, children }) {
  // unmounts on close: state discarded, cleanups run
  return <aside>{isOpen && children}</aside>;
}

function KeptDrawer({ isOpen, children }) {
  // stays mounted: state, DOM and running effects survive
  return <aside hidden={!isOpen}>{children}</aside>;
}

go deeper

for a junior

Know that {isOpen && <Panel />} removes the component entirely, so it starts over the next time it appears, while hiding it keeps it alive with its state intact.

for a middle

Explain the unmount consequences precisely — state and refs discarded, effect cleanups run, DOM removed, mount effects replayed — and name what users notice: cleared forms, lost scroll position, restarted requests.

for a senior

Decide with reasons: unmount heavy or should-be-fresh content to get teardown for free; keep expensive-to-rebuild state mounted; note that a hidden subtree still renders and its effects still run. Mention the accessibility trap of hiding without removing the element from the accessibility tree.

for a principal

Set the default and the exception: pick one convention for overlays and panels so state lifetime is predictable, and push state whose loss would hurt the user out of the toggled subtree entirely, so the mount/unmount choice stops being load-bearing.

## Two different operations that look like the same feature Both spellings make a panel appear and disappear, and in a demo they are indistinguishable. They are not the same operation, and the difference is the whole question. ```jsx // A: removes the subtree from the React tree <aside>{isOpen && <Panel />}</aside> // B: keeps the subtree, hides the DOM node <aside hidden={!isOpen}><Panel /></aside> ``` ## What A costs and buys When the condition flips to false, `Panel` is no longer part of the rendered output, so React unmounts it. Concretely: - **State is discarded.** Anything the component held is gone; reopening starts from the initial values. - **Refs are detached.** Ref objects are cleared and callback refs run their detach path. - **Effect cleanups run.** Subscriptions are torn down, intervals cleared, listeners removed — provided the effects were written with cleanups. This is the main *benefit*: an unmount is a free, correct teardown. - **DOM nodes are removed.** Memory drops, and anything the browser was doing for those nodes — layout, images, media, IntersectionObserver targets — stops. - **Reopening replays the mount path.** Initial state, mount effects, and any fetch those effects start. Whether that is desirable or a stutter depends entirely on how heavy the panel is. The corollaries people get bitten by: a form loses what the user typed; a list loses its scroll offset; an uncontrolled input loses its DOM value; a component mid-request restarts it; and there is no element left in the tree to animate on the way out, which is why exit animations need the element to survive until the animation ends. ## What B costs and buys Always rendering and toggling visibility keeps everything alive. State persists across close/open with no work from you, the DOM nodes are already laid out so reopening is instantaneous, and an exit transition has something to run on. The costs are real and easy to forget: - **It still renders.** The subtree participates in every render of its parent; hiding is a paint concern, not a render one. - **Its effects keep running.** Timers, polling, subscriptions and observers inside a hidden panel are still live. A hidden dashboard widget polling every five seconds is a classic invisible bug. - **The DOM is still there.** Memory, and any per-node browser work, persists — which matters when the hidden thing is a ten-thousand-row table. - **Accessibility depends on getting the hiding right.** The `hidden` attribute hides the element from rendering and from assistive technology, but it is applied by a user-agent style rule, so any CSS that sets `display` on that element overrides it and the content becomes visible again. If you hide with your own class instead, make sure it genuinely removes the element from the accessibility tree rather than merely moving it off-screen or setting opacity, or keyboard users will tab into invisible controls. ## The decision, stated as a rule Ask two questions. **What does it hold?** If the panel accumulates state the user would resent losing — a partially filled form, an editor with an undo stack, a scrolled list — keep it mounted. If its content should be fresh every time it opens, unmount it, because the remount *is* the reset and it costs you no reset code. **What does it cost while closed?** If it is heavy in DOM, memory, or ongoing work (polling, sockets, observers), unmount it: the cleanup you get for free is worth the remount latency. If it is small and reopened constantly, keeping it mounted removes a stutter for a trivial cost. A third consideration decides the remaining cases: if it needs to animate out, it has to still be in the tree while it animates, so it stays mounted until the transition finishes, and only then is it removed. ## The mistake worth calling out The wrong version of this answer is "conditional rendering is faster because it renders less". Sometimes it is, and sometimes it is much slower, because you have converted a cheap visibility toggle into a full mount — new elements, new effects, possibly a new fetch — on every open. The honest framing is that the two spellings have different *semantics*, and performance follows from which semantics fit the content. Choosing the mount/unmount toggle for a heavyweight panel that opens twice a session is right; choosing it for a tooltip that opens on every hover is not. ## A senior tell The candidates who have shipped this reach for the state consequence first, not the performance one: "if I unmount it, the form clears — is that what we want?" That question is the design decision. The performance discussion is downstream of it.

  • A panel is kept mounted but hidden. What surprises teams about its effects?
    They keep running. Hiding is a rendering and visibility concern; React has no notion of a paused subtree here. A hidden widget polling an endpoint, holding a socket, or running an animation frame loop continues to do all of it, burning network and battery for something nobody can see. If that matters, gate the work on the same flag you use to hide, or unmount instead.
  • Is `hidden` enough for accessibility, or do you need something else?
    The `hidden` attribute does remove the element from rendering and from the accessibility tree, but it works through a user-agent style rule, so any CSS that sets `display` on that element defeats it and the content comes back — visible and focusable. If you hide with your own class, make sure it actually removes the element from the accessibility tree rather than using opacity or off-screen positioning, otherwise keyboard users tab into invisible controls.
  • For a modal dialog, which would you pick?
    Usually unmount. A modal's content should be fresh each time it opens, it is often heavy relative to how often it is used, and unmounting gives you teardown of its effects for free. The exception is a modal holding a long form you want to reopen mid-edit — then it stays mounted, or its draft state is lifted somewhere that outlives it.

saying these in an interview costs you the question

  • Says conditional rendering is always the faster option
  • Assumes a hidden component's timers stop running
  • Expects state to survive an unmount and remount
  • Thinks hiding with CSS also skips the render work
  • Treats opacity: 0 as equivalent to being hidden

context