Skip to content

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, or forceUpdate

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