useRef Is React's Most Underrated Hook — It Handles DOM, Force Renders, and Web Workers
One Article to Understand useRef: Focusing DOM, Simulating forceRender, Managing Workers All Rely on It
React's most underestimated Hook, capable of far more than just binding to DOM.
Introduction
Many React beginners' impression of useRef stops at "using it to focus an input box," and then nothing more. In reality, useRef's capabilities go far beyond that—it can store mutable values without triggering re-renders, simulate forceRender, and manage Web Worker threads.
This article takes you through three real-world scenarios to thoroughly understand the underlying logic of useRef. Complete runnable project code is attached at the end; clone and get started immediately.
Target Audience: React beginners who know how to use useState but haven't truly understood useRef.
Project Overview
ref-demo contains two sub-projects, covering three typical use cases of useRef:
ref-demo/
├── readme.md ← Knowledge notes (DOM programming, reactivity, Web Worker)
├── ref-focus-demo/ ← useRef manipulating DOM + forceRender pattern
│ └── src/App.jsx
└── ref-worker-demo/ ← useRef holding a Web Worker instance
├── src/App.jsx
└── src/worker.js
| Sub-project | Core Knowledge Points |
|---|---|
| ref-focus-demo | useRef binding to DOM, useRef vs useState (reactive vs non-reactive), forceRender pattern |
| ref-worker-demo | useRef holding Web Worker, JS single-thread / event loop, main thread and Worker communication |
Dependency Versions: React 19.2.6, Vite 8.0.12
Core Knowledge Point 1: What is useRef? Why is it needed when we have useState?
One-sentence Understanding
useRef returns a mutable "container object". This object has a current property where you can put any value. Modifying it does not trigger a component re-render.
const ref = useRef(null)
// ref is just a plain object: { current: null }
ref.current = 'hello'
// ref.current is now 'hello', but the component will not re-render
Analogy
| Concept | Analogy |
|---|---|
useState |
A sticky note on the fridge—when the content changes, the whole family can see it (triggers a render) |
useRef |
A notepad in your pocket—you write things down, only you know, others don't (does not trigger a render) |
useState vs useRef Comparison
// useState — Reactive: change value → re-render
const [count, setCount] = useState(0)
setCount(1) // ✅ Page updates automatically
// useRef — Non-reactive: change value → no render
const countRef = useRef(0)
countRef.current = 1 // ❌ Page will not update
React's Design Philosophy: 90% of scenarios use reactive
useStateto drive the UI, 10% of edge cases useuseRef. The two work together, not as an either/or choice.
Core Knowledge Point 2: useRef Manipulating DOM — autoFocus Alternative
Scenario
An input box automatically focuses as soon as it appears, without a mouse click. HTML has the autoFocus attribute, but useRef + useEffect is more flexible—you can trigger focus at any time.
Code
import { useRef, useEffect } from 'react'
const App = () => {
const inputRef = useRef(null) // ① Create ref container
useEffect(() => {
inputRef.current.focus() // ③ After mount, ref.current points to the real DOM, manually focus
}, [])
return (
<input
ref={inputRef} // ② Bind to JSX element
type="text"
placeholder="Please enter username"
/>
)
}
Execution Timeline of This Code
Component function executes → Returns JSX → React creates DOM nodes → Mounts to page
↑ ↓
If ref.current.focus() is called here ref.current is only assigned at this point
→ ref.current is still null, error! ✅
| Step | What Happens | Value of ref.current |
|---|---|---|
| Component function executes | Calls useRef(null), creates container |
null |
| JSX returned | Declares <input ref={inputRef}> |
Still null (DOM not yet created) |
| React commit | Real <input> mounted to page |
React assigns the DOM node to ref.current |
useEffect callback |
DOM now exists, safe to call .focus() |
Points to HTMLInputElement |
Underlying Logic
Why must useEffect be used? Because the DOM does not exist when the component function executes—JSX is just a "decoration blueprint." React needs to first build the house according to the blueprint (create the DOM), and only after the house is built will it execute useEffect.
autoFocusdoesn't needuseEffectbecause it's a native browser attribute; the browser itself handles it after the element is created—it doesn't depend on JS call timing.
Another Solution in the Commented Code
The commented-out code in the project shows a version combining useRef + useState:
// Commented version: ref binds input + useState drives count
const [count, setCount] = useState(0)
const inputRef = useRef(null)
// inputRef only manages DOM focus, count manages business value, each doing its own job
This is a "separation of concerns" pattern—state manages UI data, ref manages DOM operations, without interfering with each other.
Core Knowledge Point 3: forceRender Pattern — Using useState as a "Manual Refresh Button"
What Scenarios Require It?
You have some data that doesn't need to trigger renders, but you want to manually refresh the page at certain times. For example, high-frequency update counters, animation frames, real-time data streams—if useState is used and triggers a re-render on every update, performance collapses.
Code (Current version of ref-focus-demo)
const App = () => {
const numRef = useRef(0) // ① Silent ledger
const [, forceRender] = useState(0) // ② Only take the setter, the state value itself is not used
return (
<div onClick={() => {
numRef.current += 1 // ③ Change value but don't render
forceRender(numRef.current) // ④ Manually trigger a render
}}>
{numRef.current} // ⑤ Read the latest value from ref during render
</div>
)
}
Line-by-Line Breakdown
| Code | Purpose |
|---|---|
useRef(0) |
Creates a non-reactive container to store a number |
const [, forceRender] = useState(0) |
Uses a comma in destructuring to skip the state value, only taking the setter. The meaning of this setter changes—it's no longer "update the state value," but rather "notify React to re-render" |
numRef.current += 1 |
Silently changes the value, page doesn't move |
forceRender(numRef.current) |
Calls the setter → React re-renders → Component re-executes → numRef.current reads the latest value → Page updates |
Why Not Just Use useState?
// useState version: every click triggers a render
const [num, setNum] = useState(0)
setNum(num + 1) // Change value + trigger render, two steps in one
// useRef + forceRender version: changing value and rendering are decoupled
numRef.current += 1 // Only change value
forceRender(...) // Only trigger render
In ordinary scenarios, useState is sufficient. But when you need to change a value multiple times without rendering, then batch them up for a unified refresh, the forceRender pattern comes in handy.
A More Practical Scenario
// High-frequency counter: updates value 10 times per second, but only renders every 5th time
const countRef = useRef(0)
const [, forceRender] = useState(0)
useEffect(() => {
const timer = setInterval(() => {
countRef.current += 1
if (countRef.current % 5 === 0) {
forceRender(countRef.current) // Saves 4/5 of the rendering overhead
}
}, 100)
return () => clearInterval(timer)
}, [])
Core Knowledge Point 4: useRef Holding a Worker Instance
Why Does JS Need Workers?
JS is single-threaded—all code runs on a single line. The page needs to respond to user clicks and scrolling while also executing computational tasks. If there's a time-consuming operation (large loops, AI inference, game logic), the page will freeze.
Web Worker is a solution provided by the browser: open another thread, throw the time-consuming task to it, and notify the main thread via messages upon completion.
Code (ref-worker-demo)
Main Thread App.jsx:
const App = () => {
const workerRef = useRef(null) // ① Persistently hold the Worker reference
useEffect(() => {
workerRef.current = new Worker(
new URL('./worker.js', import.meta.url) // ② Create Worker instance
)
}, [])
return <></>
}
Worker Thread worker.js:
console.log('work online')
Why Use useRef to Store the Worker?
| Using useState | Using useRef |
|---|---|
| Worker instance triggers a render ← Meaningless, unnecessary | Worker instance is only stored, not rendered ← Reasonable |
| Every setState notifies React to repaint ← Wasteful | Does not trigger a render, clean and tidy |
A Worker is a "thing that works in the background," unrelated to the UI, so it's naturally suited to be held by useRef.
Knowledge Points in the Commented Code
The project has a commented-out piece of code that blocks the main thread:
// console.time('Main thread time')
// for (let i = 0; i < 1000000; i++) {
// console.log(i)
// }
// console.timeEnd('Main thread time')
If this loop were executed, it would occupy the main thread completely, making buttons, scrolling, and input boxes on the page completely unresponsive—until the loop finishes. Computation in a Worker thread does not affect the main thread; this is the value of Workers.
Summary
Reviewing the core takeaways of this article:
useRefreturns a{ current }container, changingcurrentdoes not trigger a render—this is its essential difference fromuseState- Use
useRef+useEffectto manipulate the DOM:refgets the DOM node,useEffectensures execution after the DOM is mounted - Reactive vs non-reactive are not opposites, but complementary: 90% use
useStateto drive UI, 10% useuseReffor edge cases - forceRender pattern: The essence of the
useStatesetter is "notifying React to re-render," which can be paired withuseRefto achieve "decoupling value changes from rendering" useRefstores Worker instances: Things unrelated to the UI are best placed in ref
Next recommended learning: useRef paired with forwardRef to achieve DOM passthrough between parent and child components, and useImperativeHandle to customize instance methods exposed to parent components.
What do you use useRef for most often? Let's chat in the comments 👋
Complete Project Code
Project address: https://gitee.com/dcx2758/ai_doubao_dcx/tree/main/dcx/fe/react/basic/ref-demo
Project File Tree
ref-demo/
├── readme.md # Knowledge notes
├── ref-focus-demo/
│ ├── package.json # React 19.2.6 + Vite 8.0.12
│ ├── src/
│ │ ├── main.jsx # Entry point, StrictMode
│ │ ├── App.jsx # useRef DOM focus + forceRender
│ │ ├── App.css
│ │ └── index.css
│ └── vite.config.js
└── ref-worker-demo/
├── package.json # React 19.2.6 + Vite 8.0.12
├── src/
│ ├── main.jsx # Entry point, StrictMode
│ ├── App.jsx # useRef holding Worker instance
│ ├── worker.js # Worker thread code
│ ├── App.css
│ └── index.css
└── vite.config.js
Quick Start
# Clone the repository
git clone [email protected]:dcx2758/ai_doubao_dcx.git
cd ai_doubao_dcx/dcx/fe/react/basic/ref-demo
# Start ref-focus-demo (useRef focusing DOM + forceRender)
cd ref-focus-demo
npm install
npm run dev
# Start ref-worker-demo (useRef holding Worker)
cd ../ref-worker-demo
npm install
npm run dev