MENU
Component Design Patterns
Everything up to here has been mechanism: how state updates, how effects run, how a ref reaches a DOM node. This section is about judgment â where to draw component boundaries, how to let one component accept another's markup without knowing what it is, and which escape hatches React offers when the tree model is not enough.The bias is worth stating plainly: React has no inheritance story and does not want one. A component is a function, and the way to reuse a function is to call it, pass it arguments, or hand it other functions. Every pattern below is a shape of that idea: containment passes markup as an argument, specialization pre-fills arguments, higher-order components and render props pass and receive functions. Even error boundaries and portals â where React deliberately breaks the parent-child rules â are configured by composition.Two of the nine patterns are historical â higher-order components and render props, both replaced by custom Hooks but still common in libraries and in code written before 2019. Three others, error boundaries, portals and Strict Mode, belong in the skeleton of essentially every production application.2.11.1 Thinking in React
Given a mockup and a JSON feed, experienced React developers follow the same five steps. Learn them as a routine: each answers a question you would otherwise answer by guessing.Step 1 â break the UI into a component hierarchy. Draw boxes on the mockup and name them, applying the single responsibility principle: if a box does two unrelated things, split it. A well-shaped JSON model and a good component tree tend to match, so the data is a guide.
Figure 2.39: Boxes drawn on the mockup become the component treeStep 2 â build a static version. Write the whole
tree with props only and no state at all. This step is typing rather
than thinking, which is the point: it separates "does the markup work"
from "does the interaction work."Step 3 â find the minimal state. Ask three questions
of every piece of data on screen. Does it stay the same over time? Is it
passed in from a parent? Can you compute it from other state or props? A
yes to any of them means it is not state. Here the product list arrives
as a prop and the filtered list is computable, so only the search text
and the in-stock checkbox survive.Step 4 â decide where the state lives. For each
piece, find the closest common parent of the components that render from
it and put the state there. Both pieces are read by SearchBar and
ProductTable, so both belong in FilterableProductTable.Step 5 â add inverse data flow. State lives above
the inputs that change it, so the owner passes setters down as props and
the inputs call them. More typing than two-way binding, and in exchange
every state change has exactly one visible path. Steps 3 and 4 are the
ones that pay: most tangled React code is state duplicated instead of
derived, or parked in a component that was not the common owner.2.11.2 Composition Versus Inheritance
React has run at Meta across thousands of components without a single component inheritance hierarchy, and the official guidance has never wavered: reuse code by composing components, not by extending them. Even in the class era, extends React.Component was plumbing, not a base class to build a family tree on.The reason is not dogma. Inheritance forces a parent to anticipate every variation a child might want and to encode each as a prop it must know about. Composition lets the parent stay ignorant: it declares a hole, and the caller fills it with anything React can render.function RigidCard({ title, body, footerText }) { // every hole must be a string
return <div className="c"><h4>{title}</h4><p>{body}</p><small>{footerText}</small></div>;
}
function Card({ title, children, footer }) { // holes accept any node
return <div className="c"><h4>{title}</h4>{children}<p>{footer}</p></div>;
}
const App = () => (
<div className="row">
<RigidCard title="Rigid" body="Strings only." footerText="A button cannot go here." />
<Card title="Composed" footer={<Counter />}>
<p>Any markup fits, <b>including state</b>.</p>
</Card>
</div>
);
2.11.3 Containment
Some components cannot know their children ahead of time. A dialog, a card, a sidebar, a layout shell: each is a box with behavior and styling, and the contents are the caller's business. Such components use the children prop from Section 2.2.7 and render it as a hole in their own output.One hole is rarely enough. A layout has a left and a right; a dialog has a body and a set of actions. Because JSX elements are ordinary values, you can pass them through any prop you like, which gives you named slots with no API beyond props.const FancyBorder = ({ children }) => // fills the hole between the tags
<div className="FancyBorder">{children}</div>;
const SplitPane = ({ left, right }) => // two named slots, placed by the container
<div className="SplitPane">
<div className="pane">{left}</div>
<div className="pane">{right}</div>
</div>;
const App = () => (
<FancyBorder>
<h3>Welcome</h3>
<SplitPane left={<Contacts />} right={<Chat />} />
</FancyBorder>
);
2.11.4 Specialization
The classic argument for inheritance is specialization: X is a kind of Y, so X extends Y and overrides a few members. In React you get the same result by rendering the general component from the specific one and pre-filling its props. WelcomeDialog is a kind of Dialog, so it renders a Dialog.const Dialog = ({ title, message, children }) => (
<FancyBorder>
<h3 className="Dialog-title">{title}</h3>
<p className="Dialog-message">{message}</p>
{children}
</FancyBorder>
);
const WelcomeDialog = () => // no props: every hole is decided here
<Dialog title="Welcome" message="Thank you for visiting our spacecraft." />;
function SignUpDialog({ onSignUp }) { // specialized, and still open at the bottom
const [name, setName] = useState('');
return (
<Dialog title="Mars Program" message="How should we refer to you?">
<input value={name} onChange={(e) => setName(e.target.value)} />
<button onClick={() => onSignUp(name)}>Sign Me Up!</button>
</Dialog>);
}
2.11.5 Higher-Order Components and Render Props
Before Hooks there was no way for two components to share stateful logic without sharing a parent. Two patterns filled the gap and are still in circulation. A higher-order component is a function that takes a component and returns a new one with extra props injected; a render prop is a prop whose value is a function the component calls to decide what to render. Compare all three solutions to one problem.function withWidth(Wrapped) { // HOC: a component factory
return function WithWidth(props) {
const [w, setW] = useState(window.innerWidth);
useEffect(() => {
const on = () => setW(window.innerWidth);
window.addEventListener('resize', on);
return () => window.removeEventListener('resize', on);
}, []);
return <Wrapped {...props} width={w} />; // pass through, then add
};
}
const Bar = withWidth(({ width }) => <p>{width}px</p>);
function Width({ render }) { // render prop: call back to the caller
const [w, setW] = useState(window.innerWidth); // ...same effect as above
return render(w);
}
const Bar2 = () => <Width render={(w) => <p>{w}px</p>} />;
// useWidth(): the same state and effect, returned as a plain value
const Bar3 = () => <p>{useWidth()}px</p>; // Hook: no extra component at all
2.11.6 Error Boundaries
An uncaught error during rendering unmounts the entire root: React would rather show nothing than a corrupted interface. An error boundary catches errors thrown by its subtree while rendering and shows a fallback instead. It is the only React feature that still requires a class, because there is no Hook equivalent of static getDerivedStateFromError.class ErrorBoundary extends Component {
state = { error: null };
static getDerivedStateFromError(error) { return { error }; } // choose the fallback
componentDidCatch(error, info) { log(error, info.componentStack); } // report it
render() {
const e = this.state.error;
return e ? <p className="fb">Chart failed: {e.message}</p> : this.props.children;
}
}
const Chart = () => { throw new Error('series is empty'); };
const Dashboard = () => ( // only the chart is replaced by the red fallback
<div>
<h3>Sales</h3>
<ErrorBoundary><Chart /></ErrorBoundary>
<p>The rest of the dashboard renders normally.</p>
</div>
);
2.11.7 Portals
A component's output normally lands in the DOM under its parent's node. That breaks down for modals, dropdowns and tooltips: an ancestor with overflow: hidden, a stacking context or a CSS transform clips them or traps them behind other content. createPortal(children, domNode) renders children into any DOM node you choose, while leaving them where they were in the React tree.<div id="app-root"></div><div id="modal-root"></div>
<style>body{font:14px system-ui;margin:6px}
#modal-root{border:2px dashed #6a1b9a;padding:2px 8px}</style>
<script src="https://cdn.jsdelivr.net/npm/@babel/standalone@8.0.5/babel.min.js"></script>
<script type="importmap">{ "imports": { "react": "https://esm.sh/react@19.3.0",
"react-dom": "https://esm.sh/react-dom@19.3.0",
"react-dom/client": "https://esm.sh/react-dom@19.3.0/client" } }</script>
<script type="text/babel" data-type="module">
import React, { useState } from 'react';
import { createPortal } from 'react-dom';
import { createRoot } from 'react-dom/client';
const Modal = ({ children }) =>
createPortal(children, document.getElementById('modal-root'));
function App() {
const [log, setLog] = useState('no click yet');
return (
<div onClick={(e) => setLog(`handled by App; DOM parent #${e.target.parentNode.id}`)}>
<p>App root. Log: <b>{log}</b></p>
<Modal><button>Click me (I live in #modal-root)</button></Modal>
</div>);
}
createRoot(document.getElementById('app-root')).render(<App />);
setTimeout(() => document.querySelector('#modal-root button').click(), 100);
</script>
Figure 2.40: Browser output of Listing 2.93The button is a DOM child of #modal-root, a sibling of #app-root â
yet clicking it ran the onClick handler on the <div> inside App.
Events from a portal bubble along the React tree, not the DOM
tree, so a portal's contents behave like children in every way that
matters to your code: handlers on ancestors fire, context flows in, and
state lifted above the portal still controls it. Only the DOM placement
moves. That is what makes portals usable: a <Modal> sits next to
the data that opens it and reads the same theme context, while its
markup escapes to the end of <body> where no ancestor can clip
it.2.11.8 Strict Mode
<StrictMode> turns on extra checks for everything below it, in development builds only; it has no effect on a production build. Vite's React template wraps the whole app in it and there is rarely a reason to take it out â but the first time you meet it, its behavior looks like a bug, so it is worth seeing directly.<div id="root"></div><style>body{font:14px system-ui;margin:6px}</style>
<script src="https://cdn.jsdelivr.net/npm/@babel/standalone@8.0.5/babel.min.js"></script>
<script type="importmap">{ "imports": { "react": "https://esm.sh/react@19.3.0?dev",
"react-dom/client": "https://esm.sh/react-dom@19.3.0/client?dev" } }</script>
<script type="text/babel" data-type="module">
import React, { StrictMode, useState, useEffect } from 'react';
import { createRoot } from 'react-dom/client';
let renders = 0, setups = 0, cleanups = 0;
function Probe() {
renders++; // impure on purpose, to be counted
const [, setTick] = useState(0);
useEffect(() => {
setups++;
const id = setTimeout(() => setTick(1), 80);
return () => { cleanups++; clearTimeout(id); };
}, []);
return <p>render calls: {renders} â effect setups: {setups} â cleanups: {cleanups}</p>;
}
createRoot(document.getElementById('root')).render(
<StrictMode><Probe /></StrictMode>);
</script>
Figure 2.41: Browser output of Listing 2.94Mounting once produced two render calls and a setup-cleanup-setup
cycle; the state update 80 ms later rendered twice more, giving the four
calls and two setups on screen. React runs your code twice on purpose to
see whether it behaves the same both times. The double render catches
impure rendering â renders++ is exactly the mutation it hunts for â and
the extra setup/cleanup cycle catches effects that subscribe without
unsubscribing, which in production would leak on every remount. Callback
refs get the same attach-detach-attach treatment, as do the functions
passed to useState, useMemo and useReducer. Strict Mode also warns about
deprecated APIs reached through old dependencies: unsafe legacy
lifecycles, and the patterns React 19 removed outright.2.11.9 React and Custom Elements
Custom elements are the browser's own component model: a class extending HTMLElement, registered under a hyphenated tag name with customElements.define, usable anywhere HTML is. React 19 added full support for them and passes every test on Custom Elements Everywhere, which earlier versions did not.<div id="root"></div><style>body{font:14px system-ui;margin:6px}</style>
<script src="https://cdn.jsdelivr.net/npm/@babel/standalone@8.0.5/babel.min.js"></script>
<script>
class StarRating extends HTMLElement { // plain DOM, no React involved
set score(v) { this.textContent = '*'.repeat(v.stars) + ` (${v.label})`; }
}
customElements.define('star-rating', StarRating);
</script>
<script type="importmap">{ "imports": { "react": "https://esm.sh/react@19.3.0",
"react-dom/client": "https://esm.sh/react-dom@19.3.0/client" } }</script>
<script type="text/babel" data-type="module">
import React from 'react';
import { createRoot } from 'react-dom/client';
const App = () => <p>Rating:{' '}
<star-rating score={{ stars: 4, label: 'good' }} variant="compact" /></p>;
createRoot(document.getElementById('root')).render(<App />);
setTimeout(() => {
const el = document.querySelector('star-rating');
document.body.insertAdjacentHTML('beforeend',
`<p>score as attribute? ${el.hasAttribute('score')} â
variant as attribute? ${el.hasAttribute('variant')}</p>`);
}, 100);
</script>
Figure 2.42: Browser output of Listing 2.95The client rule is simple: if the prop name matches a property that
exists on the element instance, React assigns it as a property;
otherwise it sets an attribute. That is why score â an object, which
could never survive being stringified into an attribute â arrived intact
at the setter, while variant became an attribute. During server
rendering there are no instances to inspect, so primitive props (string,
number, true) render as attributes and everything else (object,
function, symbol, false) is omitted.Two rough edges remain. React has no onSomething mapping for custom
events, so listen with a ref and addEventListener in an effect and
remove it in the cleanup. And registration must happen before React
renders the tag, or the browser treats it as an unknown element and
upgrades it later â import the definition module at the top of your
entry file.