Derived values that can't go stale
A line item: pick a quantity and a discount, show subtotal → discount → tax → total. Each number is computed from the ones before it.
The values are easy. Keeping the dependencies between them correct as the formulas change is where React quietly rots.
Before / after
Section titled “Before / after”React — chained useMemo, hand-kept dependency arrays
Section titled “React — chained useMemo, hand-kept dependency arrays”import { useState, useMemo } from "react"
const UNIT_PRICE = 12.5const TAX_RATE = 0.0825
export function LineItem() { const [quantity, setQuantity] = useState(1) const [discountPct, setDiscountPct] = useState(0)
const subtotal = useMemo(() => quantity * UNIT_PRICE, [quantity]) const discount = useMemo(() => subtotal * (discountPct / 100), [subtotal, discountPct]) const tax = useMemo(() => (subtotal - discount) * TAX_RATE, [subtotal, discount]) const total = useMemo(() => subtotal - discount + tax, [subtotal, discount, tax])
return ( <div className="line"> <button onClick={() => setQuantity((q) => Math.max(1, q - 1))}>−</button> <input type="number" value={quantity} onChange={(e) => setQuantity(Math.max(1, Number(e.target.value) || 1))} /> <button onClick={() => setQuantity((q) => q + 1)}>+</button> <p>Total: ${total.toFixed(2)}</p> </div> )}Reactra — chained derived, dependencies inferred
Section titled “Reactra — chained derived, dependencies inferred”const UNIT_PRICE = 12.5const TAX_RATE = 0.0825
export component LineItem { state quantity: number = 1 state discountPct: number = 0
derived subtotal = quantity * UNIT_PRICE derived discount = subtotal * (discountPct / 100) derived tax = (subtotal - discount) * TAX_RATE derived total = subtotal - discount + tax
view { <div className="line"> <button onClick={() => setQuantity(Math.max(1, quantity - 1))}>−</button> <input type="number" value={quantity} onChange={(e: any) => setQuantity(Math.max(1, Number(e.target.value) || 1))} /> <button onClick={() => setQuantity(quantity + 1)}>+</button> <p>Total: ${total.toFixed(2)}</p> </div> }}import { useEffect, useMemo, useState } from "react"
export const LineItem = () => { const [quantity, setQuantity] = useState((1) as number) const [discountPct, setDiscountPct] = useState((0) as number) const subtotal = useMemo(() => quantity * UNIT_PRICE, [quantity]) const discount = useMemo(() => subtotal * (discountPct / 100), [subtotal, discountPct]) const tax = useMemo(() => (subtotal - discount) * TAX_RATE, [subtotal, discount]) const total = useMemo(() => subtotal - discount + tax, [subtotal, discount, tax]) useEffect(() => { const h = globalThis.__REACTRA_TEST__; if (h) h.update("LineItem", { quantity, discountPct, subtotal, discount, tax, total }) }) return (<> <div className="line"> <button onClick={() => setQuantity(Math.max(1, quantity - 1))}>−</button> <input type="number" value={quantity} onChange={(e: any) => setQuantity(Math.max(1, Number(e.target.value) || 1))} /> <button onClick={() => setQuantity(quantity + 1)}>+</button> <p>Total: ${total.toFixed(2)}</p> </div> </>)}export default LineItemThe bug that lives in the left column
Section titled “The bug that lives in the left column”const total = useMemo(() => subtotal - discount + tax, [subtotal, discount]) // ← forgot `tax`Drop one name from a dependency array and total silently stops updating when
tax changes. No error, no warning — just a wrong number on a screen, found in
production. Every chained useMemo is another array to keep in sync by hand.
On the right, derived total = subtotal - discount + tax has no dependency
array to forget. The compiler reads the expression and tracks what it uses. The
formula is the dependency list. Change the formula, the deps change with it.
What disappeared
Section titled “What disappeared”const [x, setX] = useState(…)tuple →state x = …(one name;setXis generated)- 4 hand-maintained
useMemodependency arrays → 0 - The “did I list every dependency?” review checklist → not a thing anymore
- Deciding what’s worth memoizing →
derivedalways memoizes, correctly
It’s not a new runtime
Section titled “It’s not a new runtime”state x = 0 → const [x, setX] = useState(0). derived y = x * 2 →
const y = useMemo(() => x * 2, [x]) — with the [x] filled in by the compiler.
It’s the React you’d write, minus the array you’d get wrong.
The formula is the dependency list. You only have to be right once.