//
Search across all documentation pages
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
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.
"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:
useRef persists across renders without causing re-renders. The useEffect runs after render, so during render ref.current still holds the value from the previous render.ref.current contains the value from render N-1. After render N completes, the effect updates ref.current to the value from render N.ref.current is undefined because no previous value exists yet.| Parameter | Type | Default | Description |
|---|---|---|---|
value | T | - | The value to track |
| Returns | T or undefined | - | Previous render's value, or undefined on first render |
| 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 |
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)
);T | undefined makes the consumer handle the initial-render case. Use the "with initial value" variation to avoid this.T is inferred from the argument, so no explicit type parameter is usually needed.usePreviousDistinct accepts a custom comparator typed as (a: T, b: T) => boolean.usePreviousDistinct with a deep comparison, or memoize the value upstream.undefined is returned. If undefined is 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.usePrevious only tracks one step back. Fix: Use the history stack variation for multi-level undo.| 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 |
undefined and the useEffect that updates it runs after render.undefined is the correct return.ref.current still holds the value from render N-1 because useEffect has not run yet.ref.current to the current value, ready for render N+1.usePrevious updates on every render, even if the value did not change.usePreviousDistinct only updates when the value actually changes (based on an equality check), skipping redundant re-renders from parent components.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);
}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;
}usePrevious captures that new reference even though the data is the same.usePreviousDistinct with a deep comparison, or memoize the value upstream with useMemo.undefined, which is ambiguous.{ hasPrevious: boolean, value: T }.count to prevCount: if count > prevCount the direction is "up", if less it is "down".prevCount is undefined, so the direction is "initial".T | undefined, forcing the consumer to handle the first-render case.T is inferred from the argument, so no explicit type parameter is usually needed.const prev = usePrevious(42);
// prev is number | undefinedThe comparator is typed as (a: T, b: T) => boolean:
const prevUser = usePreviousDistinct(user, (a, b) =>
a.id === b.id && a.name === b.name
);Reviewed by Chris St. John·Last updated Jul 16, 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