# Mastering useRef in React: A Practical Guide

f you’re diving into React, you’ve likely come across the ‎`useRef` hook. But what exactly is ‎`useRef()`, and how can it make your life easier? In this post we’ll cover what ‎`useRef` is, walk through five real-world use cases, and compare it against the alternatives so its advantages are obvious.

**What is useRef() really?**

At its core, ‎`useRef()` returns a mutable object with a ‎`.current` property that persists across component renders. Unlike state, changing ‎`.current` does not trigger a re-render. That makes refs useful for holding values you want to survive between renders without touching the UI. Think of ‎`useRef` as a “box” that can hold any value.

Key points:

*   The ref object is stable — the same object is returned on every render.
    
*   Updating ‎`ref.current` is synchronous and doesn’t cause renders.
    
*   Common uses: DOM access, storing mutable values (timers, previous values), avoiding stale closures, and exposing imperative methods.
    

**Use case 1 — DOM manipulation** 

One of the most common uses for ‎`useRef` is directly accessing a DOM node — for example, focusing an input when a component mounts.

**Before (‎**`getElementById`**):**

```jsx
import React, { useEffect, useState } from 'react';

const FocusInput = () => {
  const [inputValue, setInputValue] = useState('');

  useEffect(() => {
    document.getElementById('input').focus();
  }, []);

  return <input id="input" value={inputValue} onChange={(e) => setInputValue(e.target.value)} />;
};

```

**After (‎**`useRef`**):**

```jsx
import React, { useEffect, useRef, useState } from 'react';

const FocusInput = () => {
  const [inputValue, setInputValue] = useState('');
  const inputRef = useRef(null);

  useEffect(() => {
    inputRef.current?.focus();
  }, []);

  return <input ref={inputRef} value={inputValue} onChange={(e) => setInputValue(e.target.value)} />;
};

```

Why it’s better: no global ‎`id` lookups, no risk of colliding IDs across components, and the reference is scoped to this component. React hands you the node directly.

**Use case 2 — Storing previous values** 

‎`useRef` can hold a previous value across renders without triggering an extra render.

**Before (using state for the previous value):**

```jsx
import React, { useState, useEffect } from 'react';

const Counter = () => {
  const [count, setCount] = useState(0);
  const [prevCount, setPrevCount] = useState(0);

  useEffect(() => {
    setPrevCount(count);
  }, [count]);

  return (
    <div>
      <p>Current: {count}</p>
      <p>Previous: {prevCount}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
};

```

The ‎`setPrevCount` call schedules a second render every time ‎`count` changes — wasteful for a value that’s purely informational.

**After (‎**`useRef` **via a reusable ‎**`usePrevious` **hook):**

```jsx
import React, { useState, useEffect, useRef } from 'react';

function usePrevious(value) {
  const ref = useRef();
  useEffect(() => {
    ref.current = value;
  }, [value]);
  return ref.current; // returns the value from the previous render
}

const Counter = () => {
  const [count, setCount] = useState(0);
  const prevCount = usePrevious(count);

  return (
    <div>
      <p>Current: {count}</p>
      <p>Previous: {prevCount}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
};

```

Why it’s better: the effect writes to the ref after render, so during render ‎`usePrevious` still returns the prior value — exactly what you want. And there’s no extra render, because updating a ref doesn’t schedule one. This ‎`usePrevious` pattern is the canonical, reusable version of the idea.

**Use case 3 — Storing interval/timeout IDs** 

Storing a timer ID in state is unnecessary and causes re-renders. A ref is the right home for it.

**Before (timer ID in state — not ideal):**

```jsx
import React, { useState, useEffect } from 'react';

const TimerExample = () => {
  const [seconds, setSeconds] = useState(0);
  const [timerId, setTimerId] = useState(null);

  useEffect(() => {
    const id = setInterval(() => setSeconds(s => s + 1), 1000);
    setTimerId(id);
    return () => clearInterval(id);
  }, []);

  return <div>Seconds: {seconds}</div>;
};

```

**After (timer ID in a ref):**

```jsx
import React, { useState, useEffect, useRef } from 'react';

const TimerExample = () => {
  const [seconds, setSeconds] = useState(0);
  const timerRef = useRef(null);

  useEffect(() => {
    timerRef.current = setInterval(() => {
      setSeconds(s => s + 1);
    }, 1000);

    return () => clearInterval(timerRef.current);
  }, []);

  return <div>Seconds: {seconds}</div>;
};

```

