useRef 是 另外一个用于声明变量的内置 Hook,与 useState 不同的是,useRef 声明的变量不会触发组件重新渲染
它的基础语法如下
01import { useRef } from 'react'0203function MyComponent() {04// 声明一个 ref 变量05const ref = useRef(0)0607function clickHandler() {08// 通过 ref.current 访问变量的最新值09ref.current += 110}1112...13}
useRef() 接收一个参数, 这个参数是初始值, 可以是任何值, 也可以是函数. 其执行之后,返回一个仅包含一个 .current 属性的对象
1const ref = useRef(0)23console.log(ref.current) // 0
如果你是在 ts 中使用,通常我们无需额外为 useRef 声明的变量,添加类型声明,ts 会根据你的默认值自动推导类型,当然,你也可以通过泛型来显式声明类型
1const ref = useRef<number>(0)23console.log(ref.current) // 0
通过 useRef 声明的变量值发生变化时,不会触发组件重新渲染
而通过 useState 声明的变量值发生变化时,会触发组件重新渲染
我们通过如下的案例,来演示两者的区别
观察变量如何在不触发 UI 渲染的情况下悄悄改变
01import React, { useState, useRef } from 'react';0203const RefDemo = () => {04// 用于触发 UI 渲染的状态05const [renderCount, setRenderCount] = useState(0);0607// useRef 存储的变量:改变它不会导致 UI 更新08const clickTracker = useRef(0);0910const handleRefClick = () => {11clickTracker.current += 1;12console.log('Ref 值已改变,但 UI 没动:', clickTracker.current);13};1415const forceRender = () => {16setRenderCount(prev => prev + 1);17};1819return (20<div className="bg-slate-900 flex items-center justify-center p-6">21<div className="max-w-md w-full bg-slate-800 border border-slate-700 p-8 shadow-2xl">2223{/* 标题区域 */}24<div className="mb-8">25<h2 className="text-2xl font-bold text-white mb-2">useRef 与 useState 的区别</h2>26<p className="text-slate-400! text-sm">观察变量如何在不触发 UI 渲染的情况下悄悄改变</p>27</div>2829{/* 核心数据展示 */}30<div className="grid grid-cols-2 gap-4 mb-8">31<div className="bg-slate-900/50 p-4 border border-slate-700/50">32<span className="text-slate-500 text-xs uppercase tracking-wider font-semibold">UI 渲染次数</span>33<div className="text-3xl font-mono font-bold text-cyan-400">{renderCount}</div>34</div>35<div className="bg-slate-900/50 p-4 border border-slate-700/50">36<span className="text-slate-500 text-xs uppercase tracking-wider font-semibold">Ref 存储值</span>37<div className="text-3xl font-mono font-bold text-emerald-400">{clickTracker.current}</div>38</div>39</div>4041{/* 操作按钮 */}42<div className="space-y-4">43<button44onClick={handleRefClick}45className="w-full py-4 bg-emerald-500/10 hover:bg-emerald-500/20 border border-emerald-500/50 text-emerald-400 font-medium transition-all active:scale-[0.98] flex items-center justify-center gap-2"46>47<span className="w-2 h-2 bg-emerald-400 rounded-full animate-pulse"></span>48秘密增加 Ref (静默)49</button>5051<button52onClick={forceRender}53className="w-full py-4 bg-cyan-500 hover:bg-cyan-600 text-white font-medium shadow-lg shadow-cyan-500/20 transition-all active:scale-[0.98]"54>55触发重新渲染 (同步 UI)56</button>57</div>5859<div className="mt-8 pt-6 border-t border-slate-700">60<ul className="space-y-2 italic">61<li className="text-slate-400! text-xs">点击第一个按钮:Ref 在控制台增加,但右侧数字不变。</li>62<li className="text-slate-400! text-xs">点击第二个按钮:组件刷新,Ref 的最新值被“揭晓”。</li>63<li className="text-slate-400! text-xs">结论:useRef 是完美的“幕后英雄”,适合存储不需要立即反馈到界面的数据。</li>64</ul>65</div>66</div>67</div>68);69};7071export default RefDemo;
从另一个角度的上来说,当组件由于 state 值的变化而重新渲染时,useRef 声明的变量值不会发生变化.
useRef 通过 .current 修改值时,不会触发组件重新渲染useRef 声明的变量值不会随着组件的重新渲染而被重置基于这个特性,我们可以发掘出来一些非常有意思的用法
DOM 封装的元素,例如 input、textarea、select 等,我们可以通过他们的属性 ref 来获取其真实 DOM 的引用
通常的做法是,我们会将该引用保存到 useRef 中,因为真实 DOM 一旦创建,就会一直存在,其引用需要稳定性
如下案例所示,我们可以通过点击按钮,来获取输入框的焦点
点击卡片任意位置或按钮,
底层使用 useRef.current.focus() 实现
01import { useRef } from 'react';02import { Focus } from 'lucide-react'0304export default function MagicFocusDemo() {05const inputElement = useRef<HTMLInputElement>(null);0607const handleActivate = () => {08if (inputElement.current) {09inputElement.current.focus();10}11};1213return (14<div className="flex items-center justify-center p-4">15<div className="max-w-md w-full">16{/* 卡片容器 */}17<div18onClick={handleActivate}19className="group relative bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 p-8 cursor-pointer"20>21<div className="relative z-10">22<h3 className="text-sm font-semibold text-indigo-600 dark:text-indigo-400 mb-2 tracking-widest uppercase">Quick Action</h3>23<h2 className="text-3xl font-bold text-neutral-800 dark:text-neutral-200 mb-6 tracking-tight">24准备好开始<br />你的学习了吗?25</h2>2627<div className="relative">28<input29ref={inputElement}30type="text"31placeholder="输入项目名称..."32className="w-full bg-neutral-100 dark:bg-neutral-800 border-none focus:ring-2 focus:ring-indigo-500/20 px-5 py-4 text-neutral-700 dark:text-neutral-300 placeholder:text-neutral-400 dark:placeholder:text-neutral-600 transition-all duration-300 outline-none"33/>3435{/* 装饰性搜索图标 */}36<div className="absolute right-4 top-1/2 -translate-y-1/2 text-neutral-400 dark:text-neutral-600 group-hover:text-indigo-500 transition-colors">37<Focus size={18} />38</div>39</div>4041<button42onClick={handleActivate}43className="mt-6 w-full bg-neutral-900 dark:bg-neutral-800 hover:bg-indigo-600 dark:hover:bg-indigo-800 text-white dark:text-neutral-200 font-medium py-4 transition-all duration-300 transform active:scale-[0.97] flex items-center justify-center"44>45立即激活输入框46</button>47</div>48</div>4950{/* 教学引导 */}51<p className="mt-8 text-center text-neutral-400 text-sm font-light">52点击卡片任意位置或按钮,<br />53底层使用 <code className="bg-neutral-200 px-1 rounded text-neutral-700">useRef.current.focus()</code> 实现54</p>55</div>56</div>57);58}
初学者最大的误区,就是不假思索的滥用 useState,把所有的变量都定义为状态值. 这种做法会导致组件的重新渲染过于频繁,从而影响性能.
合理的做法是在定义变量时,依据是否会导致组件重新渲染,来决定使用 useState 还是 useRef.
除此之外,我们还可以使用 useRef 来记录定时器的引用,大家可以自行尝试,在后续的课程中,我们还会继续学习 ref 与 useImperativeHandle 的结合使用方法