useRef returns a mutable object with a .current property that persists across re-renders without causing a re-render when changed. Unlike state, updating ref.current is a direct mutation and React does not track it. This makes refs the right tool for storing values that the component needs to remember but that don't affect what's displayed on screen — DOM references, timer IDs, previous state values, and any other non-display bookkeeping. The ref object's identity is stable — it's the same object every render.
const inputRef = useRef(null);
const handleFocus = () => inputRef.current.focus();
return <input ref={inputRef} />;
Why it matters: useRef gives you a way to hold a value that survives re-renders but doesn't trigger them — essential for DOM access and storing mutable values.
Real applications: Focusing an input programmatically, storing a timer ID, tracking whether a component is mounted.
Common mistakes: Using useState for a timer ID or interval handle — this causes unnecessary re-renders every time the timer starts or stops.
useState triggers a re-render when its value changes so the UI stays up to date. useRef persists a value across renders but never triggers a re-render when its .current is mutated. Use useRef for values that need to persist but don't appear in the UI — timer IDs, previous values, DOM node references, and mutation counters. Using state for these causes unnecessary re-renders; using ref for display values means the component never updates when the value changes.
Why it matters: Choosing the wrong one either causes missed re-renders (useRef when you need state) or wasteful re-renders (useState when you just need to store a value).
Real applications: useState for a visible counter; useRef for an interval ID that shouldn't cause a re-render when it changes.
Common mistakes: Reading ref.current in render and expecting the UI to update when it changes — the component won't re-render until state changes.
To access a DOM element with a ref, create a ref with useRef(null) and attach it to a JSX element via the ref prop. After the component mounts, ref.current points to the actual DOM node, giving you access to all native DOM methods like .focus(), .scrollIntoView(), and .getBoundingClientRect(). Avoid reading ref.current during render — the DOM node is not attached yet at that point. Access the DOM node inside useEffect or event handlers, after the component has mounted.
function TextInput() {
const inputRef = useRef(null);
return (
<>
<input ref={inputRef} />
<button onClick={() => inputRef.current.focus()}>Focus</button>
</>
);
}
Why it matters: Direct DOM access is sometimes necessary — React's declarative approach covers 95% of cases, but refs handle the rest.
Real applications: Focusing an input on mount, triggering a video play/pause, measuring an element's size before rendering.
Common mistakes: Accessing ref.current during the render — the DOM node doesn't exist yet, it's only assigned after the component mounts.
forwardRef wraps a component to allow its parent to pass a ref directly to a DOM element inside it. Without forwardRef, a ref placed on a custom component points to nothing — refs don't automatically pass through to the underlying DOM. Use forwardRef when building reusable library components like custom inputs, buttons, or dialog elements that parent components may need to interact with imperatively through a ref. It is also needed when building components that wrap another component and need to expose its ref.
const FancyInput = forwardRef((props, ref) => (
<input ref={ref} className="fancy" {...props} />
));
// Parent
const ref = useRef(null);
<FancyInput ref={ref} />
Why it matters: By default you can't attach a ref to a custom component. forwardRef lets a parent component access a DOM node inside a child.
Real applications: Building reusable input components, dialog components, or any component where the parent needs to control focus or scroll.
Common mistakes: Forgetting to wrap a function component in forwardRef when trying to pass a ref to it — you'll get a warning and ref.current will be null.
useImperativeHandle is used alongside forwardRef to customize what the parent can access through the ref, instead of exposing the raw DOM node. You define exactly which methods the parent may call, keeping the component's internal DOM structure encapsulated and creating a controlled public API. For example, instead of exposing the entire input element, you expose just a { focus, clear } object. This prevents parent components from reaching into children and manipulating anything they want, maintaining better encapsulation.
const FancyInput = forwardRef((props, ref) => {
const inputRef = useRef();
useImperativeHandle(ref, () => ({
focus: () => inputRef.current.focus(),
clear: () => { inputRef.current.value = ''; }
}));
return <input ref={inputRef} />;
});
Why it matters: useImperativeHandle lets you expose only specific methods to a parent ref instead of the whole DOM element, giving you a controlled API.
Real applications: A custom input that exposes focus() and clear() but not the raw DOM node; a video player with play() and pause() methods.
Common mistakes: Exposing the raw DOM node when you only want to expose specific methods — use useImperativeHandle to limit what parents can access.
A ref's .current property persists across renders without causing a re-render when mutated, making it ideal for storing the previous value of a prop or state. The trick: update the ref in useEffect, which runs after rendering. So during render, ref.current still holds the old value from the previous render. This gives you access to both the current value (from props or state) and the previous value (from the ref) simultaneously, which is useful for animations, transitions, and detecting value direction changes.
function usePrevious(value) {
const ref = useRef();
useEffect(() => { ref.current = value; });
return ref.current;
}
function Counter() {
const [count, setCount] = useState(0);
const prevCount = usePrevious(count);
return <p>Now: {count}, Before: {prevCount}</p>;
}
Why it matters: Sometimes you need to know the previous value of a prop or state to decide how to animate or what changed — a ref is the cleanest way to hold it.
Real applications: Animating a number up or down, showing a "changed from X to Y" indicator, comparing props to avoid unnecessary work.
Common mistakes: Trying to use useState to track previous values — it adds an extra render cycle and unnecessary complexity.
Callback refs are an alternative to useRef where instead of passing a ref object, you pass a function as the ref prop. React calls this function with the DOM element when it mounts and with null when it unmounts. This gives you explicit control over when you know the node is available. Callback refs are useful when you need to respond to the node becoming available or when you need to attach a ref to multiple instances, like measuring a dynamically added list item.
function MeasureNode() {
const [height, setHeight] = useState(0);
const measuredRef = useCallback(node => {
if (node) setHeight(node.getBoundingClientRect().height);
}, []);
return <div ref={measuredRef}>Height: {height}</div>;
}
Why it matters: Callback refs fire when the element is attached or detached from the DOM, making them better than useRef for elements that might not exist on mount.
Real applications: Measuring an element's height once it's in the DOM, attaching a third-party library to a dynamically rendered element.
Common mistakes: Using a useRef and accessing it in useEffect — by the time the effect runs, the element might not have the dimensions you expect.
A single DOM node can only take one ref prop, but you can merge multiple refs onto the same element using a function that forwards the value to all of them. This is useful when a library provides a ref (like a resize observer hook) and you also need one for your own logic. The merge function takes any number of refs and returns a callback ref that assigns to all of them when called. ref(node) handles both object refs (ref.current = node) and callback refs (calling ref(node)).
function mergeRefs(...refs) {
return (node) => {
refs.forEach(ref => {
if (typeof ref === 'function') ref(node);
else if (ref) ref.current = node;
});
};
}
Why it matters: Sometimes the same DOM element needs to be controlled by multiple consumers — the parent and a third-party library, for example.
Real applications: An input needing both a measuring ref and a focus ref at the same time; a canvas used by both your animation code and a third-party library.
Common mistakes: Passing two refs directly to the same element — React only supports one ref prop, so you must merge them manually.
Storing timer IDs in a ref rather than state is the correct pattern because the ID itself shouldn't trigger a re-render — it's just an internal handle the component uses to cancel the timer later. Set intervalRef.current = setInterval(...) to start the timer and call clearInterval(intervalRef.current) to stop it. The ref persists the ID across renders so the cleanup function in useEffect can always access the correct timer ID, even if the component re-rendered between starting and stopping the timer.
function Timer() {
const intervalRef = useRef(null);
useEffect(() => {
intervalRef.current = setInterval(() => console.log('tick'), 1000);
return () => clearInterval(intervalRef.current);
}, []);
const stop = () => clearInterval(intervalRef.current);
return <button onClick={stop}>Stop</button>;
}
Why it matters: Timer IDs need to persist between renders but don't need to trigger re-renders — they're a perfect use case for useRef.
Real applications: A stopwatch start/stop button, a polling mechanism that can be cancelled, a debounce timer.
Common mistakes: Storing interval or timeout IDs in useState — this causes re-renders every time the timer starts or stops, flickering the UI.
Refs are an escape hatch for imperative DOM work that cannot be expressed declaratively. Avoid using refs to conditionally render elements (use state and conditional JSX), update visible text content (use state), or manage form values that React should control (use controlled components). Reaching into the DOM with refs when React could handle it declaratively fights against React's programming model and causes hard-to-debug sync issues between your ref mutations and React's rendering.
Why it matters: Overusing refs fights against React's declarative model — your UI becomes harder to reason about and test.
Real applications: Use state for any value that should update the UI; only use refs for things that need to escape the React rendering cycle entirely.
Common mistakes: Using refs to read or set form values — almost always better handled with controlled components and state.
Auto-focusing the first input in a modal is a classic ref use case. Create a ref with useRef(null), attach it to the input with the ref prop, and call inputRef.current.focus() inside a useEffect that runs when the modal becomes visible. The isOpen flag should be in the dependency array so focus is applied each time the modal opens, not just on the first mount. This is important for accessibility — moving focus to the modal traps keyboard navigation and helps screen reader users.
function Modal({ isOpen }) {
const inputRef = useRef(null);
useEffect(() => {
if (isOpen) {
// Small delay ensures the element is visible before focusing
inputRef.current?.focus();
}
}, [isOpen]);
if (!isOpen) return null;
return (
<div className="modal">
<input ref={inputRef} placeholder="Search..." />
</div>
);
}Auto-focusing the first interactive element in a modal is an important accessibility practice — it helps keyboard and screen reader users navigate efficiently.
Why it matters: Auto-focus on modal open is an accessibility requirement — without it, keyboard users lose focus context when the modal appears.
Real applications: Modals, dialogs, sidebars — any overlay that should immediately accept keyboard input when opened.
Common mistakes: Calling inputRef.current.focus() immediately in the component body instead of in a useEffect — the DOM node isn't ready yet.
To scroll a specific element into the viewport, attach a ref to it and call ref.current.scrollIntoView(). The standard pattern for chat windows is to attach a ref to a sentinel element at the bottom of the message list and call scrollIntoView({ behavior: 'smooth' }) whenever new messages arrive (using the messages array as a useEffect dependency). This automatically scrolls to show the newest message without any manual scroll position calculations.
function ChatWindow({ messages }) {
const bottomRef = useRef(null);
useEffect(() => {
// Scroll to bottom whenever messages change
bottomRef.current?.scrollIntoView({ behavior: 'smooth' });
}, [messages]);
return (
<div className="chat">
{messages.map(m => <p key={m.id}>{m.text}</p>)}
<div ref={bottomRef} /> {/* invisible anchor at bottom */}
</div>
);
}The behavior: 'smooth' option produces an animated scroll. Use 'instant' for immediate jumps without animation.
Why it matters: Scrolling to a specific element programmatically is needed for "back to top" buttons, anchor navigation, and form error highlights.
Real applications: Back to top button, scrolling to a form field with a validation error, scrolling to a chat message.
Common mistakes: Calling scrollIntoView before the referenced element has mounted — always do it inside a useEffect or event handler.
createRef creates a new ref object on every render — use it in class components where you initialize it in the constructor and it persists for the component's lifetime. useRef returns the same ref object on every render — use it in functional components. Using createRef inside a functional component re-creates the ref object every render, effectively losing the reference on each update, which is almost always a bug.
// Class component — createRef in constructor
class MyInput extends React.Component {
constructor(props) {
super(props);
this.inputRef = React.createRef(); // new object each mount
}
render() {
return <input ref={this.inputRef} />;
}
}
// Functional component — useRef persists across renders
function MyInput() {
const inputRef = useRef(null); // same object every render
return <input ref={inputRef} />;
}Using createRef inside a functional component would create a new ref on every render, losing the stored value — always use useRef in functional components.
Why it matters: Using createRef in a functional component resets on every render — you lose the stored DOM node or value between renders.
Real applications: useRef in all functional components; createRef only in class component constructors or factory functions outside of components.
Common mistakes: Using createRef() at the top of a functional component — it creates a brand new ref object on each render instead of reusing the same one.
DOM measurements like size and position are only available after the component mounts, because the browser hasn't laid out the element until then. Use useLayoutEffect (which runs synchronously after DOM mutations, before painting) or useEffect (which runs after painting) to call ref.current.getBoundingClientRect(). Store the result in state if it should affect what's displayed. Use a ResizeObserver inside the effect for continuous measurements that update whenever the element's size changes.
function ResizableBox() {
const boxRef = useRef(null);
const [size, setSize] = useState({ width: 0, height: 0 });
useLayoutEffect(() => {
if (boxRef.current) {
const { width, height } = boxRef.current.getBoundingClientRect();
setSize({ width, height });
}
}, []);
return (
<div ref={boxRef} className="box">
Size: {size.width} x {size.height}
</div>
);
}Use useLayoutEffect instead of useEffect when reading DOM dimensions to avoid flickering — it fires before the browser paints.
Why it matters: Reading the real size of a rendered element is essential for positioning tooltips, popovers, or building resizable panels.
Real applications: Positioning a tooltip next to its target, building a resizable split pane, measuring a text element to truncate it precisely.
Common mistakes: Using useEffect to read dimensions — the browser has already painted by then, causing a visible flash if you update layout based on the measurements.