July 29, 2026

By ReactUse Team

React usePrevious Hook: Track Previous State & Props (2026)

React gives you the current value of state and props on every render — and no built-in way to ask what the value was before. So everyone copies the same ten-line recipe: stash the value in a ref inside useEffect, return ref.current. It works in the demo, ships to production, and then one day a “count went up ↑” indicator starts claiming nothing changed — because the component re-rendered for a completely unrelated reason and the ref quietly overwrote its own history.

usePrevious from @reactuses/core tracks the previous distinct value instead of the previous render’s value, using the pattern the React docs themselves recommend — no refs, no effects, no drift. The whole implementation is twelve lines, so this post walks through the real bug in the classic recipe, why the fix looks illegal but isn’t, and the one gotcha that can send it into an infinite loop. TypeScript-first.

The Classic Recipe, and Where It Frays

This is the version that lives in a thousand blog posts and probably a few of your codebases:

function usePrevious<T>(value: T): T | undefined {
  const ref = useRef<T>();
  useEffect(() => {
    ref.current = value;
  });
  return ref.current;
}

Render, then the effect fires and copies the current value into the ref. Next render reads the ref — the value from one render ago. The subtle part is in that last phrase: this hook returns the value from the previous render, not the previous value. Those are the same thing only as long as the only reason your component re-renders is the value changing. It never stays that way.

Watch it break. A counter with a direction indicator, plus one unrelated piece of state:

function Counter() {
  const [count, setCount] = useState(0);
  const [dark, setDark] = useState(false);
  const prevCount = usePrevious(count); // the ref version

  return (
    <div>
      <button onClick={() => setCount(count + 1)}>+1</button>
      <button onClick={() => setDark(!dark)}>toggle theme</button>
      {prevCount !== undefined && prevCount !== count && (
        <span>{count > prevCount ? "↑ went up" : "↓ went down"}</span>
      )}
    </div>
  );
}

Click +1: count is 5, prevCount is 4, the indicator shows ”↑ went up”. Correct. Now click toggle theme: count hasn’t moved, but the component re-renders, the effect runs again, and the ref becomes 5. On the next render prevCount === count, and the indicator vanishes — the component now believes the count never changed. Any parent re-render, context update, or sibling state change does the same thing. The comparison you built the hook for is exactly what the hook corrupts.

This isn’t a hypothetical: it’s the same failure reported against react-use, ahooks, and reactuse itself before the implementation was replaced.

usePrevious — Previous Value, Not Previous Render

import { useState } from 'react';
import { usePrevious } from '@reactuses/core';

function Counter() {
  const [count, setCount] = useState(0);
  const [dark, setDark] = useState(false);
  const prevCount = usePrevious(count);

  return (
    <div>
      <button onClick={() => setCount(count + 1)}>+1</button>
      <button onClick={() => setDark(!dark)}>toggle theme</button>
      <p>Now: {count}, before: {prevCount ?? "—"}</p>
    </div>
  );
}

The signature:

function usePrevious<T>(value: T): T | undefined;

Toggle the theme as many times as you like — prevCount stays 4, because the count never actually changed. On the first render it returns undefined, since there is no prior value yet; type your comparisons as T | undefined accordingly.

The implementation is short enough to quote in full, and it contains no refs and no effects:

export function usePrevious<T>(value: T): T | undefined {
  const [current, setCurrent] = useState<T>(value);
  const [previous, setPrevious] = useState<T>();

  if (value !== current) {
    setPrevious(current);
    setCurrent(value);
  }

  return previous;
}

Wait — setState During Render?

Yes, and it’s not a hack: it’s the pattern from the React docs, under storing information from previous renders. Calling a setter while rendering is legal under two conditions, and this code meets both:

  • It’s the component’s own state. React handles a render-phase update by throwing away the current render’s output and immediately re-running the component with the new state — before touching the DOM, before painting, before any effect runs. No intermediate frame is ever visible.
  • It’s behind a condition that eventually goes quiet. value !== current is true for exactly one render after a change; the re-run then sees value === current and falls through. No loop.

