Sometimes, React state isn’t enough. You may need direct access to a DOM element, such as focusing an input, managing a video, or integrating a third-party library.
useRef provides a persistent object that survives re-renders without triggering a re-render when its .current property changes.
Attach a ref to a DOM element to access it directly.
import { useRef } from "react";function SearchInput() {const inputRef = useRef(null);const handleFocus = () => {inputRef.current?.focus();};return (<><input ref={inputRef} /><button onClick={handleFocus}>Focus</button></>);}
Clicking the button focuses the input without requiring state.
Refs are useful when a library manages its own DOM elements or rendering lifecycle.
import { useEffect, useRef } from "react";function Chart() {const containerRef = useRef(null);useEffect(() => {const chart = createChart(containerRef.current);return () => chart.destroy();}, []);return <div ref={containerRef} />;}
The ref provides the DOM container, while the cleanup function destroys the chart when the effect is no longer needed.
useRefIt doesn’t trigger re-renders
Unlike state, changing ref.current doesn’t tell React to update the UI.
inputRef.current.value = "Hello";
This directly changes the DOM input value. React doesn’t track this change as state, so prefer controlled inputs when the value needs to stay synchronized with React.
It can make debugging harder
Ref mutations aren’t tracked as state updates in React DevTools. Overusing refs to store UI-related data can make behavior harder to understand and maintain.
Rule of thumb: Use state for data that affects rendering and refs for values that need to persist without triggering a re-render.
React compares effect dependencies using Object.is. Primitive values such as strings, numbers, and booleans are often convenient dependencies because their identity is determined by their value.
const userId = user.id;useEffect(() => {fetchUser(userId);}, [userId]);
The effect re-runs when userId changes.
By contrast, objects and arrays can have new references even when their contents are equivalent.
const options = { active: true };useEffect(() => {connect(options);}, [options]);
Because options is recreated on every render, the effect may re-run unnecessarily. When possible, depend on the specific primitive values the effect needs or create the object inside the effect.