usePrevious
A hook that returns the last distinct value (Object.is) before the current one, with a read-only render and an effect-committed snapshot, so values created during render are safe.
Loading preview…
Installation
npx shadcn@latest add https://ui.zyeon.ai/r/use-previous.jsonPrompt
Build a React + TypeScript "usePrevious" hook (no dependencies beyond React).
Contract
- `usePrevious<T>(value: T): T | undefined`.
- Returns `undefined` on the first render for a given hook instance.
- From the second render on, returns whatever `value` was before its last
DISTINCT change — repeated equal values do not advance it (an
equality-gated snapshot, not a strict render-history log). So after the
first change `previous !== value` stays true on every later render: it
says "what was it before the last change", never "did it change in this
render".
Behavior
- Comparison is `Object.is(value, <last committed value>)` — reference
equality, the same check React itself uses for bailing out of state
updates. A new object/array with identical-looking contents counts as a
change; mutating the same object in place and passing the same reference
does not.
- Values created during render are supported input:
`usePrevious({ page, filter })`, `usePrevious(items.map(...))`,
`usePrevious(new Date())`. Each render's fresh reference counts as a
change, so `previous` is simply the object from the last committed render.
- Keep `{ current, previous }` in a `useRef` that starts as `null`. Render
only reads it: `null` → return `undefined`; `Object.is(current, value)` →
return `previous`; otherwise `current` is the previous value. A
`useEffect` keyed on `[value]` commits the shift (`null` → seed with
`{ current: value, previous: undefined }`; a different value →
`{ current: value, previous: current }`). Only committed renders move the
ref, so StrictMode's double render and a discarded concurrent render leave
nothing behind; seeding at the first commit rather than in `useRef(...)`
keeps StrictMode's second mount render from handing an inline object back
as its own "previous".
- Do not use the "adjust state during render" pattern (`setState` in the
render body when `value` differs from a stored `current`): with a value
created during render the adjusting re-render sees another new reference,
calls `setState` again, and React throws "Too many re-renders".
- The render-time ref read needs a scoped `react-hooks/refs` suppression
with a comment saying why it is safe (read-only; writes happen only in the
effect).
- No timer, no listener, nothing to clean up on unmount.
Rendering & styling
- Renders nothing itself — it is a pure value hook. Consumers own all UI,
e.g. compare the returned previous value against the current one to
render an up/down arrow with `text-primary` / `text-destructive`, or check
against `undefined` to special-case the first render.
Customization levers
- Swap `Object.is` for a custom equality function (e.g. shallow-equal for
objects, or a rounding comparator for near-equal floats) if reference
equality is too strict for a given value type.
- Derive a `direction: "up" | "down" | null` from the returned previous value,
as the demo does, when a consumer only cares about the sign of the change
rather than the raw previous value itself.Concepts
- Last distinct value — the hook holds the value before the most recent change, not the value from one render ago; a run of equal renders leaves it where it was, which is exactly what a direction arrow wants. The flip side:
previous !== valuestays true after the first change, so it cannot answer "did this render change it". - Read in render, write after commit — render only reads the committed snapshot and derives the answer; the shift lands in an effect. StrictMode's double render and a discarded concurrent render never move the snapshot, because nothing is written until React commits.
- Why not adjust state during render — calling
setStatein the render body when the value differs loops forever on a value created during render (usePrevious({ page })): every re-render builds a new object, the comparison fails again, and React stops with "Too many re-renders". - Object.is comparison — the hook compares by reference, exactly like React's own state bailout check; a structurally-identical-but-new object registers as a change, and an in-place mutation of the same object does not.
- One value, not a history stack — the hook only ever holds a single previous value; it cannot answer "what was the value three changes ago" (see Not when).
- Derived direction, not stored direction — the demo's up/down arrow is computed fresh every render from
price > previous, not stored as its own piece of state — one less thing that can drift out of sync with the value it describes.