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.
This hook remembers the last value across renders.
Installation
npx open-hook add usePreviousyarn dlx open-hook add usePreviouspnpm dlx open-hook add usePreviousAPI Reference
Parameters
| Prop | Type | Default |
|---|---|---|
value? | T | - |
Returns
| Prop | Type | Default |
|---|---|---|
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
| Metric | Value | Description |
|---|---|---|
| Bundle Size | ~0.3kb | Minified + gzipped |
| Render Impact | Minimal | Only stores reference |
| Memory Usage | Single ref | One previous value stored |
| Performance | Excellent | No complex operations |
Browser Compatibility
| Browser | Supported | Notes |
|---|---|---|
| Chrome (latest) | ✅Yes | Full support |
| Firefox (latest) | ✅Yes | Full support |
| Safari (latest) | ✅Yes | Full support |
| Edge (latest) | ✅Yes | Full support |
| IE 11 | ❌No | useRef/useEffect required |
| Node.js | ✅Yes | SSR 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
Related Hooks
- useDebounce - Debounce value changes
- useLocalStorage - Persist previous values
- useTimeout - Delay operations based on changes
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