useRef vs useState: Which One Should Hold the Value?

useState triggers a re-render when it changes and useRef does not. Use state for what the UI shows and refs for what only event handlers and Effects need.

6 min read

The main difference is one line: changing state causes a re-render, and changing a ref does not. State drives the interface, while a ref stores information React does not need to react to. That single rule decides almost every case.

The comparison at a glance

useRefuseState
ReturnsAn object with a current propertyAn array with a value and a setter
Changing itNo re-renderTriggers a re-render
MutabilityWrite current directlyReplace through the set function
Reading during renderAvoidSafe, but each render has its own snapshot

See how useRef works for the full background.

A value the UI must show: use state

A click counter displays its count, so the count belongs in state.

App.jsxApp.jsx
import { useState } from "react";
 
function Counter() {
  const [count, setCount] = useState(0);
 
  return <button onClick={() => setCount(count + 1)}>Clicked {count} times</button>;
}

Every click calls setCount, React renders again with the new number, and the button label updates. The screen stays in sync because the value lives in state.

The same counter with a ref fails

Swapping state for a ref produces a button that never changes its label.

App.jsxApp.jsx
import { useRef } from "react";
 
function Counter() {
  const countRef = useRef(0);
 
  return (
    <button onClick={() => (countRef.current += 1)}>
      Clicked {countRef.current} times
    </button>
  );
}

The ref updates on every click, but React never re-renders, so the label always shows the initial zero. The value is used for rendering, so a ref is the wrong container.

A value the UI never shows: use a ref

A timer ID is only read back to clear the timer. Nothing about it appears on screen, so it belongs in a ref.

App.jsxApp.jsx
import { useRef } from "react";
 
function PauseableAlert() {
  const timeoutRef = useRef(null);
 
  function handleStart() {
    timeoutRef.current = setTimeout(() => console.log("Done"), 3000);
  }
 
  function handleCancel() {
    clearTimeout(timeoutRef.current);
  }

The handlers store the timer ID when the timer starts and clear it when it is cancelled. None of this value appears in the JSX, so it never needs a render.

App.jsxApp.jsx
  return (
    <>
      <button onClick={handleStart}>Start</button>
      <button onClick={handleCancel}>Cancel</button>
    </>
  );
}

Storing the ID in state would also work, but it would force a re-render that changes nothing visible. The ref keeps the value across renders without that wasted work.

Which should you use?

Use state when the answer to any of these is yes.

  • Does the value appear in the JSX?
  • Does a change to it affect what renders?
  • Does an Effect depend on it?

Use a ref when all of these are true.

  • The value is only read or written in event handlers or Effects.
  • Changing it should not change the screen.
  • The value is a timer ID, DOM node, or similar escape-hatch object.

One false equivalence to avoid

A common mistake is treating a ref as "state without re-rendering" and then reading it during render anyway. That breaks purity and makes output unpredictable.

If you are tempted to read ref.current while rendering, the value belongs in state instead. For values that must be remembered across renders but stay out of the UI, a ref is exactly the right tool.

What to learn next

A ref is also the standard way to keep a copy of the previous value for later comparison. Continue with how to store previous values with useRef.

Rune AI

Rune AI

Key Insights

  • useState triggers a re-render; useRef persists without one.
  • Put displayed or decision-driving values in state.
  • Put timer IDs, DOM nodes, and handler-only values in a ref.
  • Read and write refs outside render; read state at any time.
  • Never use a ref to hold something the screen must show.
RunePowered by Rune AI

Frequently Asked Questions

Does useRef trigger a re-render?

No. Changing ref.current never notifies React, so the component does not render again. useState triggers a re-render when its set function is called with a new value.

Can useRef replace useState?

Only for values that never affect the UI. Anything the component renders or that changes what it renders must live in state.

Why is state called immutable?

Each render gets its own snapshot of state. You update it by calling the set function with a new value instead of mutating the existing object.

Conclusion

Use state for any value the UI displays or reacts to, because changing it must re-render the component. Use a ref for values that only event handlers and Effects read, where a re-render would be wasted work.