useRef Hook
Access DOM elements and persist mutable values across renders without re-rendering
useRef serves two purposes: holding a mutable value that persists across renders without causing re-renders, and accessing DOM elements directly.
Basic DOM Access
Refs give direct access to a DOM node. Common uses: focus management, text selection, media playback, integrating third-party libraries.
"use client"
import { useRef, useEffect } from "react"
export default function AutoFocus() {
const inputRef = useRef<HTMLInputElement>(null)
useEffect(() => {
// Focus input on mount
inputRef.current?.focus()
}, [])
return <input ref={inputRef} type="text" placeholder="Auto-focused" />
}
ref.current is null during the first render. The DOM node is assigned
after the component mounts. Access refs inside useEffect or event
handlers, never during render.
Mutable Values Across Renders
Updating a ref does not trigger a re-render. Use this for values that need to persist but don't affect the UI — interval IDs, previous prop values, subscription handles.
"use client"
import { useState, useRef, useEffect } from "react"
export default function TimerWithRef() {
const [count, setCount] = useState(0)
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null)
const start = () => {
if (intervalRef.current) return
intervalRef.current = setInterval(() => {
setCount((c) => c + 1)
}, 1000)
}
const stop = () => {
if (intervalRef.current) {
clearInterval(intervalRef.current)
intervalRef.current = null
}
}
useEffect(() => () => stop(), []) // cleanup on unmount
return (
<div>
<p>Count: {count}</p>
<button onClick={start}>Start</button>
<button onClick={stop}>Stop</button>
</div>
)
}
Tracking Previous Values
Refs hold values without triggering re-renders, making them perfect for comparing old vs new props or state.
"use client"
import { useState, useRef, useEffect } from "react"
export function usePrevious<T>(value: T): T | undefined {
const ref = useRef<T>()
useEffect(() => {
ref.current = value
}, [value])
return ref.current
}
export default function CounterDiff() {
const [count, setCount] = useState(0)
const prevCount = usePrevious(count)
return (
<div>
<p>
Now: {count}, Before: {prevCount ?? "—"}
</p>
<p>Change: {prevCount !== undefined ? count - prevCount : 0}</p>
<button onClick={() => setCount((c) => c + 1)}>+1</button>
</div>
)
}
forwardRef Pattern
When a parent needs a ref to a child's DOM node (e.g., focusing an input inside a custom component), wrap the child with forwardRef.
"use client"
import { useRef, forwardRef } from "react"
// Child receives ref forwarded from parent
const FancyInput = forwardRef<HTMLInputElement, { label: string }>(
(props, ref) => (
<label>
{props.label}
<input ref={ref} className="fancy-input" />
</label>
)
)
FancyInput.displayName = "FancyInput"
export default function Form() {
const inputRef = useRef<HTMLInputElement>(null)
const focusInput = () => {
inputRef.current?.focus()
}
return (
<div>
<FancyInput ref={inputRef} label="Name" />
<button onClick={focusInput}>Focus input</button>
</div>
)
}
useRef vs useState
| useRef | useState | |
|---|---|---|
| Re-render on change | No | Yes |
| Returns | { current: value } | [value, setValue] |
| Read during render | Yes (but don't write during render) | Yes |
| Use case | DOM refs, timers, previous values | UI state |
Mutating ref.current or reading it during the render phase can lead to
unpredictable behavior. React expects rendering to be a pure computation.
Keep ref mutations inside useEffect or event handlers.
See It In Action
Build a stopwatch with start/stop/reset and lap recording. Store the interval ID in a ref, track elapsed time with state.
1"use client"2 3import { useState, useRef } from "react"4 5export default function Stopwatch() {6const [elapsed, setElapsed] = useState(0)7const [laps, setLaps] = useState<number[]>([])8const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null)9 10const start = () => {11if (intervalRef.current) return12const startTime = Date.now() - elapsed * 100013intervalRef.current = setInterval(() => {14setElapsed((Date.now() - startTime) / 1000)15}, 100)16}17 18const stop = () => {19if (intervalRef.current) {20clearInterval(intervalRef.current)21intervalRef.current = null22}23}24 25const reset = () => {26stop()27setElapsed(0)28setLaps([])29}30 31const lap = () => {32setLaps((prev) => [...prev, elapsed])33}34 35return (36 37<div>38<p className="text-3xl font-mono">{elapsed.toFixed(1)}s</p>39<div className="flex gap-2 mt-2">40<button className="px-3 py-1 bg-green-500 text-white rounded" onClick={start}>Start</button>41<button className="px-3 py-1 bg-red-500 text-white rounded" onClick={stop}>Stop</button>42<button className="px-3 py-1 bg-gray-500 text-white rounded" onClick={reset}>Reset</button>43<button className="px-3 py-1 bg-blue-500 text-white rounded" onClick={lap}>Lap</button>44</div>45{laps.length > 0 && (46<ul className="mt-4 list-disc list-inside">47{laps.map((t, i) => <li key={i}>Lap {i + 1}: {t.toFixed(1)}s</li>)}48</ul>49)}50</div>51)52}A stopwatch component using useRef to hold the interval ID across renders without causing re-renders. start() computes the base start time from the current elapsed value, stop() clears the interval, reset() stops and zeros everything, and lap() snapshots the current elapsed into an array.