LearnContact
Lesson 3018 min read

useRef Hook

Learn how useRef creates a persistent, mutable reference that survives renders without triggering re-renders, and how it differs from useState.

Introduction

Not every value a component needs to remember should cause a re-render when it changes. A timer ID, the previous value of a prop, or a direct reference to a DOM node are all things you want to keep around between renders, but updating them should not, by itself, trigger React to re-render the component. This is exactly the gap useRef fills.

This lesson covers what useRef actually returns, its most common use — grabbing a direct reference to a DOM element such as an input to call .focus() on it — using it to store any mutable value across renders, and a clear comparison between useRef and useState so you know exactly which one to reach for.

What You Will Learn
  • What useRef returns and why updating it does not cause a re-render.
  • How to use a ref to access a real DOM element directly.
  • How to store any mutable value (like a timer ID or previous value) across renders.
  • A clear, practical comparison between useRef and useState.

What Is useRef?

useRef(initialValue) returns a plain, mutable object with a single property: .current, set initially to whatever value you passed in. That object is the exact same object on every render — React creates it once, on the first render, and hands back the identical reference every time afterward.

Basic useRef
import { useRef } from 'react';

function Example() {
  const renderCountRef = useRef(0);

  renderCountRef.current = renderCountRef.current + 1;

  return <p>This component has rendered {renderCountRef.current} times (not counted via state)</p>;
}
The Key Property

Changing ref.current does not schedule a re-render, and reading ref.current always gives you the latest value immediately — unlike state, which only reflects the update on the next render.

Accessing DOM Elements

The most common use of useRef is getting direct access to a real DOM element — something React normally abstracts away. You create a ref, attach it to a JSX element's ref attribute, and React automatically sets ref.current to that actual DOM node once it is rendered.

FocusInput.jsx
import { useRef } from 'react';

function FocusInput() {
  const inputRef = useRef(null);

  function handleFocusClick() {
    inputRef.current.focus(); // directly calls the real DOM input's .focus()
  }

  return (
    <div>
      <input ref={inputRef} type="text" placeholder="Click the button to focus me" />
      <button onClick={handleFocusClick}>Focus the Input</button>
    </div>
  );
}
Behavior
Clicking "Focus the Input" moves the text cursor directly into the input field, exactly like calling document.querySelector("input").focus() — but without ever touching the DOM manually.

Focus Management

Programmatically focus an input, e.g. right after a form opens or after a validation error.

Measuring Elements

Read layout info like offsetWidth or getBoundingClientRect() from a rendered element.

Triggering Media

Call methods like .play() or .pause() on a <video> or <audio> element.

Scrolling

Scroll a specific element into view with scrollIntoView().

Storing Mutable Values

Beyond DOM access, useRef is also the standard way to store any value that needs to persist across renders but should never, by itself, cause a re-render — a setInterval ID so it can be cleared later, the previous value of a prop for comparison, or a flag tracking whether a component has already mounted.

Storing a Timer ID
import { useState, useRef } from 'react';

function StopwatchButton() {
  const [isRunning, setIsRunning] = useState(false);
  const intervalIdRef = useRef(null);

  function start() {
    setIsRunning(true);
    intervalIdRef.current = setInterval(() => {
      console.log('tick');
    }, 1000);
  }

  function stop() {
    setIsRunning(false);
    clearInterval(intervalIdRef.current); // read the stored ID to clear it
  }

  return (
    <button onClick={isRunning ? stop : start}>
      {isRunning ? 'Stop' : 'Start'}
    </button>
  );
}

Storing the interval ID in state would work too, but it would trigger an unnecessary re-render every time it changed — and the component does not actually need to display that ID anywhere. A ref is the right tool because nothing in the UI depends on its value.

useRef vs useState

Both Hooks persist a value between renders, which is exactly why they are so often confused. The difference comes down to one question: should updating this value cause the UI to update too?

AspectuseStateuseRef
Triggers re-render on change?YesNo
Value available immediately after update?No — reflected on next renderYes — .current updates instantly
Typical useValues shown in the UI (count, form input, toggle)DOM references, timer IDs, previous values — not shown directly
How you read itDirectly, e.g. countThrough .current, e.g. ref.current
Causes extra renders if updated often?Yes, one per updateNo, updates are free of re-renders
A Simple Test

Ask: "Does this value need to show up on screen or affect what renders?" If yes, use useState. If it is bookkeeping the component needs internally but never displays directly, useRef is the better fit.

Common Mistakes

Avoid These Mistakes
  • Using useRef for a value that is actually displayed in the UI — since updating it will not trigger a re-render, the screen will silently go stale.
  • Reading ref.current during the render itself for a DOM node ref before the component has mounted — it will still be null.
  • Forgetting to clear a stored timer/interval ID from a ref, leading to the same cleanup bugs seen with useEffect.
  • Mutating ref.current expecting the component to visually update immediately — refs intentionally do not cause re-renders.

Best Practices

  • Reach for useRef only when a value truly does not need to affect what is rendered.
  • Initialize DOM refs with useRef(null) and guard against null before use if there is any chance the element has not mounted yet.
  • Use refs to store IDs (timers, animation frames) so they can be reliably cleared later, typically inside a useEffect cleanup.
  • Do not read or write ref.current during rendering for anything that should reflect visually — only inside effects or event handlers.
  • Prefer useState whenever you find yourself needing the UI to reflect a ref's current value.

Frequently Asked Questions

Does changing ref.current cause a re-render?

No. Updating .current is a plain mutation that React does not track for re-rendering purposes. The component will only re-render for some other reason, like a state update elsewhere.

Why is my DOM ref null the first time I check it?

The ref is only attached to the actual DOM node after React commits the render to the screen. If you read it before that point (e.g. during the render itself), it will still be null.

Can useRef store more than one value?

Yes. You can pass any initial value to useRef, including an object, so ref.current can hold multiple related pieces of information if needed.

Is useRef the same as an instance variable in a class component?

Conceptually, yes. A ref behaves similarly to this.someProperty in a class component — it persists across renders/method calls without being tied to re-rendering.

Should I use useRef to store form input values?

You can for simple, uncontrolled inputs where you only need the value at submit time. But if the UI needs to react to the value as it changes (like live validation), useState (a controlled input) is the better choice.

Key Takeaways

  • useRef(initialValue) returns a stable object with a mutable .current property.
  • Updating ref.current does not trigger a re-render, unlike updating state.
  • The most common use is a ref attribute that gives direct access to a real DOM element.
  • Refs are also ideal for storing values like timer IDs or previous values that the UI does not directly display.
  • Choose useState when a value affects rendering, and useRef when it is purely internal bookkeeping.

Summary

useRef fills an important gap that useState cannot: a place to keep a mutable value alive across renders without paying the cost of a re-render every time it changes. Whether you are focusing an input, tracking a timer ID, or remembering a previous value, useRef is the right tool whenever the UI itself does not need to reflect that value directly.

Next, you will learn useMemo — a Hook that lets you skip re-running expensive calculations on every render by memoizing their results.

Lesson 30 Completed
  • You understand what useRef returns and why it does not cause re-renders.
  • You can use a ref to access and control a real DOM element.
  • You can store mutable values like timer IDs in a ref.
  • You can clearly explain when to choose useRef over useState.
Next Lesson →

useMemo Hook