· 7 min read
React Component Patterns: Tricks to Enhance Reusability
Practical component patterns and tricks to design reusable, composable, and maintainable React components for real-world apps.
Why patterns matter
Reusability is more than copying and pasting components. It’s about designing small, focused APIs that adapt to different needs, scale with your app, and are predictable for other developers. This article walks through practical component patterns and little tricks that make React components more reusable, composable, and robust.
Note: examples use plain React (hooks) and aim for clarity rather than production-ready details. For more fundamentals, see the React docs on hooks and general patterns at React Patterns.
Guiding principles for reusable components
- Composition over inheritance. Compose small units into larger ones.
- Minimal, predictable API. Small surface area + sensible defaults.
- Separation of concerns. Keep state, markup, and behavior decoupled when possible.
- Backwards-compatible extension. Allow consumers to opt in to advanced features.
- Accessibility and performance by default.
With those in mind, here are patterns and tricks you can apply.
Presentational vs Container components (conceptual)
Split components by responsibility:
- Presentational component: focused on markup and styling, accepts props for data and callbacks.
- Container component: handles data fetching and state, renders presentational components.
This keeps UI flexible and reusable. With hooks and custom hooks, the boundary is often functional rather than separate files, but the concept still helps design APIs.
Compound components
Use when multiple UI parts need to share internal state while keeping a nice declarative API for consumers.
Example: a simple Toggle compound API where consumers use nested subcomponents.
// Toggle.js
import React, { useState, createContext, useContext } from 'react';
const ToggleContext = createContext(null);
export function Toggle({ children, initial = false }) {
const [on, setOn] = useState(initial);
const value = { on, toggle: () => setOn(o => !o) };
return (
<ToggleContext.Provider value={value}>{children}</ToggleContext.Provider>
);
}
export function ToggleOn({ children }) {
const ctx = useContext(ToggleContext);
return ctx.on ? children : null;
}
export function ToggleOff({ children }) {
const ctx = useContext(ToggleContext);
return ctx.on ? null : children;
}
export function ToggleButton() {
const ctx = useContext(ToggleContext);
return <button onClick={ctx.toggle}>{ctx.on ? 'On' : 'Off'}</button>;
}
Usage stays ergonomic:
<Toggle>
<ToggleOn>It's on</ToggleOn>
<ToggleOff>It's off</ToggleOff>
<ToggleButton />
</Toggle>
Why this is reusable: consumers can compose only the pieces they need, and the internal state is centrally managed.
See Kent C. Dodds’ writeup on compound components for deeper patterns: https://kentcdodds.com/blog/compound-components-with-react-hooks
Render props
Render props let you pass a function prop that receives internal state/handlers and returns elements. Good for flexible rendering.
function MouseTracker({ children }) {
const [pos, setPos] = React.useState({ x: 0, y: 0 });
React.useEffect(() => {
function onMove(e) {
setPos({ x: e.clientX, y: e.clientY });
}
window.addEventListener('mousemove', onMove);
return () => window.removeEventListener('mousemove', onMove);
}, []);
return children(pos);
}
// usage
<MouseTracker>
{pos => (
<div>
Mouse at {pos.x}, {pos.y}
</div>
)}
</MouseTracker>;
Trade-offs: extremely flexible but can create nested functions in JSX. Hook patterns often replace simple use cases.
Higher-order components (HOC)
HOCs are functions that wrap a component to augment behavior. Useful for cross-cutting concerns.
function withLoading(Component) {
return function WithLoading({ isLoading, ...props }) {
if (isLoading) return <div>Loading...</div>;
return <Component {...props} />;
};
}
Be mindful of hoisting static methods, refs, and display names. Prefer hooks and composition for many new cases, but HOCs are still useful when you need to wrap components dynamically.
See the React docs on HOCs for patterns and caveats: https://reactjs.org/docs/higher-order-components.html
Custom hooks: logic reusability
Hooks are the primary way to reuse behaviour in modern React.
function useToggle(initial = false) {
const [on, setOn] = React.useState(initial);
const toggle = React.useCallback(() => setOn(o => !o), []);
return { on, toggle, setOn };
}
// usage
function MyComponent() {
const { on, toggle } = useToggle();
return <button onClick={toggle}>{on ? 'On' : 'Off'}</button>;
}
Custom hooks pair nicely with compound components: hooks handle state, while compound subcomponents handle markup.
Controlled vs Uncontrolled components
Design components to support both controlled and uncontrolled usage when appropriate (like inputs). Offer a simple API that lets consumers decide:
- Controlled: parent manages state and passes value + onChange.
- Uncontrolled: component manages its own state and exposes callbacks and refs.
Pattern:
function TextInput({ value, defaultValue, onChange, ...props }) {
const [internal, setInternal] = React.useState(defaultValue || '');
const isControlled = value !== undefined;
const current = isControlled ? value : internal;
function handleChange(e) {
const v = e.target.value;
if (!isControlled) setInternal(v);
onChange && onChange(v);
}
return <input value={current} onChange={handleChange} {...props} />;
}
This gives flexibility and predictable defaults.
Prop getters and the state reducer pattern (Downshift-style)
When a component needs to expose many props for consumers to spread onto DOM nodes (e.g., open/close, aria attributes, event handlers), use prop-getter functions. This centralizes behavior and avoids accidental override.
Example prop getter API:
function useDropdown() {
const [open, setOpen] = React.useState(false);
function getToggleProps({ onClick, ...rest } = {}) {
return {
'aria-expanded': open,
onClick: e => {
setOpen(o => !o);
onClick && onClick(e);
},
...rest,
};
}
return { open, getToggleProps };
}
State reducer pattern: let consumers control or intercept state changes with a reducer callback. Downshift uses this to be extremely flexible. See Downshift repo for a real-world example: https://github.com/downshift-js/downshift
Forwarding refs and useImperativeHandle
To be reusable with parent refs (e.g., focusing an internal input), forward refs and optionally expose a controlled imperative API.
const FancyInput = React.forwardRef(function FancyInput(props, ref) {
const inputRef = React.useRef(null)
React.useImperativeHandle(ref, () => ({
focus: () => inputRef.current && inputRef.current.focus()
}))
return <input ref={inputRef} {...props} />
})
// usage
const ref = React.createRef()
<FancyInput ref={ref} />
// ref.current.focus()
Docs: https://reactjs.org/docs/forwarding-refs.html
Polymorphic components (as prop)
Allow components to render different HTML elements via an “as” prop. Helpful for buttons that sometimes need to be a link or a div.
function Box({ as: Component = 'div', children, ...props }) {
return <Component {...props}>{children}</Component>
}
// usage
<Box as='button' onClick={...}>Click me</Box>
<Box as='a' href='...'>Link</Box>
In TypeScript, polymorphic components require careful typing to keep IntelliSense correct.
Slots and children-as-a-function
Slots give precise control over where children render. You can combine compound components with slot props.
function Modal({ header, body, footer }) {
return (
<div className="modal">
<div className="modal-header">{header}</div>
<div className="modal-body">{body}</div>
<div className="modal-footer">{footer}</div>
</div>
);
}
// usage
<Modal
header={<h1>Title</h1>}
body={<p>Content</p>}
footer={<button>Close</button>}
/>;
This pattern is explicit and useful for controlled layout.
Composition tricks and small API decisions that matter
- Prop getters vs spread props: use getter functions to merge event handlers safely.
- className merging: accept className prop and merge (use utilities like clsx); avoid overwriting.
- Provide sensible defaults and allow overrides via props or render functions.
- Keep state local by default but expose hooks/props to opt into control.
- Keep components small and single-purpose, then compose them.
Performance patterns
- memoize presentational components with React.memo when props are stable.
- Use useCallback/useMemo to avoid unnecessary re-renders for callbacks/derived values.
- Avoid over-optimizing early; measure first. For lists, prefer virtualization.
Example:
const Item = React.memo(function Item({ data, onClick }) { ... })
Accessibility is part of reusability
Reusable components should have accessible defaults:
- Manage focus appropriately for dialogs and menus.
- Expose aria attributes through prop-getters so consumers can add/override attributes.
- Provide keyboard interactions by default and allow consumers to extend handlers.
Downshift and Reach UI are great references for accessible patterns.
Testing and documentation
- Document the component API with prop examples and use-cases.
- Write unit tests for behavior (state changes, prop-getter merging, callbacks).
- Test as both controlled and uncontrolled where applicable.
TypeScript tips
- Prefer narrow, explicit prop types for public APIs.
- For polymorphic components, consider established typing patterns (generic component with extends).
- Type prop-getters and state reducer callbacks so consumers can override safely.
Example: Bringing patterns together
A small Select component skeleton that uses hooks, prop getters, controlled/uncontrolled support, and forwardRef.
function useSelect({ value: controlledValue, defaultValue, onChange }) {
const isControlled = controlledValue !== undefined;
const [value, setValue] = React.useState(defaultValue || null);
const current = isControlled ? controlledValue : value;
const openRef = React.useRef(false);
function select(v) {
if (!isControlled) setValue(v);
onChange && onChange(v);
}
function getToggleProps({ onClick, ...rest } = {}) {
return {
onClick: e => {
openRef.current = !openRef.current;
onClick && onClick(e);
},
'aria-expanded': openRef.current,
...rest,
};
}
return { value: current, select, getToggleProps };
}
This skeleton demonstrates how multiple tricks interoperate to produce a flexible API.
When to prefer which pattern
- Small behavior extraction: custom hooks.
- Shared UI parts that must coordinate: compound components or context.
- Flexible rendering: render props or children-as-function.
- Cross-cutting behavior: HOC or wrapper component.
- DOM ref and imperative actions: forwardRef + useImperativeHandle.
- Many DOM props and merging handlers: prop-getters.
Final checklist for reusable components
- Is the API minimal and predictable?
- Can consumers opt into or out of internal state?
- Are behaviors composable, not monolithic?
- Is accessibility considered by default?
- Is performance reasonable for common use-cases?
Reusable components are not a single pattern. They are a combination of small design choices that make components composable, extensible, and safe to use in many contexts.
Recommended reading
- React docs on hooks and patterns: https://reactjs.org/
- React Patterns: https://reactpatterns.com/
- Downshift (state reducer, prop getters): https://github.com/downshift-js/downshift
- Kent C. Dodds on compound components: https://kentcdodds.com/blog/compound-components-with-react-hooks