React's functional components and hooks revolutionized how we build user interfaces, but they introduced a subtle challenge: unlike class components with lifecycle methods, functional components don't provide a built-in way to access previous prop or state values. When you need to compare a current value against what it was before--a common requirement for animations, transitions, or detecting actual changes--the solution isn't immediately obvious.
This guide explores the patterns and custom hooks that solve this problem elegantly, leveraging React's useRef hook to preserve previous values without triggering unnecessary re-renders. Understanding these patterns is essential for building sophisticated React applications that require change detection and comparison logic.
What you'll learn:
- Why accessing previous values matters in React applications
- How useRef provides non-reactive storage for tracking values
- Patterns for storing previous state and props
- Building a reusable usePrevious custom hook
- Practical applications including animations and form tracking
- Performance considerations for optimal implementation
Why Access Previous Values?
Understanding when and why you need previous values helps clarify which pattern to use.
Common Use Cases
Animation Direction Detection
- Determine if an element should grow or shrink based on value changes
- Create smooth transitions that respond to user input direction
- Animate between states with awareness of the transition path
Form Change Tracking
- Detect when a user has modified a field from its initial value
- Enable/disable save buttons based on actual changes
- Show visual indicators when values have been altered
API Request Optimization
- Only fetch new data when the query actually changes
- Prevent duplicate requests for the same data
- Implement debounced searching with change detection
Scroll and Window Tracking
- Calculate scroll direction and distance
- Detect when window dimensions actually change
- Implement parallax effects with velocity awareness
The useRef Hook: Non-Reactive Storage
The solution to tracking previous values lies in understanding a key property of useRef: changing a ref's current value does NOT trigger a re-render.
How useRef Works
const ref = useRef();
// ref returns an object with a .current property
// ref.current can be assigned any value
ref.current = 'previous value';
// Reading is just as simple
console.log(ref.current); // 'previous value'
Why Refs Are Perfect for Previous Values
| Feature | useState | useRef |
|---|---|---|
| Triggers re-render | Yes | No |
| Persists across renders | Yes | Yes |
| Mutable | No | Yes |
| Good for display values | Yes | No |
| Good for tracking history | No | Yes |
The ref object itself remains stable across renders--only the .current property changes, and it does so without affecting the render cycle. This makes refs ideal for storing values you need to remember but don't want to display.
Key insight: When you store a value in ref.current, it stays there until you explicitly change it, and the component won't re-render just because you updated the ref.
For more on React hooks patterns, see our guide on React form validation with Formik and Yup which demonstrates practical hook combinations in real-world applications.
Storing Previous State Values
For state values, you can use a simple direct assignment pattern:
Direct Assignment Pattern
import { useState, useRef } from 'react';
function Counter() {
const [count, setCount] = useState(0);
const prevCountRef = useRef();
const increment = () => {
// Store current value as previous before updating
prevCountRef.current = count;
setCount(c => c + 1);
};
return (
<div>
<p>Previous: {prevCountRef.current}</p>
<p>Current: {count}</p>
<button onClick={increment}>Increment</button>
</div>
);
}
How it works:
- Create a ref to hold the previous state value
- Before calling the state setter, update the ref with the current value
- The ref.current always contains the value from the previous render
- No useEffect needed because we're updating synchronously before the state change
Multiple State Values
When dealing with multiple pieces of state, create separate refs for each:
const [username, setUsername] = useState('');
const [email, setEmail] = useState('');
const prevUsernameRef = useRef();
const prevEmailRef = useRef();
This keeps tracking logic clean and maintains separation of concerns.
Tracking Previous Props
Props require a different approach because they arrive before the component renders. Direct assignment in the component body would capture stale values due to closures.
The useEffect Pattern
import { useEffect, useRef } from 'react';
function UserProfile({ user }) {
const prevUserRef = useRef();
useEffect(() => {
// After render, store current user as previous
prevUserRef.current = user;
}, [user]); // Runs after user prop changes
return (
<div>
<p>Previous: {prevUserRef.current?.name || 'None'}</p>
<p>Current: {user.name}</p>
</div>
);
}
Why useEffect is Necessary
- Timing: Props arrive before the component renders, so direct assignment would capture the old value
- Closure behavior: React's closure semantics mean values captured in the render body refer to the initial values
- Post-render capture: useEffect runs after the DOM update, giving us access to the actual rendered value
- Dependency tracking: The dependency array ensures we capture each prop change
Handling Complex Prop Types
For objects and arrays, be mindful of reference equality:
useEffect(() => {
prevUserRef.current = user;
}, [user]); // Uses shallow comparison for dependency tracking
If your object references don't change but internal values do, consider using useMemo for stable references or implementing deep comparison logic.
For data fetching patterns that work well with prop-based change detection, see our guide on Next.js data fetching with getServerSideProps and getStaticProps.
Creating a Reusable usePrevious Hook
Rather than repeating the ref + useEffect pattern throughout your codebase, encapsulate it in a custom hook.
The usePrevious Implementation
import { useRef, useEffect } from 'react';
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
}, [value]);
return ref.current;
}
Usage in Components
function Counter() {
const [count, setCount] = useState(0);
const previousCount = usePrevious(count);
return (
<div>
<p>Previous: {previousCount}</p>
<p>Current: {count}</p>
<button onClick={() => setCount(c => c + 1)}>Increment</button>
</div>
);
}
Benefits of the custom hook:
- One-line integration in any component
- Works identically for state and props
- Clear, semantic API: just pass a value, get the previous one
- Returns
undefinedon first render (expected behavior) - Easy to test and maintain
With Props
function UserCard({ user }) {
const prevUser = usePrevious(user);
if (prevUser && prevUser.id === user.id && prevUser.name !== user.name) {
console.log('User name changed!');
}
return <div>{user.name}</div>;
}
Advanced: Returning Both Previous and Current
Some scenarios benefit from having immediate access to both values:
function usePreviousAndCurrent(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
}, [value]);
return { prev: ref.current, current: value };
}
Practical Applications
Form Dirty Tracking:
function EditableField({ initialValue, onSave }) {
const [value, setValue] = useState(initialValue);
const { prev: prevInitial, current: currentValue } = usePreviousAndCurrent(value);
const hasChanged = value !== initialValue;
const didJustChange = prevInitial !== undefined && value !== prevInitial;
return (
<div>
<input value={value} onChange={e => setValue(e.target.value)} />
<button disabled={!hasChanged} onClick={() => onSave(value)}>
Save
</button>
{didJustChange && <span className="notification">Value changed!</span>}
</div>
);
}
API Optimization:
function SearchResults({ query }) {
const { prev: prevQuery, current: currentQuery } = usePreviousAndCurrent(query);
const shouldFetch = query.length >= 3 && prevQuery !== query;
useEffect(() => {
if (shouldFetch) {
fetchResults(query);
}
}, [query, shouldFetch]);
}
Choose the right approach based on your use case
Direct Assignment (State)
Update ref.current before calling setState. Works for state values where you control the update timing. Simple and synchronous.
useEffect + Ref (Props)
Use useEffect to capture prop values after render. The dependency array ensures updates on prop changes. Required for props.
Custom Hook (Universal)
Wrap the pattern in a reusable usePrevious hook. Works for both state and props with a clean, consistent API.
Dual Return (Advanced)
Return both prev and current values for immediate access to both. Useful for comparison logic and change detection.
Performance Considerations
Why Refs Are Efficient
- No re-render overhead: Changing ref.current doesn't trigger component re-renders, so there's no render cycle cost
- Stable object: The ref object is created once and persists across renders
- Fast access: Reading and writing .current is essentially free
- Minimal effect runs: Only the useEffect dependency array can cause effect re-runs
Performance Pitfalls to Avoid
Object Dependencies:
// Bad - creates new object every render
useEffect(() => {
prevRef.current = user;
}, [user]); // user object reference changes frequently
// Good - memoize for stable reference
const stableUser = useMemo(() => user, [user.name, user.email]);
useEffect(() => {
prevRef.current = stableUser;
}, [stableUser]);
Comparison with Alternatives:
| Approach | Re-renders | Complexity | Best For |
|---|---|---|---|
| useRef | None | Low | Simple previous value tracking |
| Class lifecycle | Same as hooks | Medium | Migration scenarios |
| Context | Many | High | Shared state (not recommended) |
| External state | Depends | High | Complex state management |
When to Be Careful
- Avoid using objects/arrays directly in useEffect dependencies when possible
- Memoize stable values that don't need to trigger effects
- Consider useMemo for computed values
- For complex nested objects, implement deep comparison or use libraries like use-deep-compare-effect
1import { useRef, useEffect } from 'react';2 3// Generic usePrevious hook with proper typing4function usePrevious<T>(value: T): T | undefined {5 const ref = useRef<T | undefined>(undefined);6 7 useEffect(() => {8 ref.current = value;9 }, [value]);10 11 return ref.current;12}13 14// Usage with typed values15interface User {16 id: number;17 name: string;18 email: string;19}20 21function UserProfile({ user }: { user: User }) {22 const previousUser = usePrevious<User>(user);23 24 // TypeScript knows previousUser is User | undefined25 if (previousUser && previousUser.id !== user.id) {26 console.log('User changed completely!');27 }28 29 return <div>{user.name}</div>;30}Frequently Asked Questions
Does usePrevious return undefined on the first render?
Yes, this is expected behavior. On the initial render, there is no previous value to return, so the hook returns undefined. You can check for this in your components if you need to handle the initial state differently.
Can I use usePrevious with derived state or memoized values?
Yes, usePrevious works with any value, including derived state and memoized values. Just pass the value to the hook, and it will track changes to that value regardless of its source.
How does usePrevious compare to class component componentDidUpdate?
The underlying mechanism is similar--both capture previous values before the update. Class components had this built-in with prevProps/prevState parameters, while hooks require the useRef + useEffect pattern. The result is functionally equivalent.
Will usePrevious cause memory leaks with long-running components?
No, the ref is tied to the component's lifecycle and will be garbage collected when the component unmounts. There's no additional memory overhead beyond storing the previous value.
Can I track multiple previous values (a history)?
Yes, you can extend the pattern by storing an array in the ref. Push new values to the array and shift old ones to maintain a history. However, for full undo/redo functionality, consider using a dedicated state management approach.
Summary
Accessing previous props and state values in React functional components requires a different approach than class components, but the solution is elegant and performant:
- useRef provides non-reactive storage that persists across renders without triggering re-renders
- Direct ref assignment works for state when you control the update timing
- useEffect + ref is needed for props to capture values after render
- Custom hooks like usePrevious encapsulate the pattern for clean, reusable code
- Refs are the most performant solution with minimal overhead
Common patterns include animation direction detection, form change tracking, and API request optimization. The usePrevious hook pattern has become a standard solution in the React community for this common requirement.
Next steps:
- Add usePrevious to your component utility library
- Identify components in your codebase that could benefit from previous value tracking
- Consider creating specialized hooks for common patterns like usePreviousChange or useDirtyForm
For more advanced React development techniques, explore our web development services or learn about AI-powered automation solutions that can enhance your applications.
Sources
- LogRocket: How to access previous props or state with React Hooks - Comprehensive guide covering useRef patterns, custom hooks, and practical examples
- Telerik: How to Access Previous Props & State Values with React Hooks - Detailed tutorial with code examples for state and props tracking