Common Patterns
React code is reused through composition, not inheritance. UI is composed
with children and compound components; logic is shared with custom
hooks, which replaced render props and higher-order components in most
cases. Error boundaries catch rendering errors and show a fallback.
Details
| Pattern | Idea | Today |
|---|---|---|
| Composition | Pass JSX as children or other props instead of configuring with flags | The default |
| Conditional rendering | if, a ternary or && decides what to return | The default |
| Custom hook | A use… function that shares stateful logic | The default for logic |
| Compound components | Several components share implicit state through context | UI libraries: tabs, selects, accordions |
| Render prop | A prop that is a function returning JSX | When the child decides what to pass back |
| Higher-order component | A function that takes a component and returns a new one | Legacy; replaced by hooks |
| Error boundary | A class component that catches errors of its subtree | Required; no hook equivalent |
Conditional Rendering
ifwith an early return, a ternarya ? <A /> : <B />, ora && <A />.&&with a number.{count && <List />}renders0whencountis0: React skipsfalse,nullandundefined, but renders numbers. Writecount > 0 && …or use a ternary.
Composition
- A component that accepts
childrendoes not need to know what is inside:<Card><Avatar user={user} /></Card>. - It removes prop drilling: the data is used where the element is created.
- "Slots" are ordinary props with JSX:
<Layout sidebar={<Menu />} />.
Compound Components
The parent keeps the state and provides it through context; the children read it. The user of the component controls the markup and the order, the state stays hidden.
Render Props and HOCs
- A render prop lets a component delegate rendering:
<MouseTracker render={(position) => …} />.childrencan be such a function too. - A higher-order component wraps a component to add props or behavior:
withRouter(Component),connect(mapState)(Component). - HOC problems: "wrapper hell" in DevTools, prop name collisions, unclear origin of props. A custom hook solves the same task without them.
Error Boundaries
- A class with
static getDerivedStateFromError(switch to the fallback) and/orcomponentDidCatch(log the error). There is no hook for it; thereact-error-boundarypackage provides a ready component. - It catches errors thrown below it during rendering, in lifecycle methods and in effects.
- It does not catch errors in event handlers, in asynchronous code
(
setTimeout, promise callbacks), during server rendering, or in the boundary itself. Those are handled withtry/catch. - Exception in React 19: an error thrown inside an Action (a function passed
to
startTransitionor<form action>, async ones included) and a promise rejected inusedo reach the boundary. - Without a boundary an error unmounts the whole app.
- Boundaries are placed around independent parts of the UI: the page, a widget, a route.
Example
Compound components:
import { createContext, use, useState } from 'react';
const TabsContext = createContext(null);
function Tabs({ defaultValue, children }) {
const [value, setValue] = useState(defaultValue);
return <TabsContext value={{ value, setValue }}>{children}</TabsContext>;
}
function Tab({ value, children }) {
const tabs = use(TabsContext);
return (
<button
role="tab"
aria-selected={tabs.value === value}
onClick={() => tabs.setValue(value)}
>
{children}
</button>
);
}
function TabPanel({ value, children }) {
const tabs = use(TabsContext);
return tabs.value === value ? <div role="tabpanel">{children}</div> : null;
}
export default function App() {
return (
<Tabs defaultValue="about">
<Tab value="about">About</Tab>
<Tab value="posts">Posts</Tab>
<TabPanel value="about">About me</TabPanel>
<TabPanel value="posts">My posts</TabPanel>
</Tabs>
);
}
An error boundary:
import { Component } from 'react';
class ErrorBoundary extends Component {
state = { hasError: false };
static getDerivedStateFromError() {
return { hasError: true };
}
componentDidCatch(error, info) {
console.error(error, info.componentStack);
}
render() {
if (this.state.hasError) {
return this.props.fallback;
}
return this.props.children;
}
}
// Usage
<ErrorBoundary fallback={<p>Something went wrong.</p>}>
<Profile />
</ErrorBoundary>;
Render prop and HOC
import { useState } from 'react';
function MouseTracker({ render }) {
const [position, setPosition] = useState({ x: 0, y: 0 });
return (
<div
style={{ height: '100vh' }}
onMouseMove={(event) =>
setPosition({ x: event.clientX, y: event.clientY })
}
>
{render(position)}
</div>
);
}
// Usage
<MouseTracker
render={({ x, y }) => (
<p>
The mouse position is ({x}, {y})
</p>
)}
/>;
function withAuthorization(WrappedComponent) {
return function Authorized({ isAuthorized, ...rest }) {
if (!isAuthorized) {
return <h1>Unauthorized</h1>;
}
return <WrappedComponent {...rest} />;
};
}
// Usage
const ProtectedSettings = withAuthorization(Settings);
Follow-up Questions
- Why did hooks replace HOCs and render props? The same logic is shared without extra components in the tree and with explicit inputs and outputs.
- How to handle an error in an event handler?
try/catchand state. To show the boundary's fallback, run the work instartTransitionor pass the error toshowBoundaryfrom theuseErrorBoundaryhook ofreact-error-boundary. - Composition or inheritance? Composition: React components are not extended from one another.
Common Mistakes
{items.length && <List />}rendering0.- Expecting an error boundary to catch errors from
onClickorfetch. - One error boundary for the whole app: any error replaces the entire page.
- Creating a HOC inside a component: a new component type on every render remounts the subtree.