React memory comprises of Fiber Nodes and Blueprints. But eventually, something has to touch the actual Browser DOM. This unit covers the boundary layer between React's memory and the Browser's reality.
<div className="hidden" />), and React figures out how to update the DOM.element.focus(), video.play(), window.scrollTo()).The Gap:
We cannot pass a focus={true} prop to a standard HTML input. There is no declarative attribute for playing a video or measuring the width of a div. React's data flow hits a wall here. To cross this wall, we need an Escape Hatch.
useRef returns a mutable object { current: initialValue }. This object persists for the entire lifetime of the component.
Inside the Fiber Node:
useState lives in memoizedState. When it changes, React flags the Fiber as dirty and triggers a re-render.useRef also lives in memoizedState inside the same Fiber Node.The Difference:
useRef is just a plain JavaScript object stored on the Fiber. When we change ref.current = 5:
Since useRef is just a persistent container, we use it for two very different things:
This is the most common usage. We pass the ref object to a JSX element. React puts the actual DOM node into .current.
function SearchBar() {
const inputRef = useRef(null);
const handleFocus = () => {
// Access the DOM directly to call imperative methods
inputRef.current.focus();
};
return (
<>
<input ref={inputRef} type="text" />
<button onClick={handleFocus}>Focus</button>
</>
);
}We can use refs to store values that need to survive renders (like a Timer ID or previous props) but should not trigger a UI update when they change.
function Timer() {
const intervalRef = useRef(null); // distinct from state!
const startTimer = () => {
// We store the ID here. Updating it won't re-render the component.
intervalRef.current = setInterval(() => console.log("Tick"), 1000);
};
const stopTimer = () => {
clearInterval(intervalRef.current);
};
return <button onClick={stopTimer}>Stop</button>;
}Knowing when to access a ref is critical to avoiding null errors.
ref.current here. The DOM node doesn't exist yet, or it refers to the old DOM node.return <div>{ref.current.clientWidth}</div> (Will crash or show old data).ref object, setting .current to the new DOM node. This is the safe time to read/write them (usually inside useEffect).When we write <button onClick={handleClick}>, we are not adding a standard DOM Event Listener to that button.
root (or document) of our app. This is called Event Delegation.root.Why does this matter? It proves again that React is an abstraction layer. It isolates us from the Real World (Native DOM Events) to optimize performance (fewer listeners) and ensure consistency.
| Concept | The React Way | The Browser Way | The Bridge |
|---|---|---|---|
| Updates | Declarative (state) | Imperative (.focus()) | useRef |
| Events | SyntheticEvent | Native Event | Event Delegation |
| Storage | Fiber (memoizedState) | DOM Nodes | ref.current |
| Timing | Render Phase (Abstract) | Paint (Visual) | Layout Phase |
If we create a Scroll Progress Bar component, we might be tempted to store scrollPosition in useState.
window.scrollTo(state)), we fight the browser.useRef to simply read the value when needed, or sync strictly for UI display, not control.ref is null on first renderconst inputRef = useRef(null);
console.log(inputRef.current); // -> null
return <input ref={inputRef} />;Why is it null?
Because console.log runs during the Render Phase. React is still building the Blueprint. The input element hasn't been created in the browser yet. React can't give us the DOM node until the Commit Phase finishes.