ReactuseRef Hook

useRef Hook

useRef is one of those hooks that looks deceptively simple but unlocks two completely different capabilities depending on how you use it. At its core, it gives you a box — an object with a single .current property — that persists for the lifetime of the component.

The Signature

JSX
const ref = useRef(initialValue)
// ref is { current: initialValue }
// ref.current is mutable — you can read and write it freely

The key insight: changing ref.current does not cause a re-render. This is what sets refs apart from state and makes them useful for two distinct scenarios.

Use Case 1 — Accessing DOM Elements

Pass a ref to a JSX element's ref attribute and React will set ref.current to the underlying DOM node after the component mounts. From that point on you can call any native DOM method on it.

JSX
import { useRef } from 'react'

function SearchBar() {
  const inputRef = useRef(null)

  function focusInput() {
    // ref.current is the actual <input> DOM node
    inputRef.current.focus()
  }

  return (
    <div>
      <input ref={inputRef} type="text" placeholder="Search..." />
      <button onClick={focusInput}>Focus</button>
    </div>
  )
}
Note
React sets ref.current to the DOM node after the component mounts, and resets it to null before the component unmounts. Always check ref.current !== null before calling DOM methods if the component might unmount.
Use Case 2 — Persisting Values Without Re-Renders

Sometimes you need a value that survives re-renders but whose changes should not trigger a re-render. Classic examples: timer IDs, event listener references, previous render values, animation frames.

JSX
import { useRef, useState, useEffect } from 'react'

// A stopwatch that tracks elapsed time without re-rendering on every tick
function Stopwatch() {
  const [running, setRunning] = useState(false)
  const [display, setDisplay] = useState(0)       // only re-render when user clicks
  const intervalRef = useRef(null)                 // store interval ID — no re-render
  const elapsedRef = useRef(0)                     // store elapsed ms — no re-render

  function start() {
    if (running) return
    setRunning(true)
    const startTime = Date.now() - elapsedRef.current
    intervalRef.current = setInterval(() => {
      elapsedRef.current = Date.now() - startTime
    }, 10)
  }

  function stop() {
    clearInterval(intervalRef.current)
    setRunning(false)
  }

  function reset() {
    clearInterval(intervalRef.current)
    elapsedRef.current = 0
    setRunning(false)
    setDisplay(0)
  }

  function showTime() {
    // Pull the latest value from the ref on demand
    setDisplay(elapsedRef.current)
  }

  return (
    <div>
      <p>{(display / 1000).toFixed(2)}s</p>
      <button onClick={start}>Start</button>
      <button onClick={stop}>Stop</button>
      <button onClick={showTime}>Sample</button>
      <button onClick={reset}>Reset</button>
    </div>
  )
}
Tracking Previous Values

A classic ref pattern is storing the value from the previous render so you can compare it in the current render:

JSX
import { useRef, useEffect } from 'react'

function usePrevious(value) {
  const ref = useRef(undefined)

  useEffect(() => {
    // After render completes, update the ref with the current value
    // On the NEXT render, ref.current will hold what value was this render
    ref.current = value
  })

  return ref.current   // returns the value from the previous render
}

function PriceDisplay({ price }) {
  const prevPrice = usePrevious(price)

  const direction =
    prevPrice === undefined
      ? null
      : price > prevPrice
        ? 'up'
        : price < prevPrice
          ? 'down'
          : 'same'

  return (
    <div>
      <span style={{ color: direction === 'up' ? 'green' : direction === 'down' ? 'red' : 'inherit' }}>
        ${price.toFixed(2)}
        {direction === 'up' && ' ↑'}
        {direction === 'down' && ' ↓'}
      </span>
    </div>
  )
}
useRef vs useState at a Glance

Behaviour

useState

useRef

Changing value triggers re-render

Yes

No

Value persists between renders

Yes

Yes

Access the current value

value

ref.current

Mutate the value

setValue(x)

ref.current = x

Good for

Data that drives UI output

Side-channel data, DOM nodes

Avoiding Stale Closures with Refs

Refs are also the go-to solution when an event handler or timer callback would otherwise capture a stale value from its closure:

JSX
import { useRef, useEffect, useState } from 'react'

// Custom hook: always call the latest version of a callback
function useLatestCallback(fn) {
  const ref = useRef(fn)

  useEffect(() => {
    ref.current = fn   // keep ref pointing at the latest version
  })

  return useRef((...args) => ref.current(...args)).current
}

function LiveSearch({ query }) {
  const [results, setResults] = useState([])

  const search = useLatestCallback(async (q) => {
    const data = await fetchResults(q)
    // This always uses the latest setResults, never a stale closure
    setResults(data)
  })

  useEffect(() => {
    search(query)
  }, [query, search])

  return <ResultsList results={results} />
}
Warning
Do not read or write ref.current during rendering (i.e., outside of event handlers or useEffect). React assumes the render output is pure and consistent; reading a mutable ref during render can produce inconsistent results between server and client.
Tip
If you find yourself storing derived data in a ref to avoid re-renders, first consider whether a plain variable inside the component body is enough. Variables are recomputed each render at zero cost unless the computation is expensive — in which case useMemo is the right tool.