useRef

Course 2 · Ch 1
useRef
A place to keep a value that doesn't need to trigger a re-render — including direct access to a real DOM node

Welcome to Intermediate. Every piece of changing data covered so far has gone through useState, specifically because changing it needed to update the screen. Not everything fits that description — sometimes a value just needs to persist between renders without ever causing one, or a component needs to reach directly into the actual DOM node React rendered. useRef is the hook for both of those cases.

Accessing a Real DOM Node

import { useRef } from "react"; function SearchBox() { const inputRef = useRef(null); function handleFocusClick() { inputRef.current.focus(); } return ( <div> <input ref={inputRef} /> <button onClick={handleFocusClick}>Focus the input</button> </div> ); }

useRef(null) creates a ref object with a single property, current, starting at null. Passing it to an element's special ref attribute tells React to set inputRef.current to that exact DOM node once it's rendered — from then on, inputRef.current is the real <input> element, with every normal DOM method (.focus(), .scrollIntoView(), reading .offsetHeight, and so on) available on it directly.

The Key Difference from useState

function RenderCounter() { const renderCount = useRef(0); renderCount.current = renderCount.current + 1; return <p>This component has rendered {renderCount.current} times.</p>; }

Updating renderCount.current directly — no setter function, just a plain assignment — works perfectly well, and the new value is there on the next render. What it doesn't do is cause a re-render. If nothing else in this component ever updates, the displayed count would only ever change because something else (a parent re-rendering it, for instance) triggers a new render for a different reason. This is the core distinction from useState: a ref update is invisible to React's rendering system entirely.

useStateuseRef
Changing the value...Triggers a re-renderDoes not trigger a re-render
Read/write via...value / setter functionref.current, direct assignment
Good for...Anything shown in the UIDOM access, or data that doesn't affect what's rendered

Storing a Mutable Value Across Renders

function Stopwatch() { const [seconds, setSeconds] = useState(0); const intervalRef = useRef(null); function handleStart() { intervalRef.current = setInterval(() => setSeconds((s) => s + 1), 1000); } function handleStop() { clearInterval(intervalRef.current); } return ( <div> <p>{seconds}s</p> <button onClick={handleStart}>Start</button> <button onClick={handleStop}>Stop</button> </div> ); }

Fundamentals Chapter 8's clock example stored its interval id as a plain variable inside the effect, cleaned up automatically when the effect re-ran. Here, the interval needs to be started and stopped from two separate event handlers — handleStart and handleStop — rather than a single effect's cleanup, so the id needs to survive between renders without living inside any one function call. A ref is exactly built for that: a box that keeps holding its value across every re-render, completely independent of the render cycle itself.

Don't read or write a ref during rendering for values that affect the UI
Reading someRef.current directly inside the JSX a component returns (rather than inside an event handler or effect) works technically, but breaks the assumption that rendering the same props/state twice produces the same output — since a ref's value can silently differ between two renders without React ever knowing to re-render in response. If a value needs to show up in the UI, that's the signal it belongs in useState instead, not a ref.

Coding Challenges

Challenge 1

Build a component with a text input that automatically receives focus the moment the component first appears, using useRef together with useEffect (empty dependency array).

📄 View solution
Challenge 2

Build a component with a count state value and a button that increments it. Alongside it, track how many times the button has been clicked using a separate ref, and display both numbers — they should always match, but only one of them is responsible for the re-render that shows the updated count.

📄 View solution
Challenge 3

Build a Stopwatch component with Start and Stop buttons, storing the interval id in a ref so handleStart and handleStop can both access the same id across separate function calls.

📄 View solution

Chapter 1 Quick Reference

  • useRef(initialValue) — returns { current: initialValue }, persisting across renders
  • Changing ref.current does not trigger a re-render — unlike useState's setter
  • The ref attribute on a JSX element gives ref.current direct access to that real DOM node
  • Good ref use cases: focusing an input, storing a timer/interval id across separate handlers, anything that doesn't need to appear in the UI
  • If a value needs to be shown on screen, it belongs in state, not a ref
  • Next chapter: the Context API — sharing data without prop drilling, first previewed back in Project 6