Hook
约 6524 字大约 22 分钟
2026-01-30
State
useState()
与在 Vue 中一样,在模板中使用普通变量,当变量改变时不会触发渲染。
在 React 中,我们可以使用 useState Hook 来创建一个响应式变量,它接收一个默认值(可以是一个有返回值的箭头函数),并返回一个数组。数组的第一个元素是变量的值,第二个元素是更新变量的函数。 第二个更改变量的函数我们约定使用 set + 变量名来命名如 setIndex,更改 index 时需要使用 setIndex 函数来更新变量。
import { useState } from "react";
export default function App() {
const [index, setIndex] = useState(0);
function handleAdd() {
setIndex(index + 1);
}
return (
<>
<button onClick={handleAdd}>{index}</button>
</>
);
}类组件中如何更改 state ?
在类组件中 state 同样不能直接更改,不过是通过 setState 来更新。 注意:setState 是异步对比累加赋值,更改对象某一个属性,只需传入该属性,不需要传入所有属性。而 useState() 则是覆盖更新,需要传入所有属性。 可以阅读 state 陷阱。
import React, { Component } from "react";
export default class App extends Component {
constructor(props) {
super(props);
this.state = {
index: 0,
};
}
handleAdd = () => {
this.setState({ index: this.state.index + 1 });
};
render() {
return (
<>
<button onClick={this.handleAdd}>{this.state.index}</button>
</>
);
}
}state 快照
React 中的 state 本质就是 组件某一时刻状态的“快照”,它定格了当前数据,且不能直接修改,只能通过生成新“快照”来触发组件更新。
“快照”的核心是“定格某一时刻的状态”,这一点在 React state 上体现为两个关键规则:
state 不可直接修改(快照不能涂改)
生成的 state 快照就像拍好的照片,不能直接在原照片上涂改内容。比如你有一个count状态,直接写this.state.count = 1(类组件)或count = 1(函数组件)完全无效,React 不会感知到变化,也不会重新渲染组件。更新 state = 生成新快照
要修改状态,必须通过 React 提供的“重拍快照”方法:类组件用this.setState(),函数组件用setState(useState 返回的修改函数)。调用这些方法时,React 会基于当前快照生成新的 state 快照,再用新快照重新渲染组件。
state 的快照特性,最明显的体现是 setState 的“异步更新” —— 调用 setState 后,当前作用域内的 state 还是旧快照,新快照要等下一次渲染才生效。
function Counter() {
const [count, setCount] = useState(0);
const handleClick = () => {
// 此时的count是“当前渲染的快照”,值为0
setCount(count + 1);
// 调用setState后,当前作用域的count依然是旧快照(0),不是1!
console.log(count); // 输出:0(而非预期的1)
};
// 组件重新渲染时,会使用setCount生成的“新快照”(count=1)
return <button onClick={handleClick}>点击次数:{count}</button>;
}- 点击按钮时,
setCount只是“提交了生成新快照的请求”,但没有立即替换当前的count(旧快照)。 console.log(count)读取的还是“当前渲染周期的旧快照”,所以输出 0;- 只有等 React 处理完更新,进入下一次渲染时,才会用新快照(count=1)更新组件,页面上的数字才会变成 1。
React 的渲染逻辑完全基于 快照对比,流程如下:
- 组件首次渲染时,React 生成 初始 state 快照 ,并基于这个快照渲染 DOM。
- 当调用
setState(或useState的修改函数)时,React 会根据传入的新值生成 新的 state 快照。 - React 对比“旧快照”和“新快照”:如果数据有变化,就基于新快照重新渲染组件;如果没变化,就跳过渲染(性能优化)。
- 重新渲染后,组件内读取的 state 都会变成新快照的值,直到下一次更新。
state 陷阱
- 永远不要认为调用
setState后,下一行代码就能拿到新 state,因为当前快照还没切换。
如上面的例子中,在同一个作用域中调用多次 setState,每次拿到的并不是新的快照,新快照要等下一次渲染才生效。 解决这种数据异步更新可以对 setState 传入回调解决:
setCount((pre) => pre + 1);- ==如果你的 state 变量是一个对象时,不能只更新其中的一个字段而不显式复制其他字段。
与类组件中的 setState 不一样,setXXX 是异步直接赋值,更改对象字段时,需要显式复制其他字段。
比如 const [position, setPosition] = useState({ x: 0, y: 0 }); ,若只更新其中一个字段需要传入整个对象字段,可以使用 ... 扩展运算符, 这样你就不需要单独复制每个属性。
setUser({
...position, // 使用扩展运算符传入整个对象
y: 1,
});数组同理:
const [user, setUser] = useState([]);
setUser([...user, { id: 100, name: "React" }]);也可以先拷贝一份原始值,更改拷贝的对象再进行更改。
const temp = [...position];
position.y = 1;
setPosition(temp);共享状态
有时候,你希望两个组件的状态始终同步更改。要实现这一点,可以将相关 state 从这两个组件上移除,并把 state 放到它们的公共父级,再通过 props 将 state 传递给这两个组件。这被称为“状态提升”,这是编写 React 代码时常做的事。
简而言之,将 state 放在父组件,使用 props 传递给子组件,子组件将会共享父组件的 state。
import { useState } from "react";
export default function Accordion() {
const [activeIndex, setActiveIndex] = useState(0);
return (
<>
<h2>哈萨克斯坦,阿拉木图</h2>
<Panel title="关于" isActive={activeIndex === 0} onShow={() => setActiveIndex(0)}>
阿拉木图人口约200万,是哈萨克斯坦最大的城市。它在 1929 年到 1997 年间都是首都。
</Panel>
<Panel title="词源" isActive={activeIndex === 1} onShow={() => setActiveIndex(1)}>
这个名字来自于 <span lang="kk-KZ">алма</span>
,哈萨克语中“苹果”的意思,经常被翻译成“苹果之乡”。事实上,阿拉木图的周边地区被认为是苹果的发源地,
<i lang="la">Malus sieversii</i> 被认为是现今苹果的祖先。
</Panel>
</>
);
}
function Panel({ title, children, isActive, onShow }) {
return (
<section className="panel">
<h3>{title}</h3>
{isActive ? <p>{children}</p> : <button onClick={onShow}>显示</button>}
</section>
);
}保留和重置
各个组件的 state 是相互隔离的,但状态并不存在组件内,而是由 React 来保存状态。那 React 如何知道哪个 state 属于哪个组件呢?
React 会为 UI 中的组件结构构建渲染树,通过组件在渲染树中的位置将它保存的每个状态与正确的组件关联起来。
也就是说 state 是否是保留还是重置,与组件被渲染在 UI 树的位置有关。
export default function App() {
return (
<div>
<Counter />
<Counter />
</div>
);
}上面的两个 <Counter /> 组件是在父 div 下的两个不同子节点,因此这两个组件的 state 相互独立,互不影响。
如果一个组件总是被渲染在 UI 树中的同一位置,那么它的 state 就会被保留。 如果它被移除,或者其他组件被渲染到这个位置,那么它的 state 就会被重置。
import { useState } from "react";
export default function App() {
const [dark, setDark] = useState(false);
return (
<div>
{" "}
<Counter isDark={dark} />
<button onClick={() => setDark(!dark)}>切换 {dark ? "浅色" : "深色"} 模式</button> // [!code
focus]
</div>
);
}
function Counter({ isDark }) {
const [count, setCount] = useState(0);
return (
<div
style={{
padding: "10px",
color: isDark ? "white" : "black",
backgroundColor: isDark ? "#333" : "#fff",
border: "1px solid #ccc",
}}
>
点击次数: {count}
<button onClick={() => setCount((c) => c + 1)}>+1</button>
</div>
);
}上面的例子中 ,点击切换按钮,更改了 <Counter /> 组件的 isDark 属性后样式随着发生变化,但它在父组件的位置没有改变,也就是说在 UI 树中的位置没有改变, React 认为它是同一个组件,所有 <Counter /> 的 state 会被保留。
从以上的例子我们知道了,状态与渲染树中的位置有关。相同位置的相同组件状态会保留,相同位置的不同组件状态会重置。
如果我们要使用相同组件进行条件渲染,但是不想要它们的状态被保留,而是不同的组件切换时是有新的状态应该怎么做?
- 将组件渲染在不同位置:
return (
<div>
{/* 在相同位置,state 保留 */}
{isPlayerA ? (
<Counter person="Taylor" />
) : (
<Counter person="Sarah" />
)}{" "}
{/* 在不同位置,state 重置 */}
{
isPlayerA && <Counter person="Taylor" /> // [!code ++]
}{" "}
{
!isPlayerA && <Counter person="Sarah" /> // [!code ++]
}{" "}
</div>
);- 使用 key
return (
<div>
{/* 在相同位置,state 保留 */}
{isPlayerA ? (
<Counter person="Taylor" />
) : (
<Counter person="Sarah" />
)}{" "}
{/* 使用 key,state 重置 */}
{isPlayerA ? (
<Counter key="Taylor" person="Taylor" />
) : (
<Counter key="Sarah" person="Sarah" />
)}{" "}
</div>
);在组件上使用 key 可以告诉 React 这些是不同的组件,在组件切换时 state 也因此不会被保留。
重新渲染
我们知道了,当使用 setXXX 更新状态时,React 会重新渲染组件。如果我们传入的值与当前值完全一样还会引发重新渲染组件吗?
setXXX 函数会检查新值是否与当前值相同,如果相同则不会触发重新渲染。这里有两种情况:
- 值是基本类型:如果传入的值与当前值数值与类型完全相同,则不会重新渲染。
let str = "1"; Object.is(str, "1"); //true Object.is(str, 1); //false - 值是对象:如果传入的对象与当前对象只是看上去相同(数值与类型相同),也会重新渲染。除非两者引用相同。
let obj = { a: 1 }; Object.is(obj, { a: 1 }); //false let a = obj; let b = obj; Object.is(a, b); //true
Effect
useEffect
useEffect 合并了生命周期函数 componentDidMount(组件被挂载完成后)、componentDidUpdate(组件重新渲染完成后)、componentWillUnmount(组件即将被卸载前), 用于当依赖变化时触发一些事件。
1. 语法
useEffect(() => {
//此处为组件挂载之后和组件重新渲染之后执行的代码
...
return () => {
//此处为组件即将被卸载前执行的代码
...
}
},[deps])2. 三种行为
useEffect 在初次渲染时都会执行一次,如果是在严格模式下初次渲染则会执行两次。
| 写法 | 行为 |
|---|---|
useEffect(() => { ... }, []) | 只在挂载时执行一次 |
useEffect(() => { ... }, [a, b]) | 当 a 或 b 变化时执行 |
useEffect(() => { ... }) | 每次渲染后都执行 |
提示
const [a, setA] = useState(0);
useEffect(() => {
let timer = setInterval(() => {
setA(a + 1);
}, 1000);
return () => {
clearInterval(timer);
};
}, []);如果 useEffect 函数第2个参数为空数组,那么 react 会将该 useEffect 的第1个参数建立一个闭包,该闭包里的变量 a 被永远设定为当初的值,即 0。 尽管setInterval正常工作,每次都“正常执行了”,可是 setA(a+1)中 a 的值一直没变化,一直都是当初的0, 所以造成 0 + 1 一直都等于 1 的结果。此时应该使用解决数据异步更新的方法:setA(a => a + 1)。
注意
useEffect 应该只用于副作用,而不是用于驱动应用逻辑流。如果 useEffect 只是在同步 React 内部状态,那么很可能有更好的方式。参考 你可能不需要 Effect。
清理函数
React 中的清理函数与 Vue 中清理函数 设计目的与运行时机很相似:为了释放资源,解决竞态问题;在副作用执行之前执行上一次的清理函数,在组件卸载时执行最后一次清理函数。
在开发环境中,React 会在组件首次挂载后立即重新挂载一次,所以中间会额外执行一次清理函数。 之所以会额外的执行一次清理函数,是因为在开发环境下 React 会对逻辑进行压力测试,检测代码中的 bug,帮助找到需要清理的 Effect。
1. 数据获取(Data Fetching)
如果 Effect 需要获取数据,清理函数应 中止请求 或忽略其结果:
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
let cancelled = false; // 防止竞态(race condition)
fetchUser(userId).then((data) => {
if (!cancelled) setUser(data);
});
return () => {
cancelled = true; // 组件卸载时取消
};
}, [userId]); // userId 变化时重新获取
return <div>{user?.name}</div>;
}2. 事件监听
如果在 Effect 订阅了某些监听事件,清理函数应退订这些事件:
function WindowSize() {
const [size, setSize] = useState({ width: 0, height: 0 });
useEffect(() => {
const handleResize = () => {
setSize({ width: window.innerWidth, height: window.innerHeight });
};
window.addEventListener("resize", handleResize);
handleResize(); // 初始化
return () => window.removeEventListener("resize", handleResize);
}, []);
return (
<p>
Window: {size.width} x {size.height}
</p>
);
}3. 触发动画
如果在 Effect 触发了一些动画,清理函数应将动画重置为初始状态:
useEffect(() => {
const node = ref.current;
node.style.opacity = 1; // 触发动画
return () => {
node.style.opacity = 0; // 重置为初始值
};
}, []);缓存值
useMemo
useMemo 能够在每次重新渲染的时候缓存计算的值。
useMemo 函数接收一个计算函数和一个依赖项数组作为参数,返回一个缓存值。
const xxxValue =useMemo(() =>{
return ...;
},
[...]
)计算函数是没有参数的纯函数,返回值可以是任意类型。当依赖项改变时,useMemo 会重新执行计算函数并返回结果。 下次渲染时如果依赖项不变,则返回上次缓存的结果。
依赖项是一个数组,凡是计算函数中有的数据变量都应该放在依赖数组中。
使用场景
useMemo 缓存计算结果应该仅仅作为性能优化的手段,而不是随处使用。当我们需要根据一个或者一些数据通过计算获取新的数据, 如果计算速度很快,可以不使用 useMemo,若计算速度较慢才考虑使用 useMemo 来提到组件的性能。 就应该考虑使用 useMemo。
下面是一个 useMemo 如何提高组件性能的例子:
function App() {
const [year, setYear] = useState(2000);
const [month, setMonth] = useState(0);
const formatYear = () => {
console.log("Change year");
return year + "年";
};
const calculatedYear = formatYear();
const calculatedMonth = useMemo(() => {
console.log("Change month");
return (month % 12) + 1 + "月";
}, [month]);
return (
<>
<div>
<p>{calculatedYear + calculatedMonth}</p>
<button onClick={() => setYear(year + 1)}>Change year</button>
<button onClick={() => setMonth(month + 1)}>Change month</button>
</div>
</>
);
}
export default App;上面的例子中,我们分别对 year 和 month 两个变量进行重新计算(格式化添加单位),其中 month 的计算使用了 useMemo。 当我们点击 Change year ,可以通过控制台看到打印了 Change year ,而当我们点击 Change month 时,打印了 Change month 和 Change year, 这是由于组件在渲染时会重新执行函数体内的所有代码,所以在更改 month 时 year 也会重新计算。而 month 的计算使用了 useMemo,当 year 改变时其依赖并没有 改变所以使用上次缓存的 month ,只有当其依赖项改变时才会重新计算。
上面的例子中两个计算速度都很快,试想一下如果 year 的计算很慢,那么每次重新渲染都会计算 yeat,这种高额的计算会拖慢组件的性能。因此 useMemo 的作用简而言之就是: 跳过代价昂贵的重新计算,提高组件性能。
reducer
useReducer
假如我们有一个复杂的数组对象,要对其进行添加、删除、修改、查询等操作,并返回新的数组对象。 如果使用 useState,我们就需要定义多个事件处理函数,并使用多个 setState 来更新状态,逻辑将会越来越复杂。 并且,当我们需要在其他方也对这个复杂的数组对象进行相同操作时,我们就又不得不写很多相同的代码,状态逻辑也越来越混乱。 这时候就可以使用 useReducer 来解决这个问题。
在 React 中,reducer 是一种用于 集中管理复杂状态逻辑 的模式,接收当前状态(state)和一个动作(action),返回新的状态。
React 通过内置 Hook useReducer 提供了对 reducer 模式的原生支持,特别适用于以下场景:
- 状态逻辑较复杂(如包含多个子值)
- 下一个状态依赖于前一个状态
- 需要在多个组件间共享或复用状态更新逻辑
- 希望让状态更新更可预测、可测试(类似 Redux 的思路)
基本用法
const [state, dispatch] = useReducer(reducer, initialState);reducer: 一个纯函数,格式为(state, action) => newStateinitialState: 初始状态- 返回值:
state: 当前状态dispatch: 用于派发 action 的函数
dispatch接收一个对象叫作 action 并传递给 reducer 函数,告诉 React 用户做了什么。
// 根据 id 删除
function handleDeleteTask(taskId) {
dispatch({
type: "deleted",
id: taskId,
});
}
// 修改
function handleChangeTask(task) {
dispatch({
type: "changed",
task: task,
});
}提示
action 就是一个普通的 JavaScript 对象,其结构与字段没有限制。type 可以叫 option、可以叫 category 等等,但一般需要包含发生了什么的信息, 并且尽量选择一个能够清晰描述发生事情的名字!
reducer函数就是放置状态逻辑的地方。它接受两个参数,分别为当前 state 和 action 对象,并且返回的是更新后的 state。
function yourReducer(state, action) {
// state 为当前状态
// action 为动作对象
// 返回值为新的状态
}在 reducer 函数中,通常使用 switch 语句来判断行为。
function tasksReducer(tasks, action) {
switch (action.type) {
case "changed": {
return tasks.map((t) => {
if (t.id === action.task.id) {
return action.task;
} else {
return t;
}
});
}
case "deleted": {
return tasks.filter((t) => t.id !== action.id);
}
default: {
throw Error("未知 action: " + action.type);
}
}
}- 从 React 导入
useReducerHook。
import { useReducer } from "react";const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);reducer 函数可以移到一个单独的文件。
import { useReducer } from 'react';
import tasksReducer from './tasksReducer.js';
export default function TaskApp() {
// 引入 useReducer
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);
// 编写 dispatch
function handleAddTask(text) {
dispatch({
type: 'added',
id: nextId++,
text: text,
});
}
return (
...
);
}
// 初始数据
const initialTasks = [
];export default function tasksReducer(tasks, action) {
switch (action.type) {
case "added": {
return [
...tasks,
{
id: action.id,
text: action.text,
done: false,
},
];
}
default: {
throw Error("未知 action:" + action.type);
}
}
}使用 Immer 简化 reducer
在 React 中,useImmerReducer 是一个非常强大的工具,用来简化状态管理逻辑,特别是在处理复杂嵌套状态时。 它是基于 Immer 库的一个自定义 Hook,结合了 useReducer 和 Immer 的优势,允许开发者以“可变”的方式直接修改状态,而无需手动创建不可变副本。
在 React 的 useReducer 中,更新状态需要遵循不可变性原则。这意味着你需要手动复制和展开对象或数组,这在处理嵌套状态时会变得非常繁琐。
function reducer(state, action) {
switch (action.type) {
case "UPDATE_NAME":
return {
...state,
user: {
...state.user,
name: action.payload,
},
};
default:
return state;
}
}你需要手动复制嵌套对象或数组,随着状态结构的复杂化(如多层嵌套),代码会变得冗长且难以维护。
通过 useImmerReducer,可以直接修改状态的 draft ,而无需手动展开对象或数组。最终生成的新状态仍然是不可变的。
要使用 useImmerReducer,首先需要安装 use-immer 包:
npm install use-immer然后在代码中引入 useImmerReducer :
import { useImmerReducer } from "use-immer";useImmerReducer的签名:const [state, dispatch] = useImmerReducer(reducer, initialState);reducer:接收(draft, action)参数,返回修改后的状态。initialState:初始状态。state:当前状态。dispatch:用于触发状态更新。
draft的作用:draft是一个“草稿”对象,你可以直接对它进行修改。- 修改完成后,
useImmerReducer会根据你的操作生成一个新的不可变状态。
状态更新:
- 在普通
useReducer中,你需要手动复制嵌套对象或数组。 - 在
useImmerReducer中,你只需要直接修改draft,例如:draft.user.name = action.payload; draft.posts.push(action.payload);
- 在普通
下面是一个使用 useImmerReducer 的例子:
function reducer(draft, action) {
switch (action.type) {
case "UPDATE_NAME":
draft.user.name = action.payload; // 直接修改 draft
break;
case "ADD_POST":
draft.posts.push(action.payload); // 直接修改数组
break;
default:
break;
}
}Context
使用
当我们需要将数据传递给子组件时,通常会使用 props,这在组件层级比较少时很有效。 但是当数据层级较深时,例如要将数据传递给孙子组件,props 的传递会很麻烦:需要将数据通过 props 层层传递。这时,我们可以使用 Context 。
Context 提供一种跨层级共享数据的方式,让任意后代组件都能直接访问祖先组件提供的数据,无需手动逐层传递 props。
- 创建一个 Context 并导出。
Context 使用 createContext hook 创建,它只接受一个默认值参数,可以是任何类型。
import { createContext } from "react";
export const LevelContext = createContext();- 提供 context。用使用
createContext创建的组件将可能需要用到 context 的子组件包裹起来。
- 提供 context。用使用
import { LevelContext } from "./LevelContext.js";
export default function Add() {
return <LevelContext value={需要传递的数据}>...</LevelContext>;
}- 使用 context。
引入 useContext Hook 以及创建的 context:
import { useContext } from "react";
import { LevelContext } from "./LevelContext.js";使用 useContext Hook 从 LevelContext 获取 context 的值:
export default function NeedContext() {
const level = useContext(LevelContext);
// ...
}注意事项`
在 React 中,Context 虽能解决跨层级传参问题,但极易被滥用。 优先考虑显式传递 props(即使经过多层),或通过抽象组件并用 children 传递 JSX 来减少中间层依赖——这能让数据流更清晰、组件更可维护。 只有当多个远距离组件确实需要共享低频、全局性数据(如主题、用户状态、路由信息)时,才应使用 Context,并注意拆分 Context。
操作全局共享数据
在学会使用 useContext 与 useReducer 之后,我们就可以实现类似 Redux 共享数据管理功能。
useContext用于传递全局数据useReducer用于管理全局数据
其核心思路是:将 useReducer 返回的当前状态 state 与 派发 action 的函数 dispatch 通过 useContext 创建的的组件传递给子组件。
src
components
ComA.jsx
ComB.jsx
ComC.jsx
CountContext.js
App.jsx
import { useContext } from "react";
import CountContext from "../CountContext.js";
function ComA() {
const countContext = useContext(CountContext);
return (
<>
<div>
<p>ComA count: {countContext.count}</p>
<button onClick={() => countContext.dispatch("add")}>add from ComA</button>
</div>
</>
);
}
export default ComA;import { useContext } from "react";
import CountContext from "../CountContext.js";
function ComB() {
const countContext = useContext(CountContext);
return (
<>
<div>
<p>ComA count: {countContext.count}</p>
<button onClick={() => countContext.dispatch("sub")}>substract from ComB</button>
</div>
</>
);
}
export default ComB;import { useContext } from "react";
import CountContext from "../CountContext.js";
function ComC() {
const countContext = useContext(CountContext);
return (
<>
<div>
<p>ComA count: {countContext.count}</p>
<button onClick={() => countContext.dispatch("squ")}>square from ComC</button>
</div>
</>
);
}
export default ComC;import { createContext } from "react";
const CountContext = createContext();
export default CountContext;import { useEffect, useReducer } from "react";
import CountContext from "./CountContext.js";
import ComA from "./components/ComA";
import ComB from "./components/ComB";
import ComC from "./components/ComC";
function reducer(state, action) {
switch (action) {
case "add":
return state + 1;
case "sub":
return state - 1;
case "squ":
return state * state;
default:
console.log("what?");
return state;
}
}
function App() {
const [count, dispatch] = useReducer(reducer, 0);
return (
<>
<p>App count: {count}</p>
<CountContext value={{ count, dispatch }}>
<ComA />
<ComB />
<ComC />
</CountContext>
</>
);
}
export default App;Ref
在 React 中,ref 是一种用于访问 DOM 元素或在组件多次渲染之间“记住”某个值的机制。它有两个主要用途:
- 访问真实的 DOM 节点,如聚焦输入框、测量尺寸、播放视频等。
- 存储可变值(类似实例变量),该值在组件重新渲染时不会丢失,且修改它不会触发重新渲染
useRef
在函数组件中,使用 useRef Hook 创建 ref:
const ref = useRef(initialValue);ref是一个普通 JavaScript 对象,结构为{ current: initialValue }- 修改
ref.current不会触发组件重新渲染 - 值在组件整个生命周期内持久存在
何时使用?
- 访问 DOM 元素
import { useRef, useEffect } from "react";
function TextInput() {
const inputRef = useRef(null);
useEffect(() => {
// 自动聚焦输入框
inputRef.current.focus();
}, []);
return <input ref={inputRef} type="text" />;
}- 存储可变值
当你需要一个在渲染之间保持不变的可变值(如计时器 ID、上次 props 值、手势坐标等),useRef 是理想选择:
function Timer() {
const intervalRef = useRef(null);
const [count, setCount] = useState(0);
const start = () => {
intervalRef.current = setInterval(() => {
setCount((c) => c + 1);
}, 1000);
};
const stop = () => {
clearInterval(intervalRef.current);
};
// 清理定时器
useEffect(() => {
return () => clearInterval(intervalRef.current);
}, []);
return (
<div>
<p>{count}</p>
<button onClick={start}>Start</button>
<button onClick={stop}>Stop</button>
</div>
);
}为什么不用 useState 存定时器 ID?
因为每次 setInterval 都会创建新闭包,旧的 intervalId 无法被清理。而 ref.current 始终指向最新值。
与 useState 的区别
| 特性 | useState | useRef |
|---|---|---|
| 修改值是否触发重渲染? | ✅ 是 | ❌ 否 |
| 值是否在渲染间持久化? | ✅ 是(通过状态) | ✅ 是(通过 current) |
| 适合存储什么? | 需要 UI 响应的数据(如表单值、计数) | 不需要触发 UI 更新的可变数据(如 timer ID、DOM 引用、上次值) |
forwardRef < v19
在 v19 之前,ref 不能直接传递给函数组件(因为函数组件没有“实例”)。若希望父组件能访问子组件内部的 DOM,需使用 forwardRef:
// 子组件:暴露内部 input 的 ref
const FancyInput = forwardRef((props, ref) => {
return <input ref={ref} className="fancy" {...props} />;
});
// 父组件:获取子组件内部 input 的 DOM
function App() {
const inputRef = useRef(null);
const focusInput = () => {
inputRef.current.focus(); // ✅ 成功聚焦
};
return (
<>
<FancyInput ref={inputRef} />
<button onClick={focusInput}>Focus Input</button>
</>
);
}注意事项
不要滥用 ref
优先使用 props 和 state 管理数据流。ref 应仅用于:- 必须操作 DOM 的场景(聚焦、动画、媒体控制)
- 存储与 UI 无关的可变数据
避免在渲染中读写 ref.current(除非必要)
因为它会破坏 React 的纯函数特性,可能导致难以调试的问题。TypeScript 中需指定泛型
const inputRef = useRef<HTMLInputElement>(null); const countRef = useRef<number>(0);
自定义 Hook
如果我们有一些组件需要使用相同的逻辑,那么我们可以将它封装成一个自定义 Hook。我们就可以直接在组件中使用用这个自定义 Hook,减少重复的代码。
自定义 Hook 的名称必须以 use 开头,然后紧跟一个大写字母,并且你可以返回任意类型的值。
由于自定义 Hook 需要以 use 开头,因此我们在命名其它函数的时候,应该避免以 use 开头。如果一个函数内部使用了哪怕一个 Hook,那么这个函数 都应该以 use 开头,让它成为一个 Hook。
自定义 Hook 只是封装了逻辑,因此它共享的是状态逻辑,而不是状态本身。
但我们在不同的组件中调用自定义 Hook,Hook 内部的状态会相互独立,不会相互干扰。