Derived state: don't store what you can compute
I've debugged more UI sync bugs than I can count, and almost every one traces back to the same root cause: storing a value in state when it should be computed. You calculate it once, store it, then forget to update it when the source data changes. This is why your filter buttons work sometimes and your totals are mysteriously stale.
Let's talk about why derived state is your biggest footgun in React, and how to spot and eliminate it before it costs you hours of debugging.
What is derived state?
Derived state is any value you can calculate from other state or props you already have. The classic examples: filtered lists, counts, totals, user fullnames, computed statuses. If the value can be expressed as a pure function of existing data, it shouldn't live in state.
Here's the problem: once you store a derived value in state, you've created two sources of truth. Your source data (the todos array) and your derived data (the filtered todos array) now live separately. When one changes, the other might not. React has no magical way to keep them in sync unless you write code to sync them explicitly. And humans are terrible at remembering to do that.
Compute filtered lists during render
The simplest fix is the most powerful one: compute the value during render, not in state.
function TodoList({ todos }) {
const [filter, setFilter] = useState('all');
// Compute filtered list during render
const visible = todos.filter(t =>
filter === 'all' || t.status === filter
);
return (
<>
<button onClick={() => setFilter('all')}>All</button>
<button onClick={() => setFilter('active')}>Active</button>
<button onClick={() => setFilter('done')}>Done</button>
<ul>
{visible.map(t => (
<li key={t.id}>{t.title}</li>
))}
</ul>
</>
);
}That's it. No state for visible. No useEffect to sync it. Every time the component renders, you get the correct filtered list, derived from the current todos and filter. If either changes, the next render gives you the right answer automatically.
This pattern scales beyond lists. If you're building a summary dashboard, compute the metrics during render:
function Dashboard({ orders }) {
const totalRevenue = orders.reduce((sum, o) => sum + o.amount, 0);
const averageOrderValue = totalRevenue / orders.length;
const topProduct = orders
.flatMap(o => o.items)
.reduce((acc, item) => {
const existing = acc.find(i => i.productId === item.productId);
if (existing) existing.count++;
else acc.push({ ...item, count: 1 });
return acc;
}, [])
.sort((a, b) => b.count - a.count)[0];
return (
<div>
<p>Total: ${totalRevenue}</p>
<p>Average: ${averageOrderValue}</p>
<p>Top: {topProduct?.name}</p>
</div>
);
}Every render, you get fresh numbers. You don't have to remember to update them. You can't get them wrong because they're calculated on the fly from the single source of truth: the orders array.
No useEffect to sync state with props
The anti-pattern I see most often is this: receive a prop, store it in state, then use useEffect to sync it when the prop changes.
// WRONG: Creates two sources of truth
function UserCard({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
setUser(fetchUser(userId)); // sync state with prop
}, [userId]);
return <div>{user?.name}</div>;
}Why is this wrong? Because for one render cycle, your UI shows stale data. The prop changed, but the effect hasn't fired yet. You might also forget the dependency array, or miss a dependency, and then the effect never fires and you're permanently out of sync.
The better approach: if you have the data, use it directly. If you need to fetch it, fetch it in a parent or in a server component, not in state:
// BETTER: Single source of truth
function UserCard({ user }) {
return <div>{user.name}</div>;
}If you absolutely must transform or load data based on props, do it during render with useMemo (we'll cover that next), not in useEffect and state.
The rule: useEffect is for side effects (network requests, subscriptions, analytics). It's not for keeping state in sync with props. If you feel the urge to write an effect to sync state with a prop, stop and ask: why is this value in state at all?
useMemo only if computing is expensive
Computing during render is fast. Filtering 1,000 items, sorting 100 products, summing a few numbers—these are microseconds. Modern hardware laughs at this workload. React renders fast enough that you won't notice.
But sometimes, the computation is genuinely expensive. Maybe you're sorting 100,000 rows. Maybe you're running a complex algorithm. Maybe you're building a tree structure. That's when useMemo makes sense:
function HugeList({ items, sortBy }) {
// Only recompute if items or sortBy changes
const sorted = useMemo(() => {
return items.slice().sort((a, b) => {
if (sortBy === 'name') return a.name.localeCompare(b.name);
if (sortBy === 'date') return new Date(b.date) - new Date(a.date);
return 0;
});
}, [items, sortBy]);
return (
<ul>
{sorted.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
);
}Now the sort runs only when items or sortBy changes, not on every render. If the user opens a sidebar or a tooltip, those renders don't re-sort the list.
But here's the catch: if you're using useMemo, it's a sign the computation is expensive enough to matter. If you're using it for trivial operations, you're adding overhead without benefit. The cost of useMemo (checking dependencies, managing memoized values) can exceed the cost of just recomputing.
My rule: don't use useMemo until you've profiled and confirmed it's needed. Most of the time, computing during render is fine.
One source of truth per value
This is the principle behind all of this. Every piece of data should have exactly one place where it's stored or defined. Everything else should derive from it.
In a well-structured component:
- State: User input, UI toggles, form fields. Things the user controls directly.
- Props: Data passed from above. The parent owns it.
- Computed values: Everything else. Filtered lists, totals, derived strings, calculated flags.
If you're storing a computed value in state, you've violated this principle. Now you have two places where the truth could live, and they can drift apart.
A concrete example from a real project: I was building a ride-hailing dashboard. We tracked active drivers (state), and the UI showed a count of active drivers. Somewhere, we stored that count in state too. A driver would come online, we'd increment the count in state, but if the active drivers array wasn't updated first, they'd be out of sync. The count would say 5 drivers but the list would show 3. This bug took hours to track down because the root cause was simple: the count was computed, not source data.
Common mistakes
Storing props in state without a useEffect. If you receive a value as a prop and immediately store it in state, you're creating a copy. Any changes to the prop won't be reflected until the next render triggers a state update.
Syncing state with props using useEffect but forgetting dependencies. You write useEffect(() => { setData(prop) }, []) and the effect never re-runs. Now the state is permanently out of date. It's easy to forget dependencies in complex effects, which is why this is so dangerous.
Computing values inside event handlers instead of during render. You click a button, an event handler filters the list and sets it in state. But what if the list changes and the user hasn't clicked? The state is stale. Compute during render instead, and the value is always current.
Using useCallback or useMemo for everything as a performance habit. This is cargo cult optimization. You wrap every function and value without understanding why. It adds code, adds complexity, and often makes performance worse. Only memoize if you've measured and it matters.
Key takeaways
- Derived state is any value you can calculate from other data you already have. Don't store it in state.
- Compute filtered lists, totals, and other derived values during render. React re-renders fast enough for almost all real-world computations.
- Never use useEffect to sync state with props. If you feel the urge, the value probably shouldn't be in state at all.
- Use useMemo only for computations that are genuinely expensive and you've measured to confirm. Most filter and map operations don't need it.
- Maintain one source of truth per value. Everything else should derive from it, not duplicate it.
- Ask yourself: where is this value really defined? If the answer is more than one place, you have a bug waiting to happen.






Comments
Loading comments…