跪拜 Guibai
← Back to the summary

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 useState to drive the UI, 10% of edge cases use useRef. 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.

autoFocus doesn't need useEffect because 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:

  1. useRef returns a { current } container, changing current does not trigger a render—this is its essential difference from useState
  2. Use useRef + useEffect to manipulate the DOM: ref gets the DOM node, useEffect ensures execution after the DOM is mounted
  3. Reactive vs non-reactive are not opposites, but complementary: 90% use useState to drive UI, 10% use useRef for edge cases
  4. forceRender pattern: The essence of the useState setter is "notifying React to re-render," which can be paired with useRef to achieve "decoupling value changes from rendering"
  5. useRef stores 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