useRef
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
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
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.
| useState | useRef | |
|---|---|---|
| Changing the value... | Triggers a re-render | Does not trigger a re-render |
| Read/write via... | value / setter function | ref.current, direct assignment |
| Good for... | Anything shown in the UI | DOM access, or data that doesn't affect what's rendered |
Storing a Mutable Value Across Renders
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.
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
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 solutionBuild 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 solutionBuild 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 solutionChapter 1 Quick Reference
- useRef(initialValue) — returns
{ current: initialValue }, persisting across renders - Changing
ref.currentdoes not trigger a re-render — unlikeuseState's setter - The ref attribute on a JSX element gives
ref.currentdirect 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