Compare what each version anchors its history to. The ref recipe records “whatever the value was last render” — so every render rewrites history, related or not. The state version records “whatever the value was before it last changed” — unrelated re-renders hit value === current and touch nothing. That single guard is the entire bug fix.

It also holds up under React 18’s stricter execution models, where the effect-and-ref version gets shakier. StrictMode double-invokes render functions in development: here, the second invocation runs the same comparison against the same state and lands in the same place — idempotent. Concurrent features can throw a render away before committing: a discarded render’s state updates are discarded with it, whereas a ref mutation inside a render (the other popular “fix”) escapes the render and leaks into a timeline that officially never happened. State-during-render is the one variant that’s correct under all of it.

The Gotcha: Unstable Objects

The comparison is !== — strict reference equality. Feed the hook a fresh object literal every render and value !== current is always true:

// 💥 Too many re-renders
const prev = usePrevious({ x: position.x, y: position.y });

Every render creates a new object, the guard fires, the render-phase setState triggers a re-run, which creates another new object, and React halts the whole thing with “Too many re-renders.” The fix is the usual referential-stability discipline: pass primitives, or memoize the object so its identity only changes when its contents do:

const point = useMemo(() => ({ x, y }), [x, y]);
const prevPoint = usePrevious(point); // ✅

Primitives — numbers, strings, booleans — are always safe, and they’re what you’re tracking ninety percent of the time. (If your real problem is “run an effect when a deeply-nested object actually changes,” that’s useDeepCompareEffect’s job, not this hook’s.)

usePrevious vs useLatest

The two get confused because both are “a hook holding a value across time,” but they answer opposite questions:

  • usePrevious answers “what was this value before it changed?” — for comparisons during render: direction of change, from/to labels, transition detection.
  • useLatest answers “what is this value right now?” from inside a stale closure — a setInterval callback, a debounced handler, an event listener registered once on mount.

If you’re rendering a diff, you want usePrevious. If a callback keeps seeing an old value, you want useLatest. Needing one is never a sign you need the other.

Real Use Cases

  • Direction of change. Sort arrows, price tickers, scroll direction, ”↑ 3 more than yesterday” — anything rendered from value > prev. This is the case the ref recipe visibly breaks, since one unrelated re-render erases the direction.
  • Transition detection. Fire logic when a prop crosses a boundary, not merely when it’s in a state: prevStatus === "loading" && status === "success" shows a toast exactly once per completed request. Pair with useUpdateEffect to also skip the mount render.
  • From → to animations. Number tickers and chart transitions need both endpoints; usePrevious hands you the starting value for the tween without a second piece of state.
  • “Changed from X to Y” UI. Audit-style forms and settings panels that show what edits are pending — render prev and value side by side; on the first render prev is undefined and you show nothing.

SSR Safety

usePrevious is two useState calls and a comparison — no window, no document, no effects, nothing to guard. On the server it renders once and returns undefined, the client’s first render returns the same undefined, and hydration matches by construction. Unlike hooks that read browser state (a cookie, localStorage, a media query), there is no server/client divergence to design around. It’s SSR-safe the boring way: by not doing anything.

Takeaways

  • The classic useRef + useEffect recipe tracks the previous render, not the previous value — any unrelated re-render silently rewrites it, which is precisely the bug filed against every major hooks library that shipped it.
  • usePrevious uses setState-during-render — the React-docs-sanctioned pattern: conditional, self-terminating, invisible to the user, and correct under StrictMode and concurrent rendering.
  • First render returns undefined — there’s no history yet; type against T | undefined.
  • Comparison is by reference — pass primitives or useMemo-stabilized objects, or the render-phase guard fires forever and React stops you with “Too many re-renders.”
  • Previous value vs current value in a closure are different problems — comparisons want usePrevious; stale callbacks want useLatest.

Grab it from @reactuses/core and let “previous” actually mean previous.