React SME Cookbook
All FAQs

Search Documentation

Search across all documentation pages

previousstaterefanimationcomparisoncustom-hook

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: 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.
  • Render cycle: On render N, ref.current contains the value from render N-1. After render N completes, the effect updates ref.current to the value from render N.
  • First render: On the initial render, ref.current is undefined because 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

ParameterTypeDefaultDescription
valueTThe value to track
ReturnsT or undefinedPrevious render's value, or undefined on first render

usePreviousDistinct

ParameterTypeDefaultDescription
valueTThe value to track
isEqual(a: T, b: T) => boolean===Custom equality check
ReturnsT or undefinedPrevious 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 | undefined makes the consumer handle the initial-render case. Use the "with initial value" variation to avoid this.
  • Generic 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.

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 usePreviousDistinct with a deep comparison, or memoize the value upstream.
  • undefined ambiguity — On first render, 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.
  • Undo is single-levelusePrevious only 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

PackageHook NameNotes
usehooks-tsusePreviousIdentical implementation
@uidotdev/usehooksusePreviousMinimal
ahooksusePreviousSupports custom comparison
react-useusePreviousSimple ref-based
React docsRecommended as a custom hook example

FAQs

Why does usePrevious return undefined on the first render?
  • The ref is initialized to undefined and the useEffect that updates it runs after render.
  • On the first render, no previous value exists yet, so undefined is the correct return.
How does the ref timing make usePrevious work?
  • During render N, ref.current still holds the value from render N-1 because useEffect has not run yet.
  • After render N completes, the effect updates ref.current to the current value, ready for render N+1.
What is the difference between usePrevious and usePreviousDistinct?
  • 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.
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, usePrevious captures that new reference even though the data is the same.
  • Fix: use usePreviousDistinct with a deep comparison, or memoize the value upstream with useMemo.
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 count to prevCount: if count > prevCount the direction is "up", if less it is "down".
  • On the first render, prevCount is undefined, 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 T is inferred from the argument, so no explicit type parameter is usually needed.
const prev = usePrevious(42);
// prev is number | undefined
How 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.