Most React developers first encounter
useRefwhen they need to focus an input or measure a DOM element. It works for that, but the hook's underlying behavior makes it useful in a much wider range of situations.useRefreturns a mutable container whose.currentvalue persists across renders without triggering re-renders when it changes. That combination of mutability and persistence solves a category of problems that neitheruseStatenoruseCallbackhandles cleanly.This guide covers the foundational behavior of
useRef, the DOM reference use case, and four patterns that apply it to non-DOM problems: tracking previous values, storing timer IDs, avoiding stale closures in event callbacks, and caching expensive computed values.What this covers:
How
useRefworks and how it differs fromuseStateDOM references and direct element manipulation
Tracking previous prop or state values
Storing timer and interval IDs
Stable event callbacks to avoid stale closures
Caching expensive values without triggering re-renders
When to use
useRefvs.useStatevs.useMemo
How useRef Works
useRef returns a plain object with a single .current property, initialized to the value passed as the argument:
const ref = useRef(0);
console.log(ref.current); // 0
Two properties distinguish it from state:
Mutability without re-renders. Assigning to ref.current does not notify React and does not cause the component to re-render. The change takes effect immediately in the current execution context.
Persistence across renders. The same ref object is returned on every render. Unlike a regular variable declared inside the component body, ref.current is not reset when the component re-renders.
The practical implication: useRef is the right tool when a value needs to be remembered between renders but changing it should not update the UI. When a value change should update the UI, use useState.
DOM References
The most common use case is attaching a ref to a DOM element to read properties or call methods directly:
import { useRef } from "react";
export default function SearchInput() {
const inputRef = useRef<HTMLInputElement>(null);
function focusInput() {
inputRef.current?.focus();
}
return (
<div>
<input ref={inputRef} type="text" placeholder="Search..." />
<button onClick={focusInput}>Focus</button>
</div>
);
}
The type parameter HTMLInputElement provides TypeScript's DOM typings on inputRef.current, so calling .focus(), .select(), or reading .value is type-checked. The optional chaining (?.) handles the case where the ref is not yet attached to a mounted element.
DOM refs are also useful for: measuring element dimensions with getBoundingClientRect, triggering animations on a canvas element, integrating with third-party libraries that require a direct DOM node, and managing focus in accessible UI components.
1. Tracking the Previous Value of a Prop or State
useRef combined with useEffect produces a usePrevious hook that captures what a value was on the previous render:
import { useRef, useEffect } from "react";
function usePrevious<T>(value: T): T | undefined {
const ref = useRef<T>();
useEffect(() => {
ref.current = value;
});
return ref.current;
}
Usage:
function PriceDisplay({ price }: { price: number }) {
const previousPrice = usePrevious(price);
return (
<div>
<p>Current: {price}</p>
{previousPrice !== undefined && (
<p>Previous: {previousPrice}</p>
)}
</div>
);
}
The effect runs after every render and stores the current value. When the component renders next time, ref.current contains the value from the previous render, and the effect runs again to update it for the render after that.
The effect has no dependency array, which means it runs after every render. This is intentional: the goal is to always store the most recently rendered value.
2. Storing Timer and Interval IDs
Storing a timer ID in state causes a re-render when the timer starts or stops. The timer ID is not a visual concern, so useRef is the more appropriate choice:
import { useRef } from "react";
export default function DelayedAction() {
const timerIdRef = useRef<ReturnType<typeof setTimeout> | null>(null);
function start() {
if (timerIdRef.current !== null) return; // prevent duplicate timers
timerIdRef.current = setTimeout(() => {
console.log("Action triggered");
timerIdRef.current = null;
}, 2000);
}
function cancel() {
if (timerIdRef.current !== null) {
clearTimeout(timerIdRef.current);
timerIdRef.current = null;
}
}
return (
<div>
<button onClick={start}>Start</button>
<button onClick={cancel}>Cancel</button>
</div>
);
}
Using ReturnType<typeof setTimeout> as the type is more portable than number — in Node.js environments it returns a Timeout object rather than a number, and this type works correctly in both.
The same pattern applies to setInterval IDs, animation frame IDs from requestAnimationFrame, and any other external handle that needs to be cleared later.
3. Stable Event Callbacks
When a callback is passed to an effect with an empty dependency array, it captures the values from the render in which the effect ran. If the callback's closure values change on a later render, the effect still holds the original version. This is a stale closure.
One solution is to add the callback to the effect's dependency array, which re-registers the effect on every render the callback changes. For expensive setup/teardown operations (like adding and removing event listeners), this causes unnecessary re-registration.
A useRef-based stable callback avoids re-registration while keeping the callback current:
import { useRef, useEffect } from "react";
function useStableCallback<T extends (...args: unknown[]) => unknown>(
callback: T
): T {
const callbackRef = useRef(callback);
useEffect(() => {
callbackRef.current = callback;
});
return ((...args: Parameters<T>) =>
callbackRef.current(...args)) as T;
}
Usage with an event listener:
function ScrollTracker({ onScroll }: { onScroll: (y: number) => void }) {
const stableOnScroll = useStableCallback(onScroll);
useEffect(() => {
function handleScroll() {
stableOnScroll(window.scrollY);
}
window.addEventListener("scroll", handleScroll);
return () => window.removeEventListener("scroll", handleScroll);
}, []); // empty array is safe — stableOnScroll always calls the latest callback
return null;
}
The event listener is registered once. The callbackRef is updated after every render to hold the latest onScroll prop. When the scroll event fires, it calls callbackRef.current, which is always current.
React 19 introduces useEffectEvent as an official solution to this pattern. In React 18 and below, the useRef approach is the idiomatic workaround.
4. Caching Expensive Computed Values
useMemo is the standard tool for caching expensive computations and recomputing only when dependencies change. For a value that should be computed once and never recomputed, useRef initialized with the computation result is simpler:
const processedData = useRef(expensiveTransform(rawData));
// processedData.current holds the result, computed once on first render
This is appropriate when the computation truly has no dependencies that would require recomputation. If the computation depends on props or state, use useMemo instead.
A more practical case is initializing a third-party class or SDK instance once:
const editorRef = useRef<EditorInstance | null>(null);
if (editorRef.current === null) {
editorRef.current = new EditorInstance({ /* config */ });
}
The lazy initialization pattern (if (ref.current === null)) ensures the instance is only created once, regardless of how many times the component renders.
Choosing Between useRef, useState, and useMemo
Need | Tool |
|---|---|
Value that triggers UI update when changed |
|
Cached value recomputed when dependencies change |
|
Value that persists across renders without triggering re-renders |
|
DOM element access |
|
Timer/interval ID |
|
Always-current callback reference |
|
The most common mistake is reaching for useRef when useState is appropriate because the developer wants to avoid a re-render. If the UI should reflect the value, it should be state. useRef is for values that are implementation details of the component, not data that the component renders.
Key Takeaways
useRefreturns a mutable container whose.currentvalue persists across renders without triggering re-renders when changed.DOM references are the most visible use case, but
useRefis equally useful for timer IDs, previous values, stable callbacks, and one-time initialization.usePrevioususes a no-dependency-array effect to capture the value from the previous render.Timer and interval IDs stored in refs do not cause re-renders when the timer starts or stops.
The stable callback pattern keeps a callback ref current after every render, allowing event listener effects to run once while always calling the latest version of the callback.
Use
useMemowhen a cached value has dependencies that require recomputation. UseuseRefwhen the value should be computed once and never recomputed.
Conclusion
useRef solves a specific set of problems that arize from React's render model: values that need to persist without causing re-renders, callbacks that need to be current without causing effects to re-register, and handles to external resources that need to be cleaned up. Understanding when these problems are present and reaching for useRef to address them produces components that are simpler and more efficient than approaches that route everything through state.
Using useRef for a pattern not covered here? Share it in the comments.




