Skip to main content

Effects and Refs

useRef Explained

Written by Published

A ref is a box that survives re-renders and does not trigger them. That second half is the whole difference from state.

State versus ref

Both keep a value between renders. Changing state schedules a re-render; changing a ref does not, so the screen will not update until something else causes one.

Only one of them updates the screen

jsx

function Compare() {
  const [stateCount, setStateCount] = React.useState(0)
  const refCount = React.useRef(0)

  return (
    <div>
      <p>State: {stateCount} — Ref: {refCount.current}</p>

      <button onClick={() => setStateCount(stateCount + 1)}>Bump state</button>{" "}
      <button onClick={() => { refCount.current++ }}>Bump ref (no re-render)</button>

      <p style={{ color: "#64748b" }}>
        Press the ref button several times — nothing changes. Then press the state
        button once and the ref's real value appears.
      </p>
    </div>
  )
}

const root = ReactDOM.createRoot(document.getElementById("root"))
root.render(<Compare />)

The ref really was counting the whole time. Rendering is the only thing that reads it, and nothing asked for a render.

Reaching a DOM node

The common use: put a ref on an element and React sets .current to the real node. That is the supported way to focus, measure or scroll something.

Focus and measure

jsx

function Field() {
  const inputRef = React.useRef(null)
  const [width, setWidth] = React.useState(null)

  return (
    <div>
      <input ref={inputRef} placeholder="click a button below" />
      <div style={{ marginTop: 8, display: "flex", gap: 6 }}>
        <button onClick={() => inputRef.current.focus()}>Focus it</button>
        <button onClick={() => setWidth(Math.round(inputRef.current.getBoundingClientRect().width))}>
          Measure it
        </button>
      </div>
      {width !== null && <p>The input is {width}px wide.</p>}
    </div>
  )
}

const root = ReactDOM.createRoot(document.getElementById("root"))
root.render(<Field />)

ref.current is null until React has put the element on the page, so reading it during render gives you nothing. Read it in an event handler or an effect.

Storing something mutable

The other use is a value the component needs to remember but never displays - a timer id, the previous value of a prop, whether an effect has already run.

A timer id has to live somewhere

jsx

function Stopwatch() {
  const [seconds, setSeconds] = React.useState(0)
  const timerRef = React.useRef(null)

  function start() {
    if (timerRef.current) return          // already running
    timerRef.current = setInterval(() => setSeconds((s) => s + 1), 1000)
  }

  function stop() {
    clearInterval(timerRef.current)
    timerRef.current = null
  }

  React.useEffect(() => stop, [])          // clean up on unmount

  return (
    <div>
      <p>{seconds}s</p>
      <button onClick={start}>Start</button>{" "}
      <button onClick={stop}>Stop</button>{" "}
      <button onClick={() => setSeconds(0)}>Reset</button>
    </div>
  )
}

const root = ReactDOM.createRoot(document.getElementById("root"))
root.render(<Stopwatch />)

The id must survive re-renders - a plain variable would be lost - but nothing on screen shows it, so state would only cause pointless renders.

When a ref is the wrong tool

  • Anything you render. If it appears on screen it must be state, or the screen goes stale.
  • Changing the DOM React owns. Setting textContent on a rendered node will be overwritten on the next render.
  • Avoiding re-renders. Reaching for a ref because state re-renders too often is nearly always the wrong fix.

The test is simple: does the value appear in the returned JSX? If yes, it is state. If it is bookkeeping the user never sees, a ref is right.