Why it’s better: the timer ID is an implementation detail the UI never displays, so it doesn’t belong in state. The ref keeps it around for cleanup without any extra renders.

**Use case 4 — Avoiding stale closures** 

When a callback registered once (inside ‎`setInterval`, an event listener, or a third-party callback) needs the latest state or props, a ref can hold the most recent value without re-creating the callback.

```jsx
import React, { useState, useEffect, useRef } from 'react';

// Stub for illustration — replace with your real data fetching.
const fetchData = (q) => {
  console.log('Fetching results for:', q);
};

const Polling = () => {
  const [query, setQuery] = useState('react');
  const latestQueryRef = useRef(query);

  // Keep the ref in sync with the latest query on every change.
  useEffect(() => {
    latestQueryRef.current = query;
  }, [query]);

  // This interval is created once and never re-created.
  useEffect(() => {
    const id = setInterval(() => {
      // Reading the ref gives the latest query, not the stale one
      // captured when the interval was first set up.
      fetchData(latestQueryRef.current);
    }, 5000);
    return () => clearInterval(id);
  }, []);

  return <input value={query} onChange={e => setQuery(e.target.value)} />;
};

```

Why it matters: the empty dependency array means the interval closure captures ‎`query` only once — as ‎`'react'`. Without the ref, it would poll forever with the stale value. The ref gives the closure a live window into the current state.

**Use case 5 — Integrating with third-party libraries or imperative methods** 

Refs shine at the boundary between React and the imperative world. Two common cases:

1.  Passing a DOM node to a non-React library
    

Many charting, mapping, or animation libraries want a raw DOM element to mount into. A ref hands them exactly that.

```jsx
import React, { useEffect, useRef } from 'react';
import Chart from '/chart.js';

const ChartCanvas = ({ data }) => {
  const canvasRef = useRef(null);

  useEffect(() => {
    const chart = new Chart(canvasRef.current, {
      type: 'bar',
      data,
    });
    return () => chart.destroy();
  }, [data]);

  return <canvas ref={canvasRef} />;
};

```

2.  Exposing imperative methods to a parent
    

With ‎`useImperativeHandle`, a child can expose specific methods to its parent. As of React 19, ‎`ref` is passed as a normal prop, so ‎`forwardRef` is no longer required:

```jsx
// React 19+: ref is just a prop — no forwardRef needed.
import React, { useImperativeHandle, useRef } from 'react';

const FancyInput = ({ ref, ...props }) => {
  const inputRef = useRef(null);
  useImperativeHandle(ref, () => ({
    focus: () => inputRef.current.focus(),
  }));
  return <input ref={inputRef} {...props} />;
};

// Parent
// const ref = useRef(null);
// <FancyInput ref={ref} />
// ref.current.focus();

```

If you’re on React 18 or earlier, wrap the component in ‎`forwardRef` instead:

```jsx
import React, { forwardRef, useImperativeHandle, useRef } from 'react';

const FancyInput = forwardRef((props, ref) => {
  const inputRef = useRef(null);
  useImperativeHandle(ref, () => ({
    focus: () => inputRef.current.focus(),
  }));
  return <input ref={inputRef} {...props} />;
});

```

**Best practices and pitfalls** 

Do use refs for:

*   Accessing DOM nodes.
    
*   Storing mutable values that don’t affect rendering (timers, IDs, latest values).
    
*   Avoiding stale closures.
    
*   Interop with non-React libraries and imperative APIs.
    

Don’t use refs to:

*   Replace state when the UI depends on the value. If a change should show up on screen, use state.
    
*   Force re-renders — refs intentionally do not trigger them.
    

Common pitfalls:

*   Reading ‎`ref.current` before it’s assigned — guard with optional chaining (‎`ref.current?.…`) or null checks.
    
*   Using refs for controlled input values — prefer controlled components (state) unless you deliberately want an uncontrolled input.
    
*   Reaching for ‎`id` selectors for DOM access — refs are cleaner, scoped, and safer.
    

**Conclusion** 

‎`useRef` is a small but powerful hook for keeping mutable values between renders without causing re-renders — the right tool for DOM access, timers and IDs, previous values, avoiding stale closures, and bridging imperative libraries. The rule of thumb: use state for anything the UI displays, and refs for everything mutable that it doesn’t.

Give these patterns a try and watch how they sharpen your components. Happy coding!
