useRef Hook
Hold a mutable value that persists across renders without causing re-renders.
Recipe
Quick-reference recipe card — copy-paste ready.
// Mutable instance variable
const renderCount = useRef(0);
renderCount.current += 1;
// DOM element reference
const inputRef = useRef<HTMLInputElement>(null);
inputRef.current?.focus();
// Store previous value
const prevValue = useRef(value);
useEffect(() => { prevValue.current = value; });When to reach for this: You need to access a DOM element directly, store a mutable value that should not trigger re-renders, or keep track of a previous value.
Working Example
"use client";
import { useEffect, useRef, useState } from "react";
export function Stopwatch() {
const [elapsed, setElapsed] = useState(0);
const [running, setRunning] = useState(false);
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
useEffect(() => {
if (running) {
intervalRef.current = setInterval(() => {
setElapsed((prev) => prev + 10);
}, 10);
}
return () => {
if (intervalRef.current) clearInterval(intervalRef.current);
};
}, [running]);
const reset = () => {
setRunning(false);
setElapsed(0);
};
const minutes = Math.floor(elapsed / 60000);
const seconds = Math.floor((elapsed % 60000) / 1000);
const ms = Math.floor((elapsed % 1000) / 10);
return (
<div className="space-y-3">
<p className="text-3xl font-mono tabular-nums">
{String(minutes).padStart(2, "0")}:{String(seconds).padStart(2, "0")}.
{String(ms).padStart(2, "0")}
</p>
<div className="flex gap-2">
<button
onClick={() => setRunning((r) => !r)}
className="px-3 py-1 border rounded"
>
{running ? "Stop" : "Start"}
</button>
<button onClick={reset} className="px-3 py-1 border rounded">
Reset
</button>
</div>
</div>
);
}What this demonstrates:
- Using
useRefto store the interval ID so it can be cleared later - The ref persists across renders without triggering re-renders when updated
ReturnType<typeof setInterval>provides the correct type for the timer ID- The ref is cleaned up in the effect's cleanup function
Deep Dive
How It Works
useRefreturns a mutable object with a single.currentproperty- The object reference is stable — React returns the same object on every render
- Updating
.currentdoes not trigger a re-render - Unlike
useState, there is no setter function — you mutate.currentdirectly - The ref object is created once on mount and persists until the component unmounts
Parameters & Return Values
| Parameter | Type | Description |
|---|---|---|
initialValue | T | Initial value assigned to .current |
| Return | Type | Description |
|---|---|---|
ref | { current: T } | Mutable ref object |
Variations
Focus an input on mount:
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
inputRef.current?.focus();
}, []);
return <input ref={inputRef} />;Track previous value:
function usePrevious<T>(value: T): T | undefined {
const ref = useRef<T | undefined>(undefined);
useEffect(() => {
ref.current = value;
});
return ref.current;
}Store latest callback (avoids stale closures):
const callbackRef = useRef(callback);
useEffect(() => {
callbackRef.current = callback;
});
// Use callbackRef.current in event handlers or timersMeasure a DOM element:
const divRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (divRef.current) {
const { width, height } = divRef.current.getBoundingClientRect();
setSize({ width, height });
}
}, []);TypeScript Notes
// DOM ref — use `null` as initial value with element type
const divRef = useRef<HTMLDivElement>(null);
// divRef.current is HTMLDivElement | null
// Mutable ref — pass the type as generic
const countRef = useRef<number>(0);
// countRef.current is number
// Distinction: useRef<T>(null) creates RefObject<T> (readonly .current)
// useRef<T | null>(null) creates MutableRefObject<T | null>
// For DOM refs, use the first pattern; for mutable values, use the secondGotchas
-
Reading refs during render — Accessing
ref.currentduring rendering (outsideuseEffector event handlers) can give inconsistent results. Fix: Read refs in effects or event handlers only. -
Expecting re-renders on mutation — Updating
ref.currentdoes not re-render the component. Fix: If the UI should update, useuseStateinstead. -
Null ref on first render — A DOM ref is
nulluntil React attaches it after the first render. Fix: Access DOM refs insideuseEffector after a null check. -
Ref vs. state confusion — Storing UI-visible data in a ref means the display never updates. Fix: Use refs only for values that do not need to be displayed or that drive side effects.
-
Callback refs vs. object refs — Object refs cannot notify you when the element changes (e.g., conditional rendering). Fix: Use a callback ref
ref={(node) => { ... }}when you need to react to attachment/detachment.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
useState | The value should trigger a re-render when it changes | You need a silent mutable container |
| Callback ref | You need to run code when a ref attaches or detaches | You just need a stable reference to an element |
document.getElementById | Outside React (rare) | Inside React components — use refs instead |
| Module-level variable | Value is shared across all component instances | Value should be per-component-instance |
Why refs instead of module variables? Module-level variables are shared across all instances of a component. Refs are per-instance — each mounted component gets its own .current.
FAQs
What is the difference between useRef and useState?
useRefstores a mutable value that persists across renders but does not trigger re-renders when updated.useStatestores a value that triggers a re-render when updated via its setter.- Use
useReffor values the UI does not display; useuseStatefor values the UI reflects.
Why is my DOM ref null when I try to access it?
- A DOM ref is
nulluntil React attaches it after the first render. - Access DOM refs inside
useEffect(which runs after mount) or behind a null check. - On the first render, the JSX hasn't been committed to the DOM yet.
How do I type a DOM ref vs a mutable value ref in TypeScript?
// DOM ref: use null initial, get RefObject<T> (readonly .current)
const inputRef = useRef<HTMLInputElement>(null);
// Mutable ref: include null in the generic union
const countRef = useRef<number | null>(null);
// countRef.current is number | null (mutable)Gotcha: Why doesn't updating ref.current cause my component to re-render?
- Updating
.currentmutates the object in place; React has no way to detect this change. - There is no setter function -- you assign directly:
ref.current = newValue. - If the UI should reflect the value, use
useStateinstead.
How do I store the previous value of a prop or state using useRef?
function usePrevious<T>(value: T): T | undefined {
const ref = useRef<T | undefined>(undefined);
useEffect(() => {
ref.current = value;
});
return ref.current;
}- The ref holds the value from the previous render because
useEffectruns after render.
When should I use a callback ref instead of an object ref?
- Use a callback ref
ref={(node) => { ... }}when you need to run code when an element attaches or detaches (e.g., conditional rendering). - Object refs only give you the element reference but don't notify you of changes.
- Callback refs are useful for measuring elements or integrating with non-React libraries.
Can I use useRef to store a timer or interval ID?
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
useEffect(() => {
intervalRef.current = setInterval(tick, 1000);
return () => {
if (intervalRef.current) clearInterval(intervalRef.current);
};
}, []);- Yes, this is a common pattern. The ref persists the ID across renders so cleanup can access it.
Gotcha: Is it safe to read ref.current during render?
- Reading
ref.currentduring render (outsideuseEffector event handlers) can give inconsistent results. - React may render your component multiple times (concurrent features), and the ref value could be from a different render pass.
- Read refs only in effects or event handlers.
Why use a ref instead of a module-level variable?
- Module-level variables are shared across all instances of a component.
- Refs are per-instance -- each mounted component gets its own
.current. - If two instances of a component use the same module variable, they overwrite each other.
How do I focus an input on mount using useRef?
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
inputRef.current?.focus();
}, []);
return <input ref={inputRef} />;- The ref is
nulluntil mount, so access it insideuseEffectwith optional chaining.
What is the "latest callback" ref pattern and why is it useful?
- Store the latest version of a callback in a ref, updating it on every render.
- Use
callbackRef.currentin timers or event listeners to always call the latest version. - This avoids stale closures without adding the callback to dependency arrays.
Related
- useState — when you need the value to trigger re-renders
- useEffect — access DOM refs inside effects after mount
- useCallback — stabilize callbacks that are stored in refs
- useId — generate unique IDs for DOM elements instead of using refs for identification