usePrevious
A hook that returns the last distinct value (Object.is) before the current one, via render-time state adjustment instead of a ref+effect.
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 during the
moment before the last DISTINCT change — repeated equal values do not
advance it (an equality-gated snapshot, not a strict render-history log).
Behavior
- Comparison is `Object.is(value, <currently tracked 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.
- Implemented as one piece of state, `{ current, previous }`, not a `useRef`
+ `useEffect` pair: when a render observes `value !== current` (via
`Object.is`), it calls `setState` right there in the render body to shift
`current` into `previous` before paint — React's documented "adjust state
during render" pattern. This keeps the update inside the same render pass,
so the result is deterministic under StrictMode's double-invoke and safe
if React discards and retries a concurrent render. The ref+effect version
instead writes `.current` from inside an effect, one commit after the
render that reads it — the ordering across StrictMode/concurrent renders
is not well-defined, and reading a ref's `.current` synchronously during
render is a smell most hook lint setups (including this repo's) call out.
- No effect, no ref, no timer, and 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
- Render-time state adjustment — the React-documented pattern of calling
setStateconditionally inside the render body (not insideuseEffect) to derive one piece of state from another; React detects the change and re-renders immediately, in the same commit, instead of committing stale UI and correcting it a tick later. - 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.
- Why not useRef + useEffect — that version writes
ref.current = valueafter the render commits, so the render that readsref.currentis always racing the effect that last wrote it; under StrictMode's double-invoked effects or an interrupted/resumed concurrent render, which write "wins" is not guaranteed. It also means reading a ref's.currentsynchronously during render, which this repo's lint setup flags. - One render behind, not a history stack — the hook only ever holds the single immediately-preceding value; it cannot answer "what was the last different 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.