ESC

Type to search the knowledge base.

usePrevious Hook

Keep last render’s value in a ref. Why an effect-after-paint is correct, and the first-render undefined that interviewers wait for you to name.

beginner6 min read
  • react
  • hooks
  • refs
  • interview

You need to know whether count went up or down, whether a modal just opened, or whether userId changed from the value you already rendered. useState only gives you this render. There is no built-in “previous props.”

usePrevious stores last commit’s value in a ref and returns it during the next render. The implementation is three lines. The interview is the timing: when the ref is written, what the first render returns, and why doing the write during render is the wrong instinct under concurrent React.

If you assign ref.current = value in the function body and then return ref.current, you have already overwritten the previous value. If you write during render and React discards that render, the ref still moved. Effects run after a commit, so they track committed values.

  • usePrevious is a ref that lags the render by one commit. You read ref.current during render (still the last committed value) and write the new value in an effect after paint.
  • First render returns undefined unless you pass an explicit initial. Name that. Callers that treat “changed” as prev !== next will fire on mount.
  • Updating the ref in useEffect (not during render) is correct because discarded concurrent renders never run that effect.
  • Decide whether you want “value from the last render” (no dep array) or “last value that failed Object.is” ([value]).
  • This is a custom hook: use prefix, hooks called unconditionally, no JSX.

A ref that lags the render by one commit

import { useEffect, useRef } from 'react';

function usePrevious(value) {
  const ref = useRef(undefined);

  useEffect(() => {
    ref.current = value;
  });

  return ref.current;
}

Walk one update:

  1. Mount with value = 'a'. ref.current is undefined. The hook returns undefined. React commits. The effect writes 'a'.
  2. Next render with value = 'b'. The hook returns 'a'. After paint, the effect writes 'b'.
  3. Next render with 'b' again. The hook returns 'b' (same as current, because nothing changed). After paint, the effect writes 'b' again.

The omitted dependency array is intentional: you want every commit, not only when value changes by Object.is. A parent can re-render with the same 'b' and you still want “the value from last time we rendered,” which happens to equal the current one.

If you pass [value], the effect skips writes when the value is Object.is-equal. That is “previous distinct value,” which is a different hook. Say which one you wrote.

Why the write waits until after paint

useRef is a mutable box. Changing current does not re-render. That is why it can hold “last time” without looping.

useEffect runs after the browser paints the committed tree. Sequence for a successful update:

Step value in scope ref.current What you return
Render (this commit) 'b' 'a' (from last effect) 'a'
Commit + paint 'b' 'a' already returned
Effect 'b' becomes 'b' n/a
Next render 'c' 'b' 'b'

The tempting “during render” version:

function usePreviousBroken(value) {
  const ref = useRef(undefined);
  const prev = ref.current;
  ref.current = value; // side effect in render
  return prev;
}

It often looks right in a synchronous demo. Concurrent React can render, throw away, render again. The discarded render already mutated the ref, so the surviving render sees the wrong previous. Effects do not run for discarded renders. Keep the mutation in the effect.

useLayoutEffect also runs after commit (before paint). For “previous value” you do not need layout timing; useEffect is enough and avoids extra layout work.

First render is undefined

Interviewers wait for this sentence: on the first render there is no previous value, so the hook returns undefined.

function Price({ amount }) {
  const prev = usePrevious(amount);

  useEffect(() => {
    if (prev === undefined) return; // mount, not a change
    if (prev < amount) ping('up');
  }, [amount, prev]);

  return <span>{amount}</span>;
}

Without the undefined guard, mount looks like a change from undefined to 0 (or whatever). That is a real production bug in “scroll to new message,” “animate only on update,” and “fetch when id changes but not on mount” logic.

You can accept an initial previous:

function usePrevious(value, initial) {
  const ref = useRef(initial);
  useEffect(() => {
    ref.current = value;
  });
  return ref.current;
}

Pass initial only when a fake previous is meaningful. Most callers should handle undefined.

Compare, don’t store copies you don’t need

usePrevious holds the same reference you passed. If that reference is a new object every render, previous and current are never ===, even when the fields match. Either compare with a deep/shallow helper, or previous a primitive you derived (user.id, items.length).

const prevId = usePrevious(user.id);
const changed = prevId !== undefined && prevId !== user.id;

Footguns

Using previous as a substitute for derived state. If you can compute direction from amount and a ref you update in the same event handler, you may not need a hook at all. usePrevious is for “after a render, what was on screen last time.”

Putting the hook behind a condition. if (enabled) { const prev = usePrevious(x); } breaks Rules of Hooks. Always call it; ignore the result when disabled.

Stale closures in the same render. The returned previous is for this render’s JSX and effects scheduled from this render. Logging prev inside a setTimeout created later will still see that render’s previous unless you also keep a ref.

Confusing with useState. If the UI must show “was 4, now 5,” previous has to be state (or you render both from current state). A ref does not trigger a re-render. usePrevious is for logic, not for painting the old number unless something else already re-rendered.

Interview out-loud answer

usePrevious keeps last commit’s value in a ref. During render I return ref.current, which is still the old value. After paint, an effect writes the current value into the ref. I do not write during render, because concurrent renders can be discarded and would still mutate the ref. The first render returns undefined; any “on change” logic has to ignore that. If I add [value] to the effect I get last distinct value, not last render. Each call has its own ref; the hook is not a singleton.

Further reading

Related guides