How to Access a DOM Element in React
Advertisement
🧩 Scenario
Architecture Walkthrough
The Ref Lifecycle
useRef() returns a plain JavaScript object { current: null }. React uses the ref prop to populate current after the component mounts: when React finishes rendering and inserting the DOM nodes, it sets ref.current to the actual HTMLElement. When the component unmounts, React sets ref.current back to null.
This timing has a critical implication: ref.current is always null during the render phase. Accessing it during rendering, in a computed value, or in the render return is always reading a null. The correct place to access a DOM ref is inside useEffect, which runs after the component has mounted and the DOM nodes exist. Any DOM operation (focus, scroll, measure, call an imperative API) belongs in useEffect with the ref.
Refs Do Not Trigger Re-renders
Changing ref.current directly is a mutation of a plain object, not a state update. React has no awareness of this change and does not schedule a re-render. This is what makes refs appropriate for values that need to persist across renders without causing renders: a timer ID, a previous value, a pending animation frame. If a value change needs to update the UI, it must go in useState, not useRef.
forwardRef for Component Boundaries
A ref prop passed to a custom function component does not automatically reach the inner DOM node. React does not forward refs by default because it would break component encapsulation. To expose an inner DOM node to a parent, wrap the component in React.forwardRef, which receives both props and ref as arguments and attaches the ref to the desired inner element.
Key Code Explained
import { useRef, useEffect } from 'react';
// Basic pattern: focus input on mount
function AutoFocusInput() {
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
// ref.current is the actual <input> DOM node here
inputRef.current?.focus();
}, []); // empty array = runs once after mount
return <input ref={inputRef} placeholder='I focus on mount' />;
}
// Callback ref: runs when React attaches or detaches the element
// Useful for measuring elements where dimensions are unknown until render
function MeasuredBox() {
const [height, setHeight] = useState<number | null>(null);
const measuredRef = useCallback((node: HTMLDivElement | null) => {
if (node !== null) {
// node is the DOM element — safe to measure here
setHeight(node.getBoundingClientRect().height);
}
}, []); // stable callback — no recreations on re-render
return (
<div>
<div ref={measuredRef} className='content'>
Dynamic content with unknown height
</div>
{height !== null && <p>Height: {height}px</p>}
</div>
);
}
// forwardRef: expose child DOM node to parent
const TextInput = React.forwardRef<HTMLInputElement, { label: string }>(
({ label }, ref) => {
return (
<label>
{label}
{/* ref is forwarded to the actual <input> element */}
<input ref={ref} type='text' />
</label>
);
},
);
// Parent can now attach a ref to TextInput and access the inner <input>
function Form() {
const nameRef = useRef<HTMLInputElement>(null);
const focusName = () => nameRef.current?.focus();
return (
<form>
<TextInput ref={nameRef} label='Full name' />
<button type='button' onClick={focusName}>
Focus name field
</button>
</form>
);
}
// Ref vs state: choosing the right tool
function Timer() {
const [seconds, setSeconds] = useState(0);
// timerRef stores the interval ID without triggering re-renders
// If we used useState for the ID, clearing and restarting the timer
// would cause an extra render, and the ID might be stale in a closure
const timerRef = useRef<ReturnType<typeof setInterval> | null>(null);
const start = () => {
timerRef.current = setInterval(() => {
setSeconds((prev) => prev + 1); // useState for the visible value
}, 1000);
};
const stop = () => {
if (timerRef.current !== null) {
clearInterval(timerRef.current);
}
};
return (
<div>
<p>{seconds}s</p>
<button onClick={start}>Start</button>
<button onClick={stop}>Stop</button>
</div>
);
}
The callback ref pattern (useCallback((node) => ...)) runs synchronously when React attaches or detaches the node. It is the correct approach when you need to measure element dimensions that are only available after the element is in the DOM. A useRef + useEffect pattern can miss measurement if the element's size changes due to content, but a callback ref fires precisely when the node is attached.
Tradeoffs
| Pattern | When to use | Triggers re-render |
|---|---|---|
| useRef | Stable DOM access, timer IDs, previous values | No |
| useState | Values that affect rendered output | Yes |
| Callback ref | Measuring elements on attachment | Yes (only when calling setState inside) |
| forwardRef | Exposing child DOM node to parent | No |
What Interviewers Actually Check
- Whether you know
ref.currentis null during render and why - Whether you access DOM refs inside
useEffect, not directly in the component body - Whether you know
refchanges do not trigger re-renders - Whether you know
React.forwardRefis required to pass a ref to a custom component - Whether you can distinguish when to use
useRefvsuseState
Follow-Up Questions
- What is
useImperativeHandleand when would you use it withforwardRef? - How do you attach a ref to a React component instance (class component) versus a DOM node?
- Why might accessing
ref.currentin an event handler be safe even though it is unsafe during render? - How do you use a ref to track the previous value of a prop across renders?
- What happens to a ref when the component re-renders? Does
currentget replaced with a new DOM node?
Common Candidate Mistakes
- Reading
ref.currentduring the render function body or in JSX — it is null at this point - Using
document.querySelector('#my-input')instead of a ref — selects globally, not scoped, and breaks with multiple instances - Using a ref to store a value that the UI needs to display — ref changes are invisible to React and the UI will not update
- Passing
refas a named prop to a custom component withoutforwardRef— the ref is silently dropped - Calling
ref.current.focus()without optional chaining — throws if the element has unmounted between effect setup and execution
Interview Readiness Checklist
Before you leave this question, make sure you can answer:
- Can you attach a ref to a DOM element and access it in
useEffect? - Can you explain why
ref.currentis null during render? - Can you write a callback ref to measure an element's height after mount?
- Can you use
React.forwardRefto expose a child component's DOM node to a parent? - Can you explain when to use
useRefversususeStatefor mutable values?
Summary
useRef creates a { current: null } object that React populates with the DOM node after mount. The ref prop on a JSX element binds the ref to that node. Access ref.current only inside useEffect (or event handlers), never during rendering, because the DOM node does not exist yet during the render phase. Changing ref.current does not trigger a re-render. Callback refs (a function passed as the ref prop) fire synchronously when the node is attached, making them ideal for measuring elements whose dimensions are unknown until render. To pass a ref through a component boundary, wrap the component in React.forwardRef, which receives both props and ref as separate arguments and attaches the ref to the intended inner element.
Can I use document.querySelector to access DOM elements in React?
Technically yes, but it is wrong. document.querySelector has no awareness of which component instance you want, it searches the entire document, and it can select elements from other components. Refs scope DOM access to the specific element instance and are the correct React pattern.
Advertisement
Stay Updated
Get the latest frontend challenges, interview questions and tutorials delivered to your inbox.
Advertisement