React 中 useRef 除了引用 DOM 还能做什么?
一句话总结:useRef 是一个「渲染间持久化的可变容器」,除了存 DOM 节点,还能存定时器 ID、上一次的值、跨渲染的标志位,是函数组件里不触发重渲染的「私有变量」!
正文目录
- useRef 到底是什么?
- 5 大高频使用场景 & 代码示例
- 万能兜底:什么时候用 useRef vs useState
- 预防 checklist(不再踩坑)
- 一句话总结
一、useRef 到底是什么?
const ref = useRef(initialValue); // ref.current = initialValue(初始值) // 修改 ref.current 不会触发重渲染 // 组件每次渲染,ref 是同一个对象核心特性:
| 特性 | useRef | useState |
|---|---|---|
| 修改后触发重渲染? | ❌ 不触发 | ✅ 触发 |
| 跨渲染保持值? | ✅ 是 | ✅ 是 |
| 修改方式 | ref.current = val | setState(val) |
| 适合存什么 | DOM、定时器、标志位 | 需要展示的 UI 数据 |
一句话:需要页面响应变化 → useState;只需要存个值但不影响 UI → useRef。
二、5 大高频使用场景 & 代码示例
① 引用 DOM 节点(最常见)
import { useRef, useEffect } from 'react'; function InputFocus() { const inputRef = useRef(null); useEffect(() => { inputRef.current?.focus(); // ✅ 挂载后自动聚焦 }, []); return <input ref={inputRef} />; }② 存储定时器 ID,避免泄漏
// ❌ 定时器存在普通变量里,每次渲染都会重置 function Timer() { let timerId = null; // ❌ 每次渲染都是 null useEffect(() => { timerId = setInterval(() => console.log('tick'), 1000); return () => clearInterval(timerId); // ❌ timerId 可能是 null }, []); }修复:用 useRef 存定时器
// ✅ useRef 存定时器 ID,跨渲染稳定 import { useRef, useEffect } from 'react'; function Timer() { const timerRef = useRef(null); useEffect(() => { timerRef.current = setInterval(() => { console.log('tick'); }, 1000); return () => clearInterval(timerRef.current); // ✅ 清理时能拿到 }, []); const stop = () => clearInterval(timerRef.current); return <button onClick={stop}>停止</button>; }③ 记录上一次的值(prevValue)
// ✅ 用 useRef 记录上一次的 props/state import { useRef, useEffect } from 'react'; function PriceDisplay({ price }) { const prevPriceRef = useRef(price); useEffect(() => { prevPriceRef.current = price; // ✅ 渲染后更新 }); const isUp = price > prevPriceRef.current; return ( <div style={{ color: isUp ? 'red' : 'green' }}> ¥{price} {isUp ? '↑' : '↓'} </div> ); }④ 防止 useEffect 里的闭包陷阱
// ❌ useEffect 空依赖,但里面用了最新的 state → 闭包旧值 function Counter() { const [count, setCount] = useState(0); useEffect(() => { const timer = setInterval(() => { console.log(count); // ❌ 永远是 0 setCount(count + 1); // ❌ 永远设成 1 }, 1000); return () => clearInterval(timer); }, []); }修复方案一:useRef 存最新值
// ✅ useRef 存最新 count,定时器里读 ref.current function Counter() { const [count, setCount] = useState(0); const countRef = useRef(count); useEffect(() => { countRef.current = count; // ✅ 每次渲染更新 ref }); useEffect(() => { const timer = setInterval(() => { setCount(countRef.current + 1); // ✅ 读最新值 }, 1000); return () => clearInterval(timer); }, []); return <div>{count}</div>; }修复方案二:函数式更新(更简洁)
// ✅ 函数式更新,不需要 ref function Counter() { const [count, setCount] = useState(0); useEffect(() => { const timer = setInterval(() => { setCount(prev => prev + 1); // ✅ 拿到最新值 }, 1000); return () => clearInterval(timer); }, []); return <div>{count}</div>; }⑤ 标志位:控制「只执行一次」或「是否已初始化」
// ✅ 用 useRef 做初始化标志,防止重复执行 import { useRef, useEffect } from 'react'; function useInitOnce(callback) { const initializedRef = useRef(false); useEffect(() => { if (initializedRef.current) return; // ✅ 只执行一次 initializedRef.current = true; callback(); }, [callback]); }// ✅ 防抖:用 useRef 存防抖定时器 function SearchInput() { const [value, setValue] = useState(''); const debounceRef = useRef(null); const handleChange = (e) => { const val = e.target.value; setValue(val); clearTimeout(debounceRef.current); // ✅ 清掉上一个定时器 debounceRef.current = setTimeout(() => { fetchResults(val); // ✅ 500ms 后发请求 }, 500); }; useEffect(() => { return () => clearTimeout(debounceRef.current); // ✅ 卸载时清理 }, []); return <input value={value} onChange={handleChange} />; }三、万能兜底:什么时候用 useRef vs useState
决策流程:
这个值变化后,UI 需要更新吗? ├── 是 → useState └── 否 → 这个值需要在事件/定时器里访问吗? ├── 是 → useRef └── 否 → 普通变量就行| 场景 | 用什么 | 原因 |
|---|---|---|
| 输入框内容 | useState | UI 需要响应 |
| 加载状态 | useState | UI 需要响应 |
| DOM 节点引用 | useRef | 不需要触发渲染 |
| 定时器 ID | useRef | 不需要触发渲染 |
| 上一次的值 | useRef | 用于比较,不直接渲染 |
| 防抖定时器 | useRef | 不需要触发渲染 |
| 初始化标志 | useRef | 只控制逻辑,不渲染 |
| isMounted 标志 | useRef | 防止卸载后 setState |
万能模板:
import { useState, useRef, useEffect } from 'react'; function useDebounce(callback, delay) { const timerRef = useRef(null); // ✅ 存定时器 const callbackRef = useRef(callback); // ✅ 存最新 callback useEffect(() => { callbackRef.current = callback; // ✅ 每次渲染更新 }); useEffect(() => { return () => clearTimeout(timerRef.current); // ✅ 卸载清理 }, []); const debounced = (...args) => { clearTimeout(timerRef.current); timerRef.current = setTimeout(() => { callbackRef.current(...args); // ✅ 调最新的 }, delay); }; return debounced; }四、预防 checklist
- 需要 UI 响应 → useState,不需要 → useRef
- 定时器、事件监听器的 ID用 useRef 存,useEffect cleanup 里清理
- 防抖/节流用 useRef 存定时器,别用普通变量(每次渲染重置)
- 闭包陷阱优先用函数式更新解决,实在不行再上 useRef
- 记录「上一次的值」用 useRef + useEffect,在渲染后更新
- 不要在渲染过程中写 ref.current(除初始化),只在事件回调和 effect 里写
- ref.current 读写在事件处理和 effect 里是安全的,不要在组件渲染阶段依赖它
- useRef 初始值
useRef(null)给 DOM,useRef(0)给计数器,useRef(false)给标志位
五、一句话总结
「useRef = 不触发重渲染的持久化容器」。DOM 引用只是最基础的用法,定时器 ID、上一次的值、初始化标志、防抖定时器都是它的主场。记住:UI 要变用 useState,逻辑要存不变用 useRef!
最后问候亲爱的朋友们,并邀请你们阅读我的全新著作
📚 《React 进阶实战》