usePrevious — Track the previous value of any state or prop
Recipe
import { useRef, useEffect } from "react";
/**
* usePrevious
* Returns the value from the previous render.
* Returns `undefined` on the first render.
*/
function usePrevious<T>(value: T): T | undefined {
const ref = useRef<T | undefined>(undefined);
useEffect(() => {
ref.current = value;
}, [value]);
return ref.current;
}
/**
* usePreviousDistinct
* Only updates when the value actually changes
* (skips re-renders where the value stays the same).
*/
function usePreviousDistinct<T>(
value: T,
isEqual: (a: T, b: T) => boolean = (a, b) => a === b
): T | undefined {
const prevRef = useRef<T | undefined>(undefined);
const currentRef = useRef<T>(value);
if (!isEqual(currentRef.current, value)) {
prevRef.current = currentRef.current;
currentRef.current = value;
}
return prevRef.current;
}When to reach for this: You need to compare current and previous values to detect direction of change, trigger animations, implement undo, or skip redundant side effects.
Working Example
"use client";
import { useState } from "react";
// Detect animation direction
function Counter() {
const [count, setCount] = useState(0);
const prevCount = usePrevious(count);
const direction =
prevCount === undefined
? "initial"
: count > prevCount
? "up"
: count < prevCount
? "down"
: "same";
return (
<div>
<p>
Count: {count} (was: {prevCount ?? "N/A"})
</p>
<p>Direction: {direction}</p>
<button onClick={() => setCount((c) => c + 1)}>+1</button>
<button onClick={() => setCount((c) => c - 1)}>-1</button>
</div>
);
}
// Detect route changes
function RouteChangeDetector({ pathname }: { pathname: string }) {
const prevPathname = usePrevious(pathname);
useEffect(() => {
if (prevPathname && prevPathname !== pathname) {
console.log(`Navigated from ${prevPathname} to ${pathname}`);
// Track page view, scroll to top, etc.
}
}, [pathname, prevPathname]);
return null;
}
// Simple undo for a text input
function UndoableInput() {
const [text, setText] = useState("");
const prevText = usePrevious(text);
const undo = () => {
if (prevText !== undefined) {
setText(prevText);
}
};
return (
<div>
<input
value={text}
onChange={(e) => setText(e.target.value)}
placeholder="Type something..."
/>
<button onClick={undo} disabled={prevText === undefined}>
Undo
</button>
<p style={{ color: "#999" }}>Previous: {prevText ?? "none"}</p>
</div>
);
}What this demonstrates:
- Counter direction: Compares current and previous count to determine if it went up, down, or stayed the same
- Route changes: Detects navigation by comparing pathname prop across renders
- Single-level undo: Restores the previous value of a text input
Deep Dive
How It Works
- Ref timing:
useRefpersists across renders without causing re-renders. TheuseEffectruns after render, so during renderref.currentstill holds the value from the previous render. - Render cycle: On render N,
ref.currentcontains the value from render N-1. After render N completes, the effect updatesref.currentto the value from render N. - First render: On the initial render,
ref.currentisundefinedbecause no previous value exists yet. - usePreviousDistinct: Compares values during render (not in an effect) to skip updates when the value has not actually changed. Useful when a parent re-renders without changing the prop.
Parameters & Return Values
usePrevious
| Parameter | Type | Default | Description |
|---|---|---|---|
value | T | — | The value to track |
| Returns | T or undefined | — | Previous render's value, or undefined on first render |
usePreviousDistinct
| Parameter | Type | Default | Description |
|---|---|---|---|
value | T | — | The value to track |
isEqual | (a: T, b: T) => boolean | === | Custom equality check |
| Returns | T or undefined | — | Previous distinct value |
Variations
With initial value: Avoid undefined on first render:
function usePrevious<T>(value: T, initialValue: T): T {
const ref = useRef<T>(initialValue);
useEffect(() => {
ref.current = value;
}, [value]);
return ref.current;
}History stack: Track multiple previous values for multi-level undo:
function usePreviousValues<T>(value: T, maxHistory: number = 10): T[] {
const historyRef = useRef<T[]>([]);
useEffect(() => {
historyRef.current = [value, ...historyRef.current].slice(0, maxHistory);
}, [value, maxHistory]);
return historyRef.current.slice(1); // Exclude current value
}Object comparison: For objects, use a deep comparison function:
const prevUser = usePreviousDistinct(user, (a, b) =>
JSON.stringify(a) === JSON.stringify(b)
);TypeScript Notes
- The return type
T | undefinedmakes the consumer handle the initial-render case. Use the "with initial value" variation to avoid this. - Generic
Tis inferred from the argument, so no explicit type parameter is usually needed. usePreviousDistinctaccepts a custom comparator typed as(a: T, b: T) => boolean.
Gotchas
- One render behind — The returned value is always from the previous render, not the previous state update. If multiple state updates happen in one render (batching), only the final rendered value is captured. Fix: This is by design; the hook tracks renders, not state transitions.
- Object references — For objects and arrays, "previous" changes on every render if a new reference is created each time. Fix: Use
usePreviousDistinctwith a deep comparison, or memoize the value upstream. - undefined ambiguity — On first render,
undefinedis returned. Ifundefinedis a valid value for your data, you cannot distinguish "no previous" from "previous was undefined." Fix: Use the "with initial value" variation or wrap in a{ hasPrevious, value }object. - Undo is single-level —
usePreviousonly tracks one step back. Fix: Use the history stack variation for multi-level undo. - Effect timing with Strict Mode — In React 18 Strict Mode, effects run twice in development. The ref updates correctly because the second effect overwrites the first. Fix: No action needed; production behavior is correct.
Alternatives
| Package | Hook Name | Notes |
|---|---|---|
usehooks-ts | usePrevious | Identical implementation |
@uidotdev/usehooks | usePrevious | Minimal |
ahooks | usePrevious | Supports custom comparison |
react-use | usePrevious | Simple ref-based |
| React docs | — | Recommended as a custom hook example |
FAQs
Why does usePrevious return undefined on the first render?
- The ref is initialized to
undefinedand theuseEffectthat updates it runs after render. - On the first render, no previous value exists yet, so
undefinedis the correct return.
How does the ref timing make usePrevious work?
- During render N,
ref.currentstill holds the value from render N-1 becauseuseEffecthas not run yet. - After render N completes, the effect updates
ref.currentto the current value, ready for render N+1.
What is the difference between usePrevious and usePreviousDistinct?
usePreviousupdates on every render, even if the value did not change.usePreviousDistinctonly updates when the value actually changes (based on an equality check), skipping redundant re-renders from parent components.
How do you implement multi-level undo instead of single-level?
Use a history stack variation:
function usePreviousValues<T>(value: T, max = 10): T[] {
const historyRef = useRef<T[]>([]);
useEffect(() => {
historyRef.current = [value, ...historyRef.current].slice(0, max);
}, [value, max]);
return historyRef.current.slice(1);
}How do you avoid returning undefined on the first render?
Use the "with initial value" variation:
function usePrevious<T>(value: T, initialValue: T): T {
const ref = useRef<T>(initialValue);
useEffect(() => { ref.current = value; }, [value]);
return ref.current;
}Gotcha: Why does usePrevious return a new "previous" on every render for objects?
- If the parent creates a new object reference on each render,
usePreviouscaptures that new reference even though the data is the same. - Fix: use
usePreviousDistinctwith a deep comparison, or memoize the value upstream withuseMemo.
Gotcha: Can you distinguish "no previous value" from "previous was undefined" using the basic hook?
- No. Both cases return
undefined, which is ambiguous. - Fix: use the "with initial value" variation or wrap the return in
{ hasPrevious: boolean, value: T }.
Does usePrevious track renders or state transitions?
- It tracks renders. If multiple state updates are batched into one render, only the final rendered value is captured.
- This is by design and matches React's rendering model.
How does the Counter example detect direction of change?
- It compares
counttoprevCount: ifcount > prevCountthe direction is "up", if less it is "down". - On the first render,
prevCountisundefined, so the direction is "initial".
How does TypeScript handle the return type of usePrevious?
- The return type is
T | undefined, forcing the consumer to handle the first-render case. - The generic
Tis inferred from the argument, so no explicit type parameter is usually needed.
const prev = usePrevious(42);
// prev is number | undefinedHow do you use a custom equality function with usePreviousDistinct in TypeScript?
The comparator is typed as (a: T, b: T) => boolean:
const prevUser = usePreviousDistinct(user, (a, b) =>
a.id === b.id && a.name === b.name
);Why does usePreviousDistinct compare values during render instead of in an effect?
- Comparing during render allows it to update refs synchronously before the component returns JSX.
- If it used an effect, the ref update would be delayed until after render, defeating the purpose of skipping unchanged values.
Related
- useRef — the underlying primitive
- useEffect — timing of the ref update
- useLocalStorage — persist state for undo across sessions