Open Hooks
State Management

usePrevious

Track the previous value of a state or prop in React components with automatic persistence across renders.

usePrevious

Track the previous value of a state or prop in React components with automatic persistence across renders.

Status: stable
Version: 2.0.0
Bundle Size: ~0.3kb
Requires: React 16.8+
0Current
Previousundefined

This hook remembers the last value across renders.

Installation

npx open-hook add usePrevious
yarn dlx open-hook add usePrevious
pnpm dlx open-hook add usePrevious

API Reference

Parameters

PropTypeDefault
value?
T
-

Returns

PropTypeDefault
previousValue?
T | undefined
-

TypeScript Signature

function usePrevious<T>(value: T): T | undefined;

Advanced Usage Examples

function SearchResults() {
  const [query, setQuery] = useState("");
  const [results, setResults] = useState([]);
  const previousQuery = usePrevious(query);

  useEffect(() => {
    if (query !== previousQuery && query.length > 0) {
      // Only search when query actually changed
      searchAPI(query).then(setResults);
    }
  }, [query, previousQuery]);

  return (
    <div>
      <input
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        placeholder="Search..."
      />
      {previousQuery && <p>Previous search: {previousQuery}</p>}
      <div>{/* Render results */}</div>
    </div>
  );
}
interface UserProfileProps {
  userId: string;
  userName: string;
}

function UserProfile({ userId, userName }: UserProfileProps) {
  const previousUserId = usePrevious(userId);
  const previousUserName = usePrevious(userName);

  useEffect(() => {
    if (userId !== previousUserId) {
      // User changed, fetch new profile data
      fetchUserProfile(userId);
    }
  }, [userId, previousUserId]);

  useEffect(() => {
    if (userName !== previousUserName && previousUserName) {
      // Name updated, show notification
      showNotification(`Name changed from ${previousUserName} to ${userName}`);
    }
  }, [userName, previousUserName]);

  return <div>{/* Profile UI */}</div>;
}
function DataProcessor({ data, filters }) {
  const [processedData, setProcessedData] = useState([]);
  const previousData = usePrevious(data);
  const previousFilters = usePrevious(filters);

  useEffect(() => {
    const dataChanged = data !== previousData;
    const filtersChanged =
      JSON.stringify(filters) !== JSON.stringify(previousFilters);

    if (dataChanged || filtersChanged) {
      // Only process when actual changes occurred
      const processed = processData(data, filters);
      setProcessedData(processed);

      if (dataChanged) {
        console.log("Data updated");
      }
      if (filtersChanged) {
        console.log("Filters updated");
      }
    }
  }, [data, filters, previousData, previousFilters]);

  return <div>{/* Render processed data */}</div>;
}
function FormField({ value, onValidate }) {
  const [error, setError] = useState("");
  const previousValue = usePrevious(value);

  useEffect(() => {
    if (value !== previousValue && previousValue !== undefined) {
      // Only validate after user has interacted
      const validationError = validateField(value);
      setError(validationError);
      onValidate(validationError);
    }
  }, [value, previousValue, onValidate]);

  return (
    <div>
      <input value={value} onChange={/* ... */} />
      {error && <span className="error">{error}</span>}
      {previousValue && value !== previousValue && (
        <span className="change-indicator">Changed</span>
      )}
    </div>
  );
}

Best Practices

Use usePrevious strategically for performance optimization and user experience:

// ✅ Good - Track meaningful state changes
const previousUser = usePrevious(user);

// ✅ Good - Optimize expensive operations
if (data !== previousData) {
  performExpensiveCalculation(data);
}

// ❌ Avoid - Tracking every render
const previousRenderCount = usePrevious(renderCount++);

Performance Guidelines

  • 🎯 Selective Tracking: Only track values that need comparison
  • 🔄 Effect Optimization: Use with useEffect to prevent unnecessary operations
  • 📊 State Monitoring: Track critical state changes for analytics
  • 🎨 UI Feedback: Show users what changed in forms or interfaces

Do's and Don'ts

  • ✅ Use for expensive operation optimization
  • ✅ Track prop changes in child components
  • ✅ Implement undo/redo functionality
  • ✅ Create smooth UI transitions
  • ❌ Don't track every single state variable
  • ❌ Avoid deep object comparisons without memoization
  • ❌ Don't use for simple boolean toggles
  • ❌ Avoid in every component unnecessarily

Performance Metrics

MetricValueDescription
Bundle Size~0.3kbMinified + gzipped
Render ImpactMinimalOnly stores reference
Memory UsageSingle refOne previous value stored
PerformanceExcellentNo complex operations

Browser Compatibility

BrowserSupportedNotes
Chrome (latest)YesFull support
Firefox (latest)YesFull support
Safari (latest)YesFull support
Edge (latest)YesFull support
IE 11NouseRef/useEffect required
Node.jsYesSSR compatible

Use Cases

State Change Detection

Compare current and previous state to trigger specific actions.

Undo/Redo Systems

Store previous values for implementing undo functionality.

Performance Optimization

Prevent unnecessary re-computations or API calls.

UI Transitions

Create smooth animations based on value changes.

Accessibility

Accessibility Considerations

  • Use previous values to announce changes to screen readers - Track focus states for better keyboard navigation - Monitor form field changes for validation feedback - Implement accessible undo/redo announcements

Troubleshooting

Internals

How It Works

The hook uses useRef to store the previous value and useEffect to update it after each render. The key insight is that useEffect runs after the render, so we can capture the current value as the "previous" value for the next render cycle.

This creates a one-render delay, which is exactly what we want for tracking previous values.

Testing Example

import { renderHook } from "@testing-library/react";
import { usePrevious } from "./usePrevious";

describe("usePrevious", () => {
  it("should return undefined on first render", () => {
    const { result } = renderHook(() => usePrevious("initial"));

    expect(result.current).toBeUndefined();
  });

  it("should return previous value on subsequent renders", () => {
    const { result, rerender } = renderHook(({ value }) => usePrevious(value), {
      initialProps: { value: "first" },
    });

    expect(result.current).toBeUndefined();

    rerender({ value: "second" });
    expect(result.current).toBe("first");

    rerender({ value: "third" });
    expect(result.current).toBe("second");
  });

  it("should work with complex objects", () => {
    const obj1 = { id: 1, name: "Object 1" };
    const obj2 = { id: 2, name: "Object 2" };

    const { result, rerender } = renderHook(({ value }) => usePrevious(value), {
      initialProps: { value: obj1 },
    });

    expect(result.current).toBeUndefined();

    rerender({ value: obj2 });
    expect(result.current).toBe(obj1);
  });
});

FAQ

Changelog

  • 2.0.0 — Enhanced TypeScript support, improved documentation, added comprehensive examples and testing
  • 1.1.0 — Added proper TypeScript generics and better type inference
  • 1.0.0 — Initial release with basic previous value tracking functionality