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>

);

Listing 2.88: A rigid component beside a composed one RigidCard needs a new prop, and a new branch inside it, for every kind of footer anyone ever wants. Card needed nothing: Counter is a stateful component the card has never heard of, and clicking its button re-renders the counter without touching the card. If a component needs non-visual behavior from another, export it as a plain module and import it from both; if you want a shared base class holding lifecycle logic, that is what custom Hooks (Section 2.10) are for.

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>

);

Listing 2.89: One anonymous hole and two named ones The left and right props are not strings or configuration objects but real React elements, already created and waiting to be placed; pass null to leave a slot empty, or an array to fill one with several.One subtlety: a slot element is rendered where the container puts it but was created in the caller, so it closes over the caller's variables and reads context from the caller's position in the tree.

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>);

}

Listing 2.90: A specific component built from a general one Read the two specializations side by side and the advantage over subclassing is concrete. WelcomeDialog fixes props and nothing else. SignUpDialog fixes props and fills the children hole and owns state — three kinds of extension, none of which required Dialog to expose a protected member or to know that either specialization exists.The pattern nests. A design system exposes one Button with variant and size props; the application defines PrimaryButton and DangerButton as one-line specializations, so a change of house style is a change in one file. A specialization can itself be specialized: DeleteConfirmDialog renders ConfirmDialog, which renders 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

Listing 2.91: The same shared logic as a HOC, a render prop and a Hook The differences show up at scale. Both older patterns add a component to the tree for every piece of logic shared, so a screen using five sits under five wrappers in DevTools — the "wrapper hell" that motivated Hooks. A HOC also hides the prop names it injects, so nothing tells you where width came from, and combining several means hoping their names do not collide. Render props avoid that but push the caller into a pyramid of nested callbacks. Hooks compose by ordinary variable names.You still meet both: memo is a higher-order component, React Redux's connect and React Router's old withRouter are HOCs, and drag-and-drop libraries expose render props because the parent must control what is drawn per item. Write new shared logic as a custom Hook. If you must write a HOC, name it withX, set its displayName, and never call it during a render — that remounts the subtree every time.

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>

);

Listing 2.92: A boundary that catches a child which throws getDerivedStateFromError runs during rendering and must be pure; componentDidCatch runs afterwards and is where side effects belong, such as logging to Sentry. React 19 produces a single console entry per error, where earlier versions logged the same one up to three times.Boundaries do not catch errors in event handlers, in setTimeout or promise callbacks, during server rendering, or thrown by the boundary itself — those are ordinary JavaScript errors, so use try/catch. What a boundary does catch, it catches for everything below it, so placement matters: one per route stops a broken widget blanking the application, one per panel keeps a failure local. Reset a boundary by giving it a new key, which remounts the subtree.For production, use react-error-boundary (https://github.com/bvaughn/react-error-boundary) (6.1.5, MIT, npm i react-error-boundary). It wraps the class and adds what you would otherwise write yourself: FallbackComponent receives a resetErrorBoundary function, resetKeys retries when a dependency changes, onError and onReset hook into logging, and useErrorBoundary returns showBoundary so an async failure can be pushed into the nearest boundary. At the root, createRoot takes onUncaughtError and onCaughtError callbacks that route what React catches into your logger.

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>

Listing 2.93: A modal rendered outside the app root that still bubbles into it 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. Warning: Rendering outside your React tree makes accessibility your job. Portals do not move focus, trap it, or add aria-modal and role="dialog". Use the native <dialog> element with showModal(), which handles focus trapping and the top layer for you, or a headless library that does.

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>

Listing 2.94: Strict Mode double-invoking render and effect setup 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. Warning: Never "fix" double invocation with a ref guard that makes an effect run once. The double run is a diagnostic; silencing it hides the bug, and the effect will run twice in production anyway when a Transition or Activity (Section 2.8.9) remounts the subtree.

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>

Listing 2.95: React passing a property and an attribute to a custom element 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.