useRef from Basic to Best Practice
Understand how useRef works, when to use it instead of state, how refs interact with renders, and the patterns that make them useful in real React applications.
1. What Is useRef?
Start with the simplest possible example:
const countRef = useRef(0);Explain that useRef gives you a mutable object whose .current value persists between renders.
Then introduce the core mental model:
useStateis for data that affects rendering.useRefis for data that needs to persist without triggering a render.
Show a simple example:
const renderCount = useRef(0);
renderCount.current += 1;Explain that changing current does not cause React to render again.
2. The Mental Model: useRef Survives Renders
Explain that every render creates new local variables:
function Component() {
let value = 0;
}The variable doesn't persist between renders.
But:
const value = useRef(0);does.
The same ref object remains available across renders:
Render 1
↓
ref.current = 0
↓
Render 2
↓
same ref object
↓
ref.current = 1This is the first major concept readers should understand.
3. useRef vs useState
This should be one of the most important sections.
Compare:
const [count, setCount] = useState(0);with:
const countRef = useRef(0);Explain the difference:
useState | useRef | |
|---|---|---|
| Persists between renders | Yes | Yes |
| Changing value causes render | Yes | No |
| Value accessed through | variable | .current |
| Intended for UI state | Yes | No |
| Can hold DOM references | No | Yes |
Then show the practical rule:
If changing the value should update the UI, use state.
If the value needs to persist but shouldn't update the UI, consider a ref.
4. The Most Common Use Case: DOM References
Introduce the classic example:
const inputRef = useRef<HTMLInputElement>(null);
return (
<>
<input ref={inputRef} />
<button onClick={() => inputRef.current?.focus()}>
Focus input
</button>
</>
);Explain what React does when the element mounts and why current initially contains null.
This naturally introduces:
- DOM references
.current- nullable refs
- accessing DOM APIs safely
5. Why Changing .current Doesn't Re-render
This is the useRef equivalent of the reference article's “state doesn't update immediately” section.
Show:
const countRef = useRef(0);
const handleClick = () => {
countRef.current += 1;
console.log(countRef.current);
};The value changes immediately, but the component doesn't re-render.
Then demonstrate why this doesn't update the UI:
<p>{countRef.current}</p>The value can change internally while the displayed UI remains unchanged.
This is an important distinction:
A ref stores a value. It does not tell React that the UI needs to change.
6. Don't Use useRef as a Replacement for State
Show a tempting but incorrect pattern:
const count = useRef(0);
const increment = () => {
count.current += 1;
};If the UI displays:
<p>{count.current}</p>the screen won't update.
Then show the correct version:
const [count, setCount] = useState(0);
const increment = () => {
setCount(prev => prev + 1);
};The rule should be clear:
If the value affects what you render, it probably belongs in state.
7. Storing Values Between Renders
Now move beyond DOM elements.
Explain that refs are useful for values such as:
- timer IDs
- previous values
- mutable instances
- external library objects
- flags that don't affect rendering
Example:
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const startTimer = () => {
timerRef.current = setTimeout(() => {
console.log("Done");
}, 1000);
};The timer ID needs to survive renders, but changing it doesn't need to update the UI.
8. Tracking the Previous Value
A very practical pattern:
const previousValue = useRef(value);
useEffect(() => {
previousValue.current = value;
}, [value]);Then:
console.log(previousValue.current);Explain why this works and why state would introduce unnecessary rendering in this particular case.
This is a good place to discuss the difference between:
- current value
- previous value
- render-time values
9. useRef and Closures
This should be one of the more advanced sections.
Show a callback that captures an old value:
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
console.log(count);
}, 1000);
return () => clearInterval(id);
}, []);Then introduce a ref as a way to keep access to the latest value:
const countRef = useRef(count);
useEffect(() => {
countRef.current = count;
}, [count]);Now asynchronous code can read:
console.log(countRef.current);Explain the important distinction:
useRefdoesn't magically fix closures. It gives you a mutable location that asynchronous callbacks can read from later.
10. Managing Timers and Intervals
Build a realistic example:
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
const start = () => {
intervalRef.current = setInterval(() => {
// ...
}, 1000);
};
const stop = () => {
if (intervalRef.current) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
};Explain why this belongs in a ref instead of state.
The timer ID is an implementation detail, not something the user needs rendered.
11. useRef for Third-Party Libraries
Show how refs can hold objects created by libraries:
const editorRef = useRef<Editor | null>(null);Then:
editorRef.current = new Editor(...);This pattern is especially useful for:
- editors
- charts
- maps
- animations
- WebSocket connections
- third-party UI libraries
The key idea:
React manages the UI. The ref can hold an imperative object that exists alongside that UI.
12. Callback Refs vs useRef
Introduce callback refs briefly:
<div
ref={node => {
// node is available here
}}
/>Explain when callback refs can be useful compared with:
const elementRef = useRef<HTMLDivElement>(null);Don't make this section too deep unless the article is aimed at experienced React developers.
13. useRef Initialization
Discuss:
const instanceRef = useRef(new ExpensiveThing());versus lazily creating an object when needed.
For objects that should only be created once, explain why blindly putting an expensive constructor into the argument can still execute that expression during rendering.
A safer pattern can be:
const instanceRef = useRef<ExpensiveThing | null>(null);
if (instanceRef.current === null) {
instanceRef.current = new ExpensiveThing();
}This is a good advanced best-practice section.
14. Common useRef Mistakes
Create a section similar to the reference article's “Common Mistakes to Avoid.”
Mistake 1: Using refs for UI state
const valueRef = useRef(0);when the value should appear on screen.
Mistake 2: Expecting .current changes to trigger rendering
ref.current = newValue;doesn't cause a render.
Mistake 3: Reading or writing refs during render unnecessarily
Refs are primarily for values that live outside React's reactive rendering flow.
Mistake 4: Forgetting cleanup
Especially for:
- intervals
- timeouts
- subscriptions
- event listeners
- external instances
Mistake 5: Using refs to avoid understanding state
A ref shouldn't become an escape hatch for every state-management problem.
15. useRef vs useState vs useMemo
A useful comparison for the final part:
useState
Use when:
The value affects the rendered UI.
useRef
Use when:
The value must persist but changing it doesn't need to re-render.
useMemo
Use when:
You want to cache an expensive calculation between renders.
Make it clear that these hooks solve different problems rather than being interchangeable optimization tools.
16. Putting Everything Together
End with a realistic component that combines several concepts:
- input DOM ref
- previous value ref
- timer ref
- state for UI
- cleanup
- event handlers
For example:
function SearchBox() {
const inputRef = useRef<HTMLInputElement>(null);
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const previousSearch = useRef("");
const [search, setSearch] = useState("");
const handleSearch = (value: string) => {
previousSearch.current = search;
setSearch(value);
if (timerRef.current) {
clearTimeout(timerRef.current);
}
timerRef.current = setTimeout(() => {
console.log("Searching:", value);
}, 300);
};
return (
<div>
<input
ref={inputRef}
value={search}
onChange={event => handleSearch(event.target.value)}
/>
<button onClick={() => inputRef.current?.focus()}>
Focus
</button>
</div>
);
}Then explain what each piece is responsible for.
17. Best Practices Checklist
Before adding a ref, ask:
- Does this value need to survive a render?
- Should changing this value update the UI?
- Am I storing a DOM element?
- Am I storing a timer or subscription ID?
- Does an asynchronous callback need access to the latest value?
- Am I using a ref to avoid unnecessary renders?
- Should this actually be state instead?
- Does this ref require cleanup?
A simple rule:
State drives the UI. Refs store mutable values that exist outside the UI.
Conclusion
useRef looks like a small Hook, but understanding it requires understanding one of React's most important ideas: not every value in a component needs to participate in rendering.
Once you understand that distinction, useRef becomes much easier to reason about.
You'll know when to use refs for DOM elements, timers, previous values, external instances, and asynchronous callbacks.
More importantly, you'll know when not to use them.
The goal isn't to use useRef more often.
It's to understand exactly when a value belongs in React's rendering system—and when it doesn't.