React 42 ⚛️ Accessing DOM Elements with useRef
React is declarative. You describe what the UI should look like for a given state, and React updates the DOM to match. Most of the time, you never touch the DOM directly. But some operations are inherently imperative. Focusing an input, scrolling to a position, measuring an element’s size, playing a video, or integrating a third-party library that expects a DOM node — these require a reference to the actual DOM element. useRef is the hook that provides it. It gives you a mutable object with a .current property, and when you attach it to a JSX element with the ref attribute, React sets .current to the DOM node.
The hook has two distinct uses, and conflating them causes confusion. The first is DOM access: a ref attached to a JSX element gives you the underlying DOM node so you can call imperative methods on it. The second is persistent storage: a ref that is not attached to any element holds a value across renders without triggering a re-render when it changes. A ref is like a box: React creates the box on the first render and gives you the same box on every subsequent render. You can put anything in it, and changing what is inside does not cause the component to re-render. This is what distinguishes a ref from state.
The distinction between a ref and state is the key to using refs correctly. State is for values that affect rendering. When state changes, React re-renders the component. A ref is for values that do not affect rendering. When a ref changes, React does nothing. A timer ID, a previous value, a DOM node, a WebSocket connection — these are values that need to persist across renders but do not need to trigger a re-render when they change. Putting them in state would cause unnecessary renders. Putting them in a ref keeps them out of the render cycle.
This chapter covers three areas. First, why useRef exists — the problem of accessing the DOM in a declarative framework and the problem of storing values that persist without causing renders. Second, how to use refs for DOM access — the ref attribute, the .current property, the timing of when .current is set, and the common DOM operations. Third, how to use refs for persistent storage — the difference between a ref and state, the patterns for timers, previous values, and instance variables, and the rules for when not to use a ref. The chapter ends with a complete example session, a quick reference, best practices, common pitfalls, real-world examples, and diagrams showing the ref lifecycle.
Key point: useRef returns a mutable object with a .current property that persists across renders. When attached to a JSX element with the ref attribute, .current holds the DOM node. When used for storage, .current holds any value, and changing it does not trigger a re-render. Refs are for imperative DOM access and for values that persist without affecting rendering. They are not a replacement for state.
Why useRef exists
The declarative-vs-imperative problem. React’s model is declarative: you describe the UI for a given state, and React figures out the DOM operations. But some operations cannot be described declaratively. “Focus this input” is an imperative command. “Scroll this element to the bottom” is an imperative command. “Play this video” is an imperative command. These operations need a reference to the actual DOM node, and useRef provides it. The ref is the bridge between React’s declarative model and the browser’s imperative API .
The render-persistence problem. A component’s local variables are recreated on every render. If a component needs to remember a value between renders, it must use state or a ref. State triggers a re-render when it changes; a ref does not. A timer ID, a previous value, a counter that does not appear in the UI, a connection object — these are values that need to survive renders but should not cause them. A ref is the right container for them. It is a persistent box that React creates once and hands back on every render .
The state-vs-ref problem. The distinction is about rendering. State is a value that the UI depends on. When state changes, the UI must update, so React re-renders. A ref is a value that the UI does not depend on. When a ref changes, the UI does not need to update, so React does not re-render. Putting a timer ID in state would cause a re-render every time the timer is reset, even though the timer ID is not displayed. Putting it in a ref avoids the re-render. The rule is: if the value appears in the rendered output, it is state; if it does not, it can be a ref .
The timing problem. When React renders a component, the JSX is evaluated before the DOM is created. A ref attached to a JSX element is null during the render and is set to the DOM node after React commits the changes to the DOM. This means the ref cannot be read during the render; it must be read in an effect or an event handler, after the commit. The useEffect hook with an empty dependency array runs after the commit, which is why it is the standard place to access a DOM ref. Reading .current during the render is an error, because the DOM node does not exist yet .
The list-of-refs problem. A single ref holds a single DOM node. A list of items needs a list of refs, one per item. The pattern is to create a ref that holds a Map or an array, and to populate it in the ref callback of each item. React 19 also allows a ref to be passed directly to a function component as a prop, which simplifies the pattern for reusable components. The older pattern required forwardRef, which wrapped the component and forwarded the ref to an inner DOM element .
The trade-off. Refs are an escape hatch from React’s declarative model. They bypass the reconciliation and directly manipulate the DOM, which means React does not know about the changes. A ref-based DOM mutation can be overwritten by a subsequent render, and it can cause the DOM to diverge from the virtual DOM. The recommendation is to use refs only for operations that cannot be expressed declaratively: focus, scroll, measurement, media playback, and third-party library integration. For everything else, state and props are the right tools. The trade-off is between the precision of direct DOM access and the safety of the declarative model.
a. Creating and attaching a ref
useRef is called at the top level of a component. It returns an object with a single property, .current, which is initialized to the argument passed to the hook .
import { useRef } from 'react';
function TextInput() {
const inputRef = useRef(null);
return <input ref={inputRef} />;
}
The useRef(null) call creates a ref object. On the first render, React creates the object and sets .current to null. On subsequent renders, React returns the same object. The ref attribute on the <input> tells React to set inputRef.current to the DOM node after the commit.
The .current property is null before the first commit and after the component unmounts. Between the commit and the unmount, it holds the DOM node. This is why the ref cannot be read during the render: the commit has not happened yet .
function TextInput() {
const inputRef = useRef(null);
console.log(inputRef.current); // null during render
useEffect(() => {
console.log(inputRef.current); // <input> after commit
}, []);
return <input ref={inputRef} />;
}
The effect runs after the commit, so .current is populated. The effect’s dependency array is empty, so it runs once, after the first commit. If the element is conditionally rendered, the ref is null when the element is absent and the DOM node when it is present.
b. DOM operations with refs
Once the ref holds a DOM node, any DOM method can be called on it. The common operations are focus, scroll, and measurement .
Focus:
function SearchForm() {
const inputRef = useRef(null);
function handleClick() {
inputRef.current.focus();
}
return (
<>
<input ref={inputRef} />
<button onClick={handleClick}>Focus</button>
</>
);
}
The focus() method is called on the DOM node, not on the ref. The ref is the container; .current is the node. The method is called in an event handler, which runs after the commit, so .current is populated .
Scroll:
function MessageList({ messages }) {
const listRef = useRef(null);
useEffect(() => {
const list = listRef.current;
if (list) {
list.scrollTop = list.scrollHeight;
}
}, [messages]);
return (
<div ref={listRef} style={{ height: 200, overflow: 'auto' }}>
{messages.map((m) => (
<p key={m.id}>{m.text}</p>
))}
</div>
);
}
The effect runs after each render where messages changes. It reads scrollHeight and sets scrollTop to scroll the list to the bottom. The ref is the container element, and the scroll properties are DOM properties .
Measurement:
function MeasuredBox() {
const boxRef = useRef(null);
const [size, setSize] = useState({ width: 0, height: 0 });
useEffect(() => {
const box = boxRef.current;
if (box) {
const { width, height } = box.getBoundingClientRect();
setSize({ width, height });
}
}, []);
return (
<div ref={boxRef}>
<p>Width: {size.width}, Height: {size.height}</p>
</div>
);
}
The getBoundingClientRect() method returns the element’s size and position. The result is stored in state, which triggers a re-render to display the measurements. The effect runs once, after the first commit, so the measurements are taken after the element is in the DOM.
Media playback:
function VideoPlayer({ src }) {
const videoRef = useRef(null);
function handlePlay() {
videoRef.current.play();
}
function handlePause() {
videoRef.current.pause();
}
return (
<>
<video ref={videoRef} src={src} />
<button onClick={handlePlay}>Play</button>
<button onClick={handlePause}>Pause</button>
</>
);
}
The play() and pause() methods are called on the video element. The ref provides the element, and the methods are the browser’s media API.
c. Refs for persistent values
A ref that is not attached to any element is a persistent container. Its .current property can hold any value, and changing it does not cause a re-render. This is the pattern for values that need to survive renders but do not affect the UI .
Timer IDs:
function Stopwatch() {
const [seconds, setSeconds] = useState(0);
const timerRef = useRef(null);
function start() {
if (timerRef.current) return;
timerRef.current = setInterval(() => {
setSeconds((s) => s + 1);
}, 1000);
}
function stop() {
clearInterval(timerRef.current);
timerRef.current = null;
}
useEffect(() => {
return () => clearInterval(timerRef.current);
}, []);
return (
<>
<p>{seconds}</p>
<button onClick={start}>Start</button>
<button onClick={stop}>Stop</button>
</>
);
}
The timer ID is stored in timerRef.current. It is not displayed, so it does not need to be state. The start function checks whether a timer is already running by reading the ref. The cleanup effect clears the timer when the component unmounts, which prevents the interval from firing after the component is gone .
Previous value:
function Counter() {
const [count, setCount] = useState(0);
const prevCountRef = useRef();
useEffect(() => {
prevCountRef.current = count;
}, [count]);
return (
<p>
Now: {count}, Before: {prevCountRef.current}
</p>
);
}
The ref holds the previous value of count. The effect updates it after each render where count changes. During the render, prevCountRef.current is still the previous value, because the effect has not run yet. This is the standard pattern for displaying “previous value” alongside the current one. The ref does not trigger a re-render when it changes, but the effect that updates it runs after the render that changed count, so the displayed value is correct on the next render .
Instance variables:
function Form() {
const [text, setText] = useState('');
const isFirstRender = useRef(true);
useEffect(() => {
if (isFirstRender.current) {
isFirstRender.current = false;
return;
}
console.log('Form updated');
}, [text]);
return <input value={text} onChange={(e) => setText(e.target.value)} />;
}
The ref holds a boolean that tracks whether the component has rendered before. The effect skips the first render by checking the ref. This is a common pattern for “skip the initial effect” logic. The ref is not state because it does not affect the rendered output; it only affects the effect’s behavior .
Storing a callback:
function useInterval(callback, delay) {
const savedCallback = useRef(callback);
useEffect(() => {
savedCallback.current = callback;
}, [callback]);
useEffect(() => {
if (delay === null) return;
const id = setInterval(() => savedCallback.current(), delay);
return () => clearInterval(id);
}, [delay]);
}
The useInterval hook stores the latest callback in a ref, so the interval always calls the current callback without resetting the interval when the callback changes. This is the standard pattern for intervals that need the latest closure. The ref is the right container because the callback does not affect rendering, and changing it should not restart the interval .
Complete Example Session
// ============================================
// PART 1: A BASIC DOM REF
// ============================================
import { useRef } from 'react';
function TextInput() {
const inputRef = useRef(null);
return <input ref={inputRef} />;
}
// inputRef.current is null during render.
// After commit, it is the <input> DOM node.
// ============================================
// PART 2: FOCUS AN INPUT
// ============================================
function SearchForm() {
const inputRef = useRef(null);
function handleClick() {
inputRef.current.focus();
}
return (
<>
<input ref={inputRef} placeholder="Search..." />
<button onClick={handleClick}>Focus</button>
</>
);
}
// ============================================
// PART 3: SCROLL TO BOTTOM
// ============================================
function MessageList({ messages }) {
const listRef = useRef(null);
useEffect(() => {
const list = listRef.current;
if (list) {
list.scrollTop = list.scrollHeight;
}
}, [messages]);
return (
<div ref={listRef} style={{ height: 200, overflow: 'auto' }}>
{messages.map((m) => (
<p key={m.id}>{m.text}</p>
))}
</div>
);
}
// ============================================
// PART 4: MEASURE AN ELEMENT
// ============================================
function MeasuredBox() {
const boxRef = useRef(null);
const [size, setSize] = useState({ width: 0, height: 0 });
useEffect(() => {
const box = boxRef.current;
if (box) {
const { width, height } = box.getBoundingClientRect();
setSize({ width, height });
}
}, []);
return (
<div ref={boxRef}>
<p>Width: {size.width}, Height: {size.height}</p>
</div>
);
}
// ============================================
// PART 5: TIMER ID IN A REF
// ============================================
function Stopwatch() {
const [seconds, setSeconds] = useState(0);
const timerRef = useRef(null);
function start() {
if (timerRef.current) return;
timerRef.current = setInterval(() => {
setSeconds((s) => s + 1);
}, 1000);
}
function stop() {
clearInterval(timerRef.current);
timerRef.current = null;
}
useEffect(() => {
return () => clearInterval(timerRef.current);
}, []);
return (
<>
<p>{seconds}</p>
<button onClick={start}>Start</button>
<button onClick={stop}>Stop</button>
</>
);
}
// ============================================
// PART 6: PREVIOUS VALUE IN A REF
// ============================================
function Counter() {
const [count, setCount] = useState(0);
const prevCountRef = useRef();
useEffect(() => {
prevCountRef.current = count;
}, [count]);
return (
<p>
Now: {count}, Before: {prevCountRef.current}
</p>
);
}
// ============================================
// PART 7: SKIP FIRST RENDER
// ============================================
function Form() {
const [text, setText] = useState('');
const isFirstRender = useRef(true);
useEffect(() => {
if (isFirstRender.current) {
isFirstRender.current = false;
return;
}
console.log('Form updated');
}, [text]);
return <input value={text} onChange={(e) => setText(e.target.value)} />;
}
// ============================================
// PART 8: LIST OF REFS
// ============================================
function ItemList({ items }) {
const itemRefs = useRef([]);
useEffect(() => {
itemRefs.current = items.map((_, i) => itemRefs.current[i] || null);
}, [items]);
function scrollToItem(index) {
itemRefs.current[index]?.scrollIntoView({ behavior: 'smooth' });
}
return (
<>
<ul>
{items.map((item, i) => (
<li key={item.id} ref={(el) => (itemRefs.current[i] = el)}>
{item.text}
</li>
))}
</ul>
<button onClick={() => scrollToItem(2)}>Scroll to Item 3</button>
</>
);
}
// ============================================
// PART 9: CALLBACK REF
// ============================================
function CallbackRefExample() {
const [height, setHeight] = useState(0);
const measuredRef = useCallback((node) => {
if (node !== null) {
setHeight(node.getBoundingClientRect().height);
}
}, []);
return (
<>
<h1 ref={measuredRef}>Hello, world</h1>
<p>Height: {height}px</p>
</>
);
}
// The callback ref is called with the DOM node when
// the element mounts and with null when it unmounts.
// ============================================
// PART 10: THE COMPLETE FORM
// ============================================
function LoginForm() {
const emailRef = useRef(null);
const passwordRef = useRef(null);
const [error, setError] = useState('');
function handleSubmit(e) {
e.preventDefault();
const email = emailRef.current.value;
const password = passwordRef.current.value;
if (!email.includes('@')) {
setError('Invalid email');
emailRef.current.focus();
return;
}
if (password.length < 8) {
setError('Password must be at least 8 characters');
passwordRef.current.focus();
return;
}
setError('');
console.log('Submitted:', { email, password });
}
useEffect(() => {
emailRef.current.focus();
}, []);
return (
<form onSubmit={handleSubmit}>
<div>
<label>Email</label>
<input ref={emailRef} type="email" />
</div>
<div>
<label>Password</label>
<input ref={passwordRef} type="password" />
</div>
{error && <p style={{ color: 'red' }}>{error}</p>}
<button type="submit">Log in</button>
</form>
);
}
The ten parts show a basic DOM ref, focusing an input, scrolling to bottom, measuring an element, a timer ID in a ref, a previous value in a ref, skipping the first render, a list of refs, a callback ref, and a complete form with focus management.
Quick Reference
useRef Signature
| Call | Returns |
|---|---|
useRef(initialValue) | { current: initialValue } |
ref.current | The current value or DOM node |
ref.current = value | Sets the value; no re-render |
Ref vs State
| Aspect | Ref | State |
|---|---|---|
| Triggers re-render | No | Yes |
| Persists across renders | Yes | Yes |
| Read during render | No (not reliable) | Yes |
| Mutated directly | Yes | No (use setter) |
| Use case | DOM, timers, instance vars | UI values |
Ref Timing
| Phase | ref.current |
|---|---|
| During render | null or previous value |
| After commit | DOM node |
| After unmount | null |
| In effect | DOM node (after commit) |
Common Ref Uses
| Use | Pattern |
|---|---|
| Focus | inputRef.current.focus() |
| Scroll | listRef.current.scrollTop = ... |
| Measure | boxRef.current.getBoundingClientRect() |
| Media | videoRef.current.play() |
| Timer | timerRef.current = setInterval(...) |
| Previous value | prevRef.current = value in effect |
Callback Ref
| Form | Behavior |
|---|---|
ref={callbackFn} | Called with node on mount, null on unmount |
useCallback wrapper | Stable callback across renders |
| List of refs | ref={(el) => (refs.current[i] = el)} |
Best Practices
✅ Do This:
// Read ref.current in effects or event handlers
useEffect(() => { inputRef.current.focus(); }, []); // ✅
// Use refs for DOM access
<input ref={inputRef} /> // ✅
// Use refs for values that don't affect rendering
const timerRef = useRef(null); // ✅
// Use useCallback for callback refs
const measuredRef = useCallback((node) => { ... }, []); // ✅
// Clean up timers and subscriptions in effects
return () => clearInterval(timerRef.current); // ✅
// Use a list of refs for lists
ref={(el) => (itemRefs.current[i] = el)} // ✅
❌ Don’t Do This:
// Don't read ref.current during render
console.log(inputRef.current); // null during render // ❌
// Don't use a ref for values that affect rendering
const countRef = useRef(0); // use state instead // ❌
// Don't mutate the DOM in a way React doesn't know about
inputRef.current.value = 'forced'; // may be overwritten // ❌
// Don't forget cleanup for timers
setInterval(() => {}, 1000); // no clearInterval // ❌
// Don't use refs for conditional rendering
{ref.current && <Child />} // does not trigger re-render // ❌
// Don't use a ref when a state variable works
const [count, setCount] = useState(0); // not useRef // ✅
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
ref.current is null | Read during render | Read in effect or event handler |
| Component does not re-render | Ref change does not trigger render | Use state for UI values |
| Ref overwritten by render | DOM mutation conflicts with React | Use declarative rendering where possible |
| Timer not cleared | No cleanup effect | clearInterval in cleanup |
| Callback ref recreated each render | Inline arrow function | Wrap in useCallback |
| List of refs wrong length | Not initialized | Initialize as empty array |
| Ref attached to function component | React 19+ or forwardRef | Use React 19 or forwardRef |
Real-World Examples
1. Basic DOM Ref
const inputRef = useRef(null);
<input ref={inputRef} />
2. Focus on Mount
useEffect(() => { inputRef.current.focus(); }, []);
3. Scroll to Bottom
listRef.current.scrollTop = listRef.current.scrollHeight;
4. Measure Element
const { width } = boxRef.current.getBoundingClientRect();
5. Timer ID
timerRef.current = setInterval(...);
6. Previous Value
useEffect(() => { prevRef.current = value; }, [value]);
7. Skip First Render
const isFirst = useRef(true);
8. List of Refs
ref={(el) => (itemRefs.current[i] = el)}
9. Callback Ref
const measuredRef = useCallback((node) => { ... }, []);
10. Video Playback
videoRef.current.play();
Visual
The Ref Lifecycle
┌──────────────────────────────────────────────────────────────┐
│ THE REF LIFECYCLE │
│ │
│ Render 1 │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ useRef(null) → { current: null } │ │
│ │ JSX evaluated → <input ref={inputRef} /> │ │
│ └──────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ Commit │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ React creates the <input> DOM node │ │
│ │ React sets inputRef.current = <input> │ │
│ └──────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ Effects run │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ useEffect runs after commit │ │
│ │ inputRef.current is the DOM node │ │
│ └──────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ Render 2 (state change) │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ useRef(null) → same object { current: <input> } │ │
│ │ inputRef.current is still the DOM node │ │
│ └──────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ Unmount │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ React sets inputRef.current = null │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────┘
Ref vs State
┌──────────────────────────────────────────────────────────────┐
│ REF vs STATE │
│ │
│ STATE: │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ const [count, setCount] = useState(0) │ │
│ │ │ │
│ │ setCount(1) │ │
│ │ → re-render │ │
│ │ → count is 1 in the new render │ │
│ │ │ │
│ │ Use when the value affects the rendered output. │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ REF: │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ const countRef = useRef(0) │ │
│ │ │ │
│ │ countRef.current = 1 │ │
│ │ → no re-render │ │
│ │ → countRef.current is 1 │ │
│ │ │ │
│ │ Use when the value does not affect the output. │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ The question: does the UI depend on the value? │
│ Yes → state │
│ No → ref │
│ │
└──────────────────────────────────────────────────────────────┘
DOM Ref Access
┌──────────────────────────────────────────────────────────────┐
│ DOM REF ACCESS │
│ │
│ function TextInput() { │
│ const inputRef = useRef(null); │
│ │
│ return <input ref={inputRef} />; │
│ } │
│ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Render: inputRef.current = null │ │
│ │ Commit: inputRef.current = <input> │ │
│ │ Effect: inputRef.current = <input> │ │
│ │ │ │
│ │ inputRef.current.focus() → focuses the input │ │
│ │ inputRef.current.value → the input's value │ │
│ │ inputRef.current.scrollTop → scroll position │ │
│ │ inputRef.current.getBounding... → size and position │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ The ref is the container. .current is the DOM node. │
│ The DOM methods are called on .current. │
│ │
└──────────────────────────────────────────────────────────────┘
List of Refs
┌──────────────────────────────────────────────────────────────┐
│ LIST OF REFS │
│ │
│ const itemRefs = useRef([]); │
│ │
│ <ul> │
│ {items.map((item, i) => ( │
│ <li │
│ key={item.id} │
│ ref={(el) => (itemRefs.current[i] = el)} │
│ > │
│ {item.text} │
│ </li> │
│ ))} │
│ </ul> │
│ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ itemRefs.current[0] = <li> for item 0 │ │
│ │ itemRefs.current[1] = <li> for item 1 │ │
│ │ itemRefs.current[2] = <li> for item 2 │ │
│ │ │ │
│ │ Each ref callback is called with the DOM node │ │
│ │ when the element mounts, and with null when it │ │
│ │ unmounts. │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ This is the pattern for accessing individual items in a │
│ list, such as scrolling to a specific item or measuring │
│ each one. │
│ │
└──────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
useRef | Returns { current: initialValue } |
| Persistence | Same object across renders |
| Re-render | Changing .current does not trigger a render |
| DOM access | ref attribute sets .current to the DOM node |
| Timing | .current is null during render, set after commit |
| Cleanup | .current is null after unmount |
| Ref vs state | Ref for non-rendering values; state for rendering values |
| Callback ref | ref={(el) => ...} called on mount and unmount |
| List of refs | useRef([]) with ref callbacks |
| Forwarding | React 19+ or forwardRef for function components |
Key takeaways:
useRefreturns a mutable object that persists across renders. The object has a.currentproperty that holds the value. React creates the object once and returns the same object on every render. Changing.currentdoes not trigger a re-render .- The
refattribute attaches a DOM node to a ref. When a ref is passed to a JSX element, React sets.currentto the DOM node after the commit. Before the commit,.currentisnull. After the unmount, it isnullagain . - Refs are for DOM access and persistent values. A ref attached to an element gives you the DOM node for imperative operations: focus, scroll, measure, play. A ref not attached to an element holds a value that persists across renders without causing re-renders: a timer ID, a previous value, a connection object .
- The distinction between a ref and state is rendering. If the value affects the rendered output, it is state. If it does not, it can be a ref. Putting a timer ID in state causes unnecessary re-renders; putting it in a ref avoids them .
- Refs cannot be read during the render. The DOM node does not exist until after the commit. The ref must be read in an effect or an event handler, after React has updated the DOM. Reading it during the render returns
nullor a stale value . - Callback refs provide more control. A callback ref is a function that receives the DOM node when the element mounts and
nullwhen it unmounts. It is useful for measuring elements and for lists of refs. The callback should be wrapped inuseCallbackto avoid unnecessary calls. - Lists of refs use a ref that holds a collection. The pattern is
useRef([])and a ref callback that assigns each node to an index. The collection is not a single DOM node; it is an array or aMapof nodes. - Refs are an escape hatch. They bypass React’s declarative model and manipulate the DOM directly. React does not know about the changes, and a subsequent render can overwrite them. Use refs only for operations that cannot be expressed declaratively.
Remember: useRef is the hook for values that persist across renders without causing them. When attached to a JSX element, it provides the DOM node for imperative operations. When used as a container, it holds any value that needs to survive renders but does not affect the UI. The distinction between a ref and state is the distinction between values the UI depends on and values it does not. Refs cannot be read during the render; they are read in effects and event handlers, after the commit. For focus, scroll, measurement, media playback, timers, and previous values, the ref is the right tool. For anything that appears in the rendered output, state is the right tool.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!