Hooks

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.

Preview in your theme

Loading preview…

"use client"

import * as React from "react"

/**
 * Tracks the LAST DISTINCT value (compared with `Object.is`) — repeated
 * equal values do not advance it, so after a "flat" update `previous` still
 * holds the value from before the last real change (which is exactly what
 * direction indicators want). Returns `undefined` on the very
 * first render — there is no "previous" before the first one.
 *
 * Because it is the last *distinct* value, `previous !== value` stays true on
 * every render after the first change; it cannot tell you whether the value
 * changed in *this* render.

Installation

npx shadcn@latest add https://ui.zyeon.ai/r/use-previous.json

Prompt

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 !== value stays 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 setState in 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.

On This Page