The most cloned bug in React, straight from a real codebase:
function Cart({ items }: { items: Item[] }) {
const [total, setTotal] = useState(0);
useEffect(() => {
setTotal(items.reduce((sum, i) => sum + i.price, 0));
}, [items]);
// two sources of truth, one repair crew
}A prop copied into state, plus an effect to keep them reconciled. It works until it does not: the stale value paints first, a second writer updates one copy but not the other, and the effect race ships to production. Every line of it exists to fix a problem the first line created.
Step 1: Spot the duplicate
The signature: useState(prop) followed by a useEffect that writes it back when the prop changes.
const [query, setQuery] = useState(props.initialQuery);
useEffect(() => {
setQuery(props.initialQuery); // the effect repairs drift
}, [props.initialQuery]); // the drift is the copy's faultTwo sources of truth for one value. The effect is the tell: state that exists to mirror other state is not state, it is a cache of a calculation you could just do.
Step 2: Compute it during render instead
If the value is a transformation of data you already have, calculate it in the render body:
function Cart({ items }: { items: Item[] }) {
const total = items.reduce((sum, i) => sum + i.price, 0);
const fullName = `${user.first} ${user.last}`;
const visible = items.filter((i) => i.status === filter);
// can never drift. updates every render by definition.
}A filtered list, a formatted total, a full name. It updates with every render and resets when inputs change, no effect required, no second writer possible. The useEffect version had one job, "stay in sync", and the render-body version cannot do otherwise.
Step 3: Memoize only when the math is heavy
Filtering ten thousand rows on every keystroke earns useMemo. Formatting a date does not:
// 10k rows x keystroke: measured, then memoized
const visible = useMemo(
() => items.filter((i) => matches(i, query)),
[items, query],
);
// a date format: not worth a dependency array
const label = format(order.createdAt);Reach for useMemo when a profiler shows the cost, not as a reflex, because the dependency array is its own bug surface. Derive-and-memoize fixes performance; derive-inline fixes correctness. The prop-copy antipattern is a correctness bug, and plain derivation already kills it.
Step 4: Keep a key for genuinely resettable state
When a component holds editable state seeded from an input, a draft of the selected record, a form with initial values, the documented fix is key, not a reset effect:
// wrong: reset-on-change effect
const [draft, setDraft] = useState(record);
useEffect(() => setDraft(record), [record]);
// right: the parent adds a key
<RecordEditor key={record.id} initial={record} />
// id change -> remount -> every useState inside starts cleanReact unmounts and remounts the component when the key changes, resetting all state inside with zero effect code. This is the pattern the React docs recommend by name.
Step 5: Lift or adjust state when nothing else fits
If the derived value must itself be editable, derive the editable thing instead:
// wrong: store the total, sync it forever
const [total, setTotal] = useState(base);
useEffect(() => setTotal(base + adjustment), [base, adjustment]);
// right: store only what the user changes
const [adjustment, setAdjustment] = useState(0);
const total = base + adjustment; // derived at render, always rightStore the adjustment, not the total. Setting state during render, the guarded previous-value comparison the docs describe, remains for the rare case the value must update in response to its own past, and it has rules: guard it, keep it out of loops, expect a re-render.
The whole rule is one sentence: if you can compute it from what you already have, you do not need to store it. Delete the copy, delete the syncing effect, and the bug class goes with them.
React codebase full of effects that mirror props into state? A derivation pass usually deletes half of them in a day. Tell me about the screen and I will tell you which effects are hiding copies.