useRef
React useRef 入门教程#
一、useRef 是什么?#
useRef 是 React 提供的核心 Hook 之一,它返回一个可变的 ref 对象,该对象在组件的整个生命周期内保持不变 。这个对象只有一个 current 属性,你可以读写它的值,且不会触发组件重新渲染。
一句话定义:useRef 是 React 组件的”内部小仓库”——存数据、不刷新页面 。
二、useRef 与 useState 的核心区别#
这是初学者最容易混淆的地方 :
| 特性 | useState | useRef |
|---|---|---|
| 更新是否触发渲染 | ✅ 是 | ❌ 否 |
| 数据生命周期 | 组件整个生命周期 | 组件整个生命周期 |
| 访问方式 | 直接 count | 通过 .current |
| 更新方式 | setCount(newValue)(异步) | ref.current = newValue(同步) |
| 能否操作 DOM | ❌ 不能 | ✅ 能 |
| 适用场景 | 需要驱动 UI 更新的状态 | 不需要 UI 反映的内部数据 |
本质区别 :
useState是告诉 React “UI 要变了”;useRef是说”我需要记住一个东西,但 UI 不用变”。
三、三大核心使用场景#
场景一:访问 DOM 元素(最经典用法)#
这是 useRef 最广为人知的用法——获取 DOM 节点并操作它 。
import React, { useRef } from 'react';
function InputFocus() {
const inputRef = useRef(null);
const handleClick = () => {
// 直接操作 DOM,聚焦输入框
inputRef.current.focus();
};
return (
<div>
<input ref={inputRef} type="text" placeholder="点击按钮聚焦我" />
<button onClick={handleClick}>聚焦输入框</button>
</div>
);
}jsx常见 DOM 操作:
inputRef.current.focus(); // 聚焦
inputRef.current.blur(); // 失焦
inputRef.current.value; // 获取输入值(非受控组件)
inputRef.current.scrollIntoView(); // 滚动到视野javascript场景二:保存不触发渲染的值(性能优化利器)#
这是 useRef 被严重低估的用法。很多开发者只知道它能取 DOM,却不知道它能存任何数据 。
import React, { useRef, useState } from 'react';
function ClickCounter() {
const [displayCount, setDisplayCount] = useState(0);
const realCountRef = useRef(0); // 实际点击次数(不显示)
const handleClick = () => {
realCountRef.current += 1; // 同步更新,不触发渲染
console.log('实际点击次数:', realCountRef.current);
// 每点击 5 次才更新一次 UI
if (realCountRef.current % 5 === 0) {
setDisplayCount(realCountRef.current);
}
};
return (
<div>
<p>页面显示: {displayCount}</p>
<p>实际已点击: {realCountRef.current}(不会实时更新)</p>
<button onClick={handleClick}>点击我</button>
</div>
);
}jsx关键点:点击按钮时 realCountRef.current 已经变了,但页面上显示的值不会更新。只有调用 setDisplayCount 时页面才会刷新 。
场景三:解决闭包陷阱(保存最新值)#
在异步操作中,useState 的值可能不是最新的,而 useRef 总是最新的 。
function SearchBox() {
const [query, setQuery] = useState('');
const queryRef = useRef('');
const handleChange = (value) => {
setQuery(value);
queryRef.current = value; // 保持同步
};
const handleSearch = () => {
// 模拟异步请求
setTimeout(() => {
console.log('state 值:', query); // 可能是旧值!
console.log('ref 值:', queryRef.current); // 永远是最新值 ✅
api.search(queryRef.current);
}, 500);
};
return (
<div>
<input
value={query}
onChange={(e) => handleChange(e.target.value)}
/>
<button onClick={handleSearch}>搜索</button>
</div>
);
}jsx为什么需要这个? 如果用户在 500ms 内修改了输入内容,query 状态可能还没更新,但 queryRef.current 已经是最新的了 。
四、实战案例:高性能搜索框#
以下是一个用 useRef 优化后的搜索组件,对比”全 state”方案性能提升 13.9 倍 :
import React, { useRef, useState, useCallback } from 'react';
function SearchBox() {
// 只保存需要改变 UI 的状态
const [query, setQuery] = useState('');
const [suggestions, setSuggestions] = useState([]);
// 所有"内部管理"的数据都用 useRef
const lastQueryRef = useRef('');
const cacheRef = useRef(new Map());
const abortControllerRef = useRef(null);
const debounceTimerRef = useRef(null);
const handleInput = useCallback((value) => {
setQuery(value); // 只有这一个 setState 触发渲染
// 防抖:清除前一个定时器
if (debounceTimerRef.current) {
clearTimeout(debounceTimerRef.current);
}
debounceTimerRef.current = setTimeout(() => {
performSearch(value);
}, 300);
}, []);
const performSearch = async (value) => {
// 去重检查
if (lastQueryRef.current === value) return;
lastQueryRef.current = value;
// 取消前一个请求
if (abortControllerRef.current) {
abortControllerRef.current.abort();
}
// 缓存检查
if (cacheRef.current.has(value)) {
setSuggestions(cacheRef.current.get(value));
return;
}
// 发起新请求
const controller = new AbortController();
abortControllerRef.current = controller;
try {
const data = await fetchSuggestions(value, controller.signal);
cacheRef.current.set(value, data); // 缓存结果,零渲染成本
setSuggestions(data);
} catch (err) {
if (err.name !== 'AbortError') {
console.error('搜索失败', err);
}
}
};
return (
<div>
<input
value={query}
onChange={(e) => handleInput(e.target.value)}
placeholder="搜索内容..."
/>
<div className="suggestions">
{suggestions.map(s => (
<div key={s.id}>{s.text}</div>
))}
</div>
</div>
);
}jsx性能对比 :
| 指标 | 全 useState 方案 | useRef 优化方案 |
|---|---|---|
| 总重渲染次数 | 27 次 | 1 次 |
| 平均单次渲染耗时 | 145ms | 8ms |
| 总耗时 | 3.9 秒 | 280ms |
| 用户体验 | 可感知卡顿 ⚠️ | 完全流畅 ✅ |
五、常见误区与注意事项#
误区 1:用 useRef 存需要显示的数据#
// ❌ 错误:想显示最新值,但页面不会更新
function WrongDemo() {
const countRef = useRef(0);
return (
<div>
<p>{countRef.current}</p> {/* 点击后这里不会变! */}
<button onClick={() => countRef.current++}>+1</button>
</div>
);
}jsx原因:修改 ref.current 不会触发 React 的重新渲染机制 。
误区 2:把 ref.current 作为依赖项#
// ❌ 错误:useEffect 不会监听 ref 变化
useEffect(() => {
console.log(countRef.current);
}, [countRef.current]); // 这不会生效!jsx正确做法:如果需要监听 ref 变化,应改用 useState 或结合其他机制 。
误区 3:用 useState 存不需要 UI 的数据#
// ❌ 错误:定时器 ID 不需要触发渲染
const [timerId, setTimerId] = useState(null);
// ✅ 正确:用 useRef 存储
const timerIdRef = useRef(null);jsx六、何时用 useState,何时用 useRef?#
一个清晰的决策树 :
┌─────────────────────────────────────────────┐
│ 这个值的变化会影响 UI 显示吗? │
└────────────┬────────────────────────────────┘
│
┌───────┴────────┐
│ │
是 否
│ │
↓ ↓
用 useState 用 useRefplaintext实际案例判断 :
| 数据 | 该用哪个 | 原因 |
|---|---|---|
| 用户在输入框输入的词 | useState | 需要显示在 input 中 |
| 上一次搜索的词(去重用) | useRef | 只是逻辑判断,不显示 |
| 输入框是否 focused | useState | 需要改变 UI 样式 |
| 防抖定时器 ID | useRef | 只用来清理定时器 |
| 表单中所有字段的值 | useState | 需要实时显示 |
| 表单提交前的验证缓存 | useRef | 只用于逻辑处理 |
| 模态框是否打开 | useState | 需要显示/隐藏 UI |
| 模态框的 DOM ref(控制焦点) | useRef | 不影响显示 |
七、useRef 存储的数据类型#
useRef 可以存储任何类型的数据,远不止 DOM 元素 :
function App() {
const domRef = useRef(null); // DOM 元素
const coordsRef = useRef({ x: -1, y: -1 }); // 对象
const callbackRef = useRef(() => {}); // 函数
const cacheRef = useRef(new Map()); // Map 实例
const wsRef = useRef(null); // WebSocket 实例
const prevPropsRef = useRef(props); // 上一次的 props
// ...
}jsx特点 :
- 可以存储任何类型的数据
- 数据在组件整个生命周期内有效,组件销毁后自动清理
- 修改不会触发重新渲染
- 修改后立即生效(同步)
- 通过
.current读写,没有额外的 get/set 方法
八、总结#
| 要点 | 说明 |
|---|---|
| 核心作用 | 在多次渲染之间持久保存数据,不触发重新渲染 |
| 返回值 | { current: initialValue } 对象 |
| 更新方式 | ref.current = newValue(直接赋值) |
| DOM 操作 | ref={domRef} 绑定后通过 domRef.current 访问 |
| 性能价值 | 将非 UI 数据从 state 中分离,减少不必要的重渲染 |
| 闭包救星 | 在异步回调中始终能获取到最新值 |
一句话记住 useRef :
useState是页面数据管理员(驱动 UI),useRef是页面内部小仓库(不驱动 UI)。
合理使用 useRef,能让你的 React 应用性能提升数倍,同时代码逻辑更加清晰。