React Patterns
Composition, hooks, and the useEffect footguns.
React Patterns
The React component model is a function from state to UI. Most problems come from fighting that model instead of working with it.
Analogy
A React component is like a Lego brick. Each brick has a fixed shape (the component's props and output), but what matters is that bricks snap together — you build a spaceship by combining a cockpit piece, a wing piece, and an engine piece. You don't carve a new figurine from a block of wood every time you want something slightly different. When a React component tries to handle "everything a card could be" with fifteen optional props and deep if-branches, that's whittling — and the next requirement always makes the knife slip. Composition means handing the caller a socket and letting them snap in whatever they want.
Composition over inheritance
React has no class inheritance hierarchy to extend. Components compose by accepting children and other components as props.
// Bad — tight coupling, hard to reuse
function UserCard() {
return (
<div>
<Avatar /> {/* hardcoded inside */}
<UserName />
</div>
);
}
// Good — slot pattern lets callers customise
function Card({ children }: { children: React.ReactNode }) {
return <div className="card">{children}</div>;
}
// Caller controls what goes inside
<Card>
<Avatar src={user.avatar} />
<UserName name={user.name} />
</Card>
This pattern — passing components as children or named props — is more flexible than any inheritance hierarchy.
Custom hooks
Extract stateful logic into functions whose names start with use. This keeps components focused on rendering.
function useWindowSize() {
const [size, setSize] = useState({ width: 0, height: 0 });
useEffect(() => {
const update = () =>
setSize({ width: window.innerWidth, height: window.innerHeight });
window.addEventListener("resize", update);
update();
return () => window.removeEventListener("resize", update);
}, []);
return size;
}
The hook is testable in isolation, composable with other hooks, and reusable across components.
The three useEffect footguns
1. Stale closures
A function defined inside a component captures the values of variables at the time it runs. If useEffect has an empty dependency array [], it captures those values at mount and never updates.
// Bug: count is always 0 inside the interval
useEffect(() => {
const id = setInterval(() => console.log(count), 1000);
return () => clearInterval(id);
}, []); // empty deps = stale closure
// Fix: include count in deps, or use a ref
2. Infinite effect loop
If an effect sets state, and that state is in the dependency array, every render triggers the effect — which sets state — which triggers a render.
// Bug: re-renders forever
useEffect(() => {
setData(transform(data));
}, [data]); // data updates → effect runs → data updates → …
// Fix: compute the derived value during render — no effect needed
const transformed = transform(data);
3. Derived state as an effect
The most common mistake: initialise state from a prop, then keep it in sync via an effect.
// Bug: name lags one render behind; also an effect on every prop change
const [name, setName] = useState(props.initialName);
useEffect(() => {
setName(props.initialName);
}, [props.initialName]);
// Fix option 1: compute during render (if truly derived)
const displayName = props.initialName.trim();
// Fix option 2: add key on the parent so React resets the whole component
<NameField key={userId} initialName={user.name} />
Lifting state up
When two sibling components need to share state, move the state to their closest common ancestor and pass it down as props.
function Parent() {
const [value, setValue] = useState("");
return (
<>
<Input value={value} onChange={setValue} />
<Preview value={value} />
</>
);
}
When to use context
Context is not a performance optimisation — every consumer re-renders when context value changes. Use it for slow-moving values: authentication state, theme, locale.
For frequently-changing data (search query, form fields), lift state to the nearest shared ancestor and pass props.
useMemo and useCallback cost
Both come with overhead: a dependency comparison on every render, a closure allocation, and cache storage. They pay off only when:
useMemo: the computation is genuinely expensive (measured, not assumed).useCallback: the function is passed to a child wrapped inReact.memo.
Wrapping every function in useCallback out of habit makes code harder to read with no measurable benefit.