Why? Prevents source‑of‑truth drift and reduces update cascades.
New React developers sometimes “cache” derived data in state because it feels faster or more convenient. The trap? You now have two sources of truth. Forget to keep them in sync and your UI lies to you—think stale counts, wrong filters, or layout glitches. React is already great at recomputing pure values during render; let it handle the math instead of managing more state than you need.
import { useMemo, useState } from "react";
type Product = { id: number; name: string; price: number };
export default function Cart({ products }: { products: Product[] }) {
const [discount, setDiscount] = useState(0); // genuine state
// Total is derived → compute, not store.
const total = useMemo(
() => products.reduce((sum, p) => sum + p.price, 0) * (1 - discount),
[products, discount]
);
return (
<section className="space-y-2">
<h2 className="text-xl font-semibold">Cart</h2>
{products.map((p) => (
<p key={p.id}>
{p.name} — ${p.price}
</p>
))}
<label className="block">
Discount %
<input
type="number"
value={discount * 100}
onChange={(e) => setDiscount(+e.target.value / 100)}
className="ml-2 w-20 border"
/>
</label>
<p className="font-bold">Total: ${total.toFixed(2)}</p>
</section>
);
}
// Trying to “cache” the total in state—easy to desync.
function CartBroken({ products }: { products: Product[] }) {
const [discount, setDiscount] = useState(0);
const [total, setTotal] = useState(0); // ❌ redundant state
// Update total when products change (but what if we forget one?)
useEffect(() => {
const t = products.reduce((sum, p) => sum + p.price, 0) * (1 - discount);
setTotal(t);
}, [products]); // ❌ forgot to include discount!
return (
<>
{/* …product list… */}
<p>Total: ${total.toFixed(2)}</p> {/* stale after discount changes */}
</>
);
}
Outcome: Applying a discount doesn’t refresh total
because the effect’s dependency list is incomplete—classic “derived state drift.”
useMemo
when the calculation is heavy.useState
is a synchronization liability—eliminate the unnecessary ones.useMemo
/useEffect
must list every input; forgetting one re‑creates drift.