Skip to main content

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​

PatternIdeaToday
CompositionPass JSX as children or other props instead of configuring with flagsThe default
Conditional renderingif, a ternary or && decides what to returnThe default
Custom hookA use… function that shares stateful logicThe default for logic
Compound componentsSeveral components share implicit state through contextUI libraries: tabs, selects, accordions
Render propA prop that is a function returning JSXWhen the child decides what to pass back
Higher-order componentA function that takes a component and returns a new oneLegacy; replaced by hooks
Error boundaryA class component that catches errors of its subtreeRequired; no hook equivalent

Conditional Rendering​

  • if with an early return, a ternary a ? <A /> : <B />, or a && <A />.
  • && with a number. {count && <List />} renders 0 when count is 0: React skips false, null and undefined, but renders numbers. Write count > 0 && … or use a ternary.

Composition​

  • A component that accepts children does 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) => …} />. children can 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/or componentDidCatch (log the error). There is no hook for it; the react-error-boundary package 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 with try / catch.
  • Exception in React 19: an error thrown inside an Action (a function passed to startTransition or <form action>, async ones included) and a promise rejected in use do 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 / catch and state. To show the boundary's fallback, run the work in startTransition or pass the error to showBoundary from the useErrorBoundary hook of react-error-boundary.
  • Composition or inheritance? Composition: React components are not extended from one another.

Common Mistakes​

  • {items.length && <List />} rendering 0.
  • Expecting an error boundary to catch errors from onClick or fetch.
  • 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.

See Also​