I9 · Part 2 · Intermediate
Error boundaries, portals & keys in depth
Containing a crash, escaping the DOM tree, and using keys on purpose.
07On this page
Why one broken component blanks the page
Since React 16, an error thrown during render that nothing catches unmounts the entire tree. That is deliberate: a half-rendered UI showing stale or wrong data is considered worse than showing nothing. So you have to decide where the damage stops, and that is an error boundary.
Error boundaries are still classes
class ErrorBoundary extends React.Component {
state = { error: null };
static getDerivedStateFromError(error) {
return { error }; // render the fallback
}
componentDidCatch(error, info) {
logToService(error, info.componentStack); // report it
}
render() {
if (this.state.error) return this.props.fallback;
return this.props.children;
}
}
There is no hook equivalent, which is the one remaining reason to write a
class in React. In practice you write it once, or install
react-error-boundary and get a reset function with it.
What they do and do not catch
| Caught | Not caught |
|---|---|
| Errors during render | Errors in event handlers |
| Errors in lifecycle methods | Errors in setTimeout or a promise |
| Errors in constructors of the tree below | Errors in the boundary itself |
| Server-side rendering errors |
Handlers and async code are not caught because React is not on the stack when
they run — there is no render to abandon. Use try/catch there
and set error state yourself.
Where to put them
<ErrorBoundary fallback={<FullPageError />}> // last resort
<Layout>
<ErrorBoundary fallback={<WidgetError />}> // one panel fails alone
<RevenueChart />
</ErrorBoundary>
<Outlet />
</Layout>
</ErrorBoundary>
Granularity is the whole design decision. One boundary at the root turns any bug into a blank page; a boundary around each independent panel means a broken chart leaves the rest of the dashboard usable. Put them where a failure is survivable.
Give the fallback a way out — a retry button that resets the boundary's state, or a link home. A dead end with "Something went wrong" is barely better than the blank page.
Portals: rendering somewhere else in the DOM
createPortal(children, domNode)
function Modal({ children, onClose }) {
return createPortal(
<div className="overlay" onClick={onClose}>
<div className="modal">{children}</div>
</div>,
document.body
);
}
The modal is still a child in the React tree — it receives context, its state belongs to its parent, and events bubble to its React parent, not its DOM parent. Only the DOM placement changes.
The problem portals actually solve
A modal with z-index: 9999 can still render behind a header if
an ancestor created a stacking context — which happens with
transform, opacity below 1, filter or
will-change, not just position. No z-index inside
that context can escape it. The same applies to
overflow: hidden clipping a dropdown. A portal moves the node
out of the offending subtree entirely.
A modal is not just a portal
The DOM placement is the easy part. A correct dialog also needs focus moved in
on open, focus trapped while it is open, focus returned to the trigger on
close, Escape to dismiss, the background made inert, and
role="dialog" aria-modal="true" with a label.
<dialog ref={ref} onClose={onClose}> // the browser does most of it
The native <dialog> element with
showModal() handles focus, Escape, inertness and the top layer
for free — and being in the top layer means it does not need a portal at
all. Worth reaching for before a library.
Keys as a deliberate tool
Beyond lists, changing a key is how you tell React "this is a
different instance now". React unmounts the old component and mounts a fresh
one, resetting all of its state.
<ProfileForm key={userId} user={user} /> // switching user clears the draft
<ErrorBoundary key={location.pathname}> // navigating clears a caught error
<Chart key={dataVersion} data={data} /> // force a third-party widget to rebuild
Each of these replaces an effect that watched a value and reset something. Fewer moving parts, and no render with the stale value first.
The cost is real: everything below unmounts, so scroll position, focus and any in-flight work in that subtree are discarded. Use it when you want exactly that, not as a way to force a refresh.
Interview answer, one sentence
"An uncaught render error unmounts the whole tree, so boundaries are how you
decide where a failure stops — per panel, not just at the root, and they
catch render errors but never handlers or async code. Portals change the DOM
parent without changing the React parent, which is how you escape a stacking
context or an overflow: hidden."
Opens in the editor: write it, run it, and check it against real tests.
All 66 for this level →Finished reading?
0 of 57 chapters read in this series