Understanding the Lifecycle of useRef
in React and Avoiding Stale Reference Bugs¶
React's useRef
hook is often used to persist values across renders without triggering re-renders. However, it's important to understand that useRef
only survives re-renders, not re-mounts. This distinction can lead to subtle and hard-to-diagnose bugs, especially when working with closures or asynchronous logic.
Re-renders vs Remounts: Understanding the Difference¶
Before diving into the useRef
lifecycle, it's crucial to understand the difference between re-renders and remounts in React:
Re-renders occur when React updates an existing component instance in response to state or props changes. During a re-render:
- The component function runs again
- New JSX is generated and compared to the previous render
- The DOM is updated only where necessary (reconciliation)
- Component instance and all hooks (including
useRef
) maintain their identity - Triggered by:
setState
, props changes from parent, context changes, orforceUpdate
Remounts occur when React completely destroys a component instance and creates a new one. During a remount:
- The old component instance is fully discarded
- All hooks are re-initialized from scratch
- The entire component lifecycle starts over (mount → render → effect)
- Any cleanup functions from the old instance are executed
- Triggered by:
key
prop changes, conditional rendering (condition && <Component />
), route navigation, or parent component structure changes
The key insight is that useRef
survives re-renders but gets reset during remounts, which can lead to stale reference bugs when closures outlive the component instance.
useRef
is tied to the component instance¶
When a component re-renders due to state or props changes, useRef
maintains its identity. However, when the component is re-mounted - for example, due to a change in the key
prop, conditional rendering, or route transitions - a new component instance is created, and the old one is discarded. The useRef
is then re-initialized along with the rest of the component.
This becomes problematic when an earlier closure outlives the component itself and references the old ref object. Since that ref will never be updated again, the closure holds a stale reference. These are a few example cases where a closure might outlive the component:
1. Uncancelled timers: setTimeout
or setInterval
callbacks that don't have their timers cleared in cleanup functions
2. Promises: Asynchronous operations that resolve after the component unmounts
3. Recursive calls: Functions that call themselves with timeouts or in response to events or processing results
Example: Logging a stale ref due to a remount¶
import { useEffect, useRef } from 'react';
function MyComponent({ id }) {
const latestIdRef = useRef(id);
useEffect(() => {
latestIdRef.current = id;
}, [id]);
useEffect(() => {
const interval = setInterval(() => {
console.log('Latest ID from ref:', latestIdRef.current);
}, 60000); // 1 minute
// Without cleanup: interval continues after unmount with stale ref
// return () => clearInterval(interval);
}, []);
return <div>Component with ID: {id}</div>;
}
And rendered like this:
<MyComponent key={userId} id={userId} />
In this setup, whenever userId
changes, the key
change forces a full unmount and remount. The new component gets a new useRef
object, but the old setInterval
continues to run and holds a reference to the stale ref from the destroyed component instance. When the interval eventually fires (after 1 minute in this example), it logs the outdated value because the old ref is never updated again. As a result, the logged value is stale.
Timeline example:
1. Component mounts with userId: 1
, interval starts with 1-minute delay
2. After 30 seconds, userId
changes to 2
, triggering unmount/remount, but the interval is not cancelled upon unmount
3. New component instance created with userId: 2
and a new latestIdRef
4. After the full minute, the old interval fires, logging the stale value 1
instead of the current 2
This problem becomes more pronounced with longer timeouts or when remounts happen frequently relative to the interval duration.
Recommendations¶
- Avoid relying on
useRef
for values that need to persist across component unmounts and remounts. - Be cautious when using closures that capture
useRef
values, particularly in asynchronous or interval-based logic. - If persistence across remounts is required, consider lifting state to a shared parent component, using context, or using external state management.
- Prefer lifecycle-safe patterns such as
useEffect
cleanups or observable subscriptions that are scoped to the component's active instance.
Understanding how useRef
behaves with respect to the component lifecycle can help prevent subtle bugs and ensure your application logic remains predictable.
Further readings¶
- JavaScript Closures - MDN - Understanding closures and variable capture
- useRef Hook - Official React documentation for the
useRef
hook - useEffect Hook - Official documentation on
useEffect
and cleanup functions - Lifecycle of Reactive Effects - Understanding React component lifecycle and effects
- Keys in React - How the
key
prop affects component identity - Reconciliation - How React preserves and resets component state
- Learn React - Escape Hatches - Advanced patterns for working with refs and effects