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
const ref = useRef(initialValue)
// ref is { current: initialValue }
// ref.current is mutable — you can read and write it freelyThe 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.
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>
)
}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.
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:
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 |
|
|
Mutate the value |
|
|
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:
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} />
}