Back to Blog
Lesson 2 of the Intermediate React: Hooks, State & Data Patterns course
ReactJune 25, 20264 min read

Persistent Mutable Values with useRef: Managing React State

Learn to use useRef for persistent mutable values that don't trigger re-renders. Master tracking props and solving stale closures in your React projects.

ReactuseRefhooksstateperformancejavascriptfrontend

Previously in this course, we covered React useRef Hook: Mastering DOM Access and Mutable State, where we used refs primarily to interact with the DOM. This lesson builds on that foundation by exploring how to use useRef as a "box" for instance variables—data that needs to persist across the entire lifetime of a component without causing a re-render when it changes.

When we talk about mutable state that doesn't trigger UI updates, we are talking about escaping the standard "render-cycle" flow of React. While useState is for data that drives the view, useRef is for data that supports the logic behind the scenes.

Why Persistent Mutable Values Matter

In a standard React component, every time a function component re-runs, all local variables are re-initialized. If you define let count = 0 inside your component, it resets to 0 every time the component renders.

useRef solves this by providing a stable object that React keeps alive for the lifetime of the component. It's essentially a persistent container: { current: initialValue }. Because changing the .current property does not trigger a re-render, it is the perfect place for values that don't affect the UI directly but are necessary for internal component logic.

1. Tracking Previous Prop Values

A classic use case is comparing current props to previous ones. Since React doesn't provide a built-in usePrevious hook, we build it using useRef.

JAVASCRIPT
import { useRef, useEffect } from CE9178">'react';

function usePrevious(value) {
  const ref = useRef();

  useEffect(() => {
    ref.current = value;
  }, [value]);

  return ref.current;
}

In this pattern, the useEffect runs after the render, updating our "box" with the latest value. When the component re-renders next, the ref.current still holds the value from the previous render.

2. Managing Timers and Interval IDs

If you start a setInterval in a component, you need the ID to clear it later. Storing that ID in state would be a mistake because updating the ID shouldn't trigger a re-render.

JAVASCRIPT
function TimerComponent() {
  const intervalRef = useRef(null);

  const startTimer = () => {
    intervalRef.current = setInterval(() => {
      console.log(CE9178">'Tick');
    }, 1000);
  };

  const stopTimer = () => {
    clearInterval(intervalRef.current);
  };

  return (
    <>
      <button onClick={startTimer}>Start</button>
      <button onClick={stopTimer}>Stop</button>
    </>
  );
}

3. Preventing Stale Closures

Stale closures occur when an asynchronous callback (like a setTimeout or an event listener) captures a variable from an older render. By putting that variable into a ref, you ensure the callback always accesses the most up-to-date version of the value.

Hands-on Exercise: Dashboard Sync Status

In our ongoing dashboard project, let's track whether a data-fetching process has been initialized.

Task: Create a component that tracks the number of times a "Refresh" button has been clicked, but also keep a ref that stores the last timestamp of the fetch. Use this ref to prevent "double-taps" if the user clicks too quickly (e.g., ignore clicks if they occur less than 500ms apart).

  1. Initialize a lastFetchTime ref to 0.
  2. On click, compare Date.now() with lastFetchTime.current.
  3. If the difference is > 500, update the ref and proceed with the logic.

Common Pitfalls

  • Reading/Writing during render: Never read or write ref.current during the actual rendering of your component (the body of the function). This makes your component's behavior unpredictable. Only interact with refs in useEffect or event handlers.
  • Assuming refs cause re-renders: A common mistake is expecting the UI to update when you change ref.current. If the data is needed for the UI, use useState. If it's for internal logic, use useRef.
  • Overusing refs for everything: Just because you can store variables in a ref doesn't mean you should. If you find yourself needing to store complex state that depends on previous values, useReducer is often a better, more predictable choice.

Recap

  • useRef creates a stable, mutable container that persists across re-renders.
  • It is ideal for storing IDs (timers, event listeners) or values that don't need to trigger UI updates.
  • Use the "ref-in-effect" pattern to track previous props or solve stale closure issues.
  • Always keep ref interactions inside useEffect or event handlers to maintain React rendering: Why state updates re-run your components consistency.

Up next: We will explore how to use useMemo to cache expensive calculations and prevent unnecessary work, keeping our dashboard snappy and responsive.

Similar Posts