# How to Use useRef for Managing Mutable Values Without Re-renders

If you've spent any time working with React hooks, you've probably encountered situations where you need to store a value that persists across renders but doesn't need to trigger a re-render when it changes. That's exactly where [`useRef`](https://react.dev/reference/react/useRef) shines, and understanding when and how to use it can make your React applications significantly more efficient.

## What Makes useRef Different?

At first glance, `useRef` might seem similar to `useState`. Both persist values across renders, both are React hooks, and both are tools for managing component state. But here's the crucial difference: when you update a ref, your component doesn't re-render. When you update state with `useState`, it does.

Think of `useRef` as a box that holds a value. You can put something in the box, take it out, or replace it with something else—all without React caring about what you're doing. The component won't re-render, no matter how many times you change what's inside.

```javascript
const countRef = useRef(0);

// This will NOT cause a re-render
countRef.current = countRef.current + 1;
```

Compare this to `useState`:

```javascript
const [count, setCount] = useState(0);

// This WILL cause a re-render
setCount(count + 1);
```

## When Should You Actually Use useRef?

The question isn't just "can I use `useRef`?" but "should I use `useRef`?" Here are the scenarios where it makes the most sense.

### Accessing DOM Elements Directly

This is probably the most common use case, and it's the one that feels most natural coming from vanilla JavaScript. Sometimes you just need direct access to a DOM node—maybe to focus an input, measure an element's dimensions, or integrate with a third-party library that expects a DOM reference.

```javascript
function TextInputWithFocus() {
  const inputRef = useRef(null);

  const handleFocus = () => {
    inputRef.current.focus();
  };

  return (
    <div>
      <input ref={inputRef} type="text" />
      <button onClick={handleFocus}>Focus the input</button>
    </div>
  );
}
```

This pattern is clean, straightforward, and doesn't introduce any unnecessary re-renders.

### Storing Previous Values

One particularly useful pattern is storing the previous value of a prop or state. This lets you compare current and previous values without the complexity of additional state management.

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

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

  return ref.current;
}

function Counter({ count }) {
  const previousCount = usePrevious(count);

  return (
    <div>
      <p>Current: {count}</p>
      <p>Previous: {previousCount}</p>
      <p>Changed by: {count - (previousCount || 0)}</p>
    </div>
  );
}
```

This works because `useEffect` runs after the render, so `ref.current` still holds the old value during the render phase.

### Managing Timers and Intervals

If you've ever tried to clear a timer in a React component, you know the pain of losing the timer ID between renders. `useRef` solves this elegantly.

```javascript
function Timer() {
  const [seconds, setSeconds] = useState(0);
  const intervalRef = useRef(null);

  const startTimer = () => {
    if (intervalRef.current !== null) return;
    
    intervalRef.current = setInterval(() => {
      setSeconds(s => s + 1);
    }, 1000);
  };

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

  useEffect(() => {
    return () => {
      if (intervalRef.current !== null) {
        clearInterval(intervalRef.current);
      }
    };
  }, []);

  return (
    <div>
      <p>Seconds: {seconds}</p>
      <button onClick={startTimer}>Start</button>
      <button onClick={stopTimer}>Stop</button>
    </div>
  );
}
```

The ref keeps track of the interval ID across renders, and we can reliably clear it when needed.

### Avoiding Stale Closures

Here's where things get interesting. Sometimes you need the latest value of a prop or state inside a callback that doesn't re-create on every render. This is where refs can save you from the dreaded stale closure problem.

```javascript
function SearchComponent() {
  const [query, setQuery] = useState('');
  const latestQueryRef = useRef(query);

  useEffect(() => {
    latestQueryRef.current = query;
  }, [query]);

  const handleSearch = useCallback(() => {
    // This always has the latest query value
    // even though handleSearch doesn't change
    fetch(`/api/search?q=${latestQueryRef.current}`)
      .then(response => response.json())
      .then(data => console.log(data));
  }, []); // Empty deps - function never recreates

  return (
    <div>
      <input 
        value={query} 
        onChange={(e) => setQuery(e.target.value)} 
      />
      <button onClick={handleSearch}>Search</button>
    </div>
  );
}
```

## Common Pitfalls and How to Avoid Them

### Don't Read Refs During Render

This is the biggest mistake developers make with `useRef`. Reading `ref.current` during the render phase can lead to inconsistent behavior because refs don't trigger re-renders.

```javascript
// ❌ Bad - reading ref during render
function BadExample() {
  const countRef = useRef(0);
  
  const increment = () => {
    countRef.current++;
  };

  // This will always show 0 because changing the ref doesn't re-render
  return <div>{countRef.current}</div>;
}

// ✅ Good - use state for values that affect the UI
function GoodExample() {
  const [count, setCount] = useState(0);
  
  const increment = () => {
    setCount(count + 1);
  };

  return <div>{count}</div>;
}
```

### Remember: Refs are Synchronous

Unlike state updates, which React batches and processes asynchronously, <mark> ref updates happen immediately and synchronously.</mark>

```javascript
function Example() {
  const ref = useRef(0);
  
  const handleClick = () => {
    ref.current = 1;
    console.log(ref.current); // Logs 1 immediately
    
    ref.current = 2;
    console.log(ref.current); // Logs 2 immediately
  };
}
```

This can be both a blessing and a curse. It's great when you need immediate access to a value, but it means you need to be careful about when and how you update refs.

## useRef vs useState: Making the Right Choice

The decision between `useRef` and `useState` comes down to one key question: does this value need to trigger a re-render when it changes?

Use `useState` when:

* The value affects what's rendered on screen
    
* You need React to respond to changes in the value
    
* The value is part of your component's visual state
    

Use `useRef` when:

* The value doesn't affect the rendered output
    
* You need to store metadata about your component
    
* You're interacting with DOM elements directly
    
* You need to avoid triggering re-renders for performance
    

## Advanced Pattern: Combining useRef with useEffect

One powerful pattern is using refs to track whether a component has mounted or to prevent certain effects from running on the initial render.

```javascript
function ComponentWithMountCheck() {
  const [data, setData] = useState(null);
  const hasMounted = useRef(false);

  useEffect(() => {
    if (!hasMounted.current) {
      hasMounted.current = true;
      return;
    }

    // This only runs on updates, not on mount
    console.log('Data changed:', data);
    saveDataToLocalStorage(data);
  }, [data]);

  return <div>{/* Your component */}</div>;
}
```

This pattern is particularly useful when you want different behavior on mount versus subsequent updates.

## Performance Considerations

Using `useRef` appropriately can significantly improve your component's performance. Since refs don't cause re-renders, they're perfect for storing values that change frequently but don't need to update the UI.

Consider a scroll position tracker:

```javascript
function ScrollTracker() {
  const scrollPositionRef = useRef(0);
  const [visibleSection, setVisibleSection] = useState('top');

  useEffect(() => {
    const handleScroll = () => {
      scrollPositionRef.current = window.scrollY;
      
      // Only update state (and trigger re-render) when crossing thresholds
      if (scrollPositionRef.current > 500 && visibleSection !== 'bottom') {
        setVisibleSection('bottom');
      } else if (scrollPositionRef.current <= 500 && visibleSection !== 'top') {
        setVisibleSection('top');
      }
    };

    window.addEventListener('scroll', handleScroll);
    return () => window.removeEventListener('scroll', handleScroll);
  }, [visibleSection]);

  return <div>Current section: {visibleSection}</div>;
}
```

Here, we're tracking every scroll event with a ref (no re-renders), but only updating state when it actually matters for the UI.

## Real-World Example: Debounced Search

Let's put it all together with a practical example that combines several concepts:

```javascript
function DebouncedSearch() {
  const [searchTerm, setSearchTerm] = useState('');
  const [results, setResults] = useState([]);
  const timeoutRef = useRef(null);
  const requestRef = useRef(null);

  const performSearch = async (term) => {
    if (requestRef.current) {
      requestRef.current.abort();
    }

    const controller = new AbortController();
    requestRef.current = controller;

    try {
      const response = await fetch(`/api/search?q=${term}`, {
        signal: controller.signal
      });
      const data = await response.json();
      setResults(data);
    } catch (error) {
      if (error.name !== 'AbortError') {
        console.error('Search failed:', error);
      }
    }
  };

  const handleSearchChange = (e) => {
    const value = e.target.value;
    setSearchTerm(value);

    if (timeoutRef.current) {
      clearTimeout(timeoutRef.current);
    }

    timeoutRef.current = setTimeout(() => {
      if (value.trim()) {
        performSearch(value);
      } else {
        setResults([]);
      }
    }, 300);
  };

  useEffect(() => {
    return () => {
      if (timeoutRef.current) {
        clearTimeout(timeoutRef.current);
      }
      if (requestRef.current) {
        requestRef.current.abort();
      }
    };
  }, []);

  return (
    <div>
      <input 
        type="text"
        value={searchTerm}
        onChange={handleSearchChange}
        placeholder="Search..."
      />
      <ul>
        {results.map(result => (
          <li key={result.id}>{result.title}</li>
        ))}
      </ul>
    </div>
  );
}
```

This example demonstrates several key `useRef` patterns: managing timers, handling cleanup, and tracking mutable values that don't need to cause re-renders.

## Wrapping Up

The `useRef` hook is one of those tools that seems simple on the surface but reveals its true power once you understand when and how to use it properly. It's not about replacing `useState`—it's about complementing it. Use refs for implementation details that don't affect the rendered output, and you'll write more efficient, cleaner React code.

The key takeaway? If changing a value should update what the user sees, use `useState`. If it's just something you need to remember between renders without affecting the UI, reach for `useRef`. Master this distinction, and you'll avoid a lot of common React performance issues while writing more maintainable code.
