Most React state bugs come from storing too much.
When a value can be derived from props, URL state, or another value already in memory, storing it separately creates a synchronization problem. The bug might not appear on the first render, but it usually shows up after filters, async saves, or optimistic updates enter the screen.
Keep the state surface small
I use a simple order of operations:
- Derive values during render when they are cheap and deterministic.
- Use state for values that change because of user interaction or async lifecycle.
- Use refs for transient browser values that should not re-render the tree.
- Use effects for synchronization with systems outside React.
That keeps hooks readable and makes the component easier to split later.
type Filter = "all" | "featured";
function getVisibleProjects<T extends { featured: boolean }>(
projects: T[],
filter: Filter,
): T[] {
if (filter === "featured") {
return projects.filter((project) => project.featured);
}
return projects;
}
The function is small, but it prevents a common mistake: storing filtered results as state and then trying to keep them synchronized.
Effects are boundaries
An effect should read like a bridge to something React does not own: a subscription, a timer, a physics engine, a browser API, analytics, or local storage. If an effect only updates state that could have been derived during render, it is usually hiding unnecessary complexity.
The goal is not fewer hooks for its own sake. The goal is fewer moving parts competing for authority over the same interface.
