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.
- 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.
usePreviousis a ref that lags the render by one commit. You readref.currentduring render (still the last committed value) and write the new value in an effect after paint.- First render returns
undefinedunless you pass an explicit initial. Name that. Callers that treat “changed” asprev !== nextwill 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:
useprefix, 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:
- Mount with
value = 'a'.ref.currentisundefined. The hook returnsundefined. React commits. The effect writes'a'. - Next render with
value = 'b'. The hook returns'a'. After paint, the effect writes'b'. - 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
usePreviouskeeps last commit’s value in a ref. During render I returnref.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 returnsundefined; 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 on this site
- useRef for Values and DOM: mutable boxes that skip re-renders
- useEffect Fundamentals: effects run after paint
- Rules of Hooks: slots, not names
- Custom Hooks Design: extract stateful logic, not JSX
- useEffect Dependency Array: omitted vs
[value] - Closures: why the effect sees this render’s
value - Implement Function Once: another “first call is special” API
Related guides
- useCopyToClipboard HookCopy text with navigator.clipboard, fallback execCommand, transient copied state, and permissions that fail in insecure contexts.
- useDebouncedValue HookDebounce a changing value in React without firing effects on every keystroke. Cleanup, leading vs trailing, and the stale timer that updates unmounted state.
- useEventListener HookAttach a window, document, or element listener with a stable handler ref so you do not rebind on every render. Capture option and target changes.
- useIdle HookDetect idle users: reset a timer on input events, fire idle after timeout, and decide whether mount starts the timer immediately.
- useInfiniteScroll HookLoad the next page when a sentinel enters the viewport. IntersectionObserver, hasMore, in-flight guards, and the footer that never intersects.