React
Chapter 09: useRef & DOM Interaction — Accessing real DOM nodes, storing mutable values without re-renders, previous state tracking, and video/audio control.
1. What is useRef?
useRef is a built-in React hook that returns a mutable object: { current: initialValue }. It has two distinct superpowers:
- Persists across renders: Unlike normal local variables, its value is remembered between render cycles.
- Does NOT trigger a re-render: Mutating
ref.current = newValuemodifies data silently without asking React to recalculate the UI.
💡 Real-World Analogy: Secret Pocket Notebook vs Wall Whiteboard
Think of storing data inside a component:
- State (Wall Whiteboard): Whenever you change something on the whiteboard, the whole room stops and updates their notes (causes a re-render).
- useRef (Secret Pocket Notebook): You take out your notebook, jot down a timer ID or secret coordinate, and put it back in your pocket. No one in the room notices, and nothing re-renders!
2. State vs Ref vs Regular Variable
useState
UI Data
- Persists across renders.
- Triggers re-render on change.
- Use for values displayed on screen.
useRef
Silent Memory & DOM
- Persists across renders.
- Never triggers re-render.
- Use for DOM nodes, timer IDs, previous values.
let / const
Transient Data
- Resets every render.
- No re-render trigger.
- Use only for temporary derived math.
3. Use Case 1: Direct DOM Element Access
Attach a ref to any JSX element via the ref attribute. After React mounts the real DOM node, it assigns the underlying HTML node to ref.current:
function AutoFocusSearch() {
const inputRef = React.useRef(null); // 1. Create Ref
function handleFocusClick() {
// 2. Direct DOM method execution:
inputRef.current.focus();
inputRef.current.style.borderColor = "#6C4CE8";
}
React.useEffect(() => {
// Auto-focus input on initial page load:
if (inputRef.current) {
inputRef.current.focus();
}
}, []);
return (
<div className="search-box">
<input ref={inputRef} placeholder="Search documentation..." />
<button onClick={handleFocusClick}>Focus Input</button>
</div>
);
}
4. Use Case 2: Storing Mutable Values (Timers & Previous State)
Because modifying ref.current does not cause a re-render, it is the ideal place to store interval IDs, click debouncers, or track previous state snapshots:
function Stopwatch() {
const [seconds, setSeconds] = React.useState(0);
const timerRef = React.useRef(null); // Stores timer ID silently
function startTimer() {
if (timerRef.current !== null) return; // Already running
timerRef.current = setInterval(() => {
setSeconds(prev => prev + 1);
}, 1000);
}
function stopTimer() {
clearInterval(timerRef.current);
timerRef.current = null; // Clear reference
}
return (
<div>
<h3>Time: {seconds}s</h3>
<button onClick={startTimer}>Start</button>
<button onClick={stopTimer}>Stop</button>
</div>
);
}
5. Hands-On Practice Challenges
Master direct DOM operations and persistent ref containers with these interactive coding exercises. Click "Solve Challenge" to open the editor!
Challenge 1: Replicate useRef Container Object
Write a factory function createRefObject(initialValue) that creates and returns a persistent mutable ref container object { current: initialValue }.
- Return an object with a single
currentproperty. - Initialize
currentwithinitialValue(defaultnull). - Ensure mutating
currentupdates the object reference in place.
Ref initial value: "initial_text" | Ref updated value: "updated_text"
createRefObject to construct the mutable reference box.
Challenge 2: Previous State Tracker Simulation
Implement a closure trackPreviousState() that stores the latest value and returns both the current value and the previous value on each update.
- Track
currentValandprevValin closure memory. - On
update(newVal), slidecurrentValtoprevVal. - Return
{ current, previous }.
Step 1: { current: "First", previous: null } | Step 2: { current: "Second", previous: "First" }
trackPreviousState and click "▶ Run Code".
Challenge 3: Silent Render Counter with Ref
Write a function createRenderCounter() that uses a ref object to increment and return the render count on every call without triggering any state updates.
- Store counter in a
{ current: 0 }ref container. - Increment
ref.currenton each invocation. - Return string formatted with current count.
Component has rendered 1 times. | Component has rendered 2 times.
createRenderCounter and run the code.