目录
[1. 为组件添加状态(最基础用法)](#1. 为组件添加状态(最基础用法))
[2. 根据先前的 state 更新 state(函数式更新)](#2. 根据先前的 state 更新 state(函数式更新))
[3. 更新状态中的对象和数组(不可变更新)](#3. 更新状态中的对象和数组(不可变更新))
[4. 避免重复创建初始状态](#4. 避免重复创建初始状态)
[5. 使用 key 重置状态](#5. 使用 key 重置状态)
[6. 存储前一次渲染的信息](#6. 存储前一次渲染的信息)
[三、关键补充:set 函数的核心特性](#三、关键补充:set 函数的核心特性)
[1. 连接到外部系统](#1. 连接到外部系统)
[2. 在自定义 Hook 中封装 Effect](#2. 在自定义 Hook 中封装 Effect)
[3. 控制非 React 小部件](#3. 控制非 React 小部件)
[4. 使用 Effect 请求数据](#4. 使用 Effect 请求数据)
[5. 指定响应式依赖项](#5. 指定响应式依赖项)
[6. 在 Effect 中根据先前 state 更新 state](#6. 在 Effect 中根据先前 state 更新 state)
[7. 删除不必要的对象依赖项](#7. 删除不必要的对象依赖项)
[8. 删除不必要的函数依赖项](#8. 删除不必要的函数依赖项)
[9. 从 Effect 读取最新的 props 和 state](#9. 从 Effect 读取最新的 props 和 state)
[一、基础 语法](#一、基础 语法)
[1. 向组件树深层传递数据](#1. 向组件树深层传递数据)
[2. 通过 context 更新传递的数据](#2. 通过 context 更新传递的数据)
[3. 指定后备方案默认值](#3. 指定后备方案默认值)
[4. 覆盖组件树一部分的 context](#4. 覆盖组件树一部分的 context)
[5. 在传递对象和函数时优化重新渲染](#5. 在传递对象和函数时优化重新渲染)
[1. 向组件添加 reducer(基础用法:替代复杂 useState)](#1. 向组件添加 reducer(基础用法:替代复杂 useState))
[2. 实现 reducer 函数(核心规则 + 复杂状态示例)](#2. 实现 reducer 函数(核心规则 + 复杂状态示例))
[(1)reducer 必须遵守的 3 个规则](#(1)reducer 必须遵守的 3 个规则)
[(2)复杂状态示例:管理一个 "待办列表(todos)"](#(2)复杂状态示例:管理一个 “待办列表(todos)”)
[3. 避免重新创建初始值(使用 init 函数)](#3. 避免重新创建初始值(使用 init 函数))
场景:从本地存储(localStorage)读取待办列表作为初始状态
[三、关键补充:dispatch 函数的特性](#三、关键补充:dispatch 函数的特性)
[四、useReducer vs useState:什么时候该用哪个?](#四、useReducer vs useState:什么时候该用哪个?)
[1. 跳过组件的重新渲染(最常用场景)](#1. 跳过组件的重新渲染(最常用场景))
[2. 从记忆化回调中更新 state](#2. 从记忆化回调中更新 state)
[方式 1:函数式更新(推荐,无需依赖 state)](#方式 1:函数式更新(推荐,无需依赖 state))
[方式 2:依赖 state(当更新需要其他状态 /props 时)](#方式 2:依赖 state(当更新需要其他状态 /props 时))
[3. 防止频繁触发 Effect](#3. 防止频繁触发 Effect)
[4. 优化自定义 Hook](#4. 优化自定义 Hook)
[三、关键补充:useCallback 的使用误区](#三、关键补充:useCallback 的使用误区)
[1. 跳过代价昂贵的重新计算](#1. 跳过代价昂贵的重新计算)
[2. 跳过组件的重新渲染](#2. 跳过组件的重新渲染)
[3. 防止过于频繁地触发 Effect](#3. 防止过于频繁地触发 Effect)
[4. 记忆另一个 Hook 的依赖](#4. 记忆另一个 Hook 的依赖)
[5. 记忆一个函数](#5. 记忆一个函数)
[1. 使用 ref 引用一个值(跨渲染存储普通值)](#1. 使用 ref 引用一个值(跨渲染存储普通值))
[场景 1:存储定时器 ID(用于组件卸载时清除)](#场景 1:存储定时器 ID(用于组件卸载时清除))
[场景 2:存储前一次的状态 /props](#场景 2:存储前一次的状态 /props)
[2. 通过 ref 操作 DOM(最常用场景)](#2. 通过 ref 操作 DOM(最常用场景))
[场景 1:聚焦输入框(页面加载后自动聚焦)](#场景 1:聚焦输入框(页面加载后自动聚焦))
[场景 2:获取 DOM 元素的尺寸(比如宽度、高度)](#场景 2:获取 DOM 元素的尺寸(比如宽度、高度))
[3. 避免重复创建 ref 的内容](#3. 避免重复创建 ref 的内容)
[场景:ref 初始值是复杂对象(避免重复创建)](#场景:ref 初始值是复杂对象(避免重复创建))
useState
一、基础语法
useState 接收 1 个参数 initialState(初始状态),返回一个数组,结构如下:
const [state, setState] = useState(initialState);
逐个解释核心概念:
- initialState**(初始状态)** :组件第一次渲染时的状态值,可以是任意类型(数字、字符串、布尔值、对象、数组,甚至是
null/undefined); - state**(当前状态)**:存储当前的状态值,组件渲染时会使用这个值渲染 UI;
- setState**(更新状态的函数,即你说的** setSomething**)** :触发状态更新的 "触发器",调用后会修改
state的值,并让 React 重新渲染组件;
-
- 注意:
setState是 "异步更新"(React 会批量处理多个更新,提升性能),不能在调用后立刻拿到最新的state; - 可以接收两种参数:直接传 "新状态值"(比如
setCount(5)),或传 "更新函数"(比如setCount(prev => prev + 1))。
- 注意:
二、核心用法
1. 为组件添加状态(最基础用法)
这是 useState 最核心的用途:给原本 "无状态" 的函数组件添加动态状态,让组件能响应用户操作(比如点击、输入)并更新 UI。
场景:实现一个简单的计数器,点击按钮让数字加 1。
import { useState } from 'react';
function Counter() {
// 1. 添加状态:初始值为 0,返回 [当前计数, 更新计数的函数]
const [count, setCount] = useState(0);
// 2. 点击事件:调用 setCount 更新状态
const handleIncrement = () => {
setCount(count + 1); // 传新状态值
};
return (
<div>
<p>当前计数:{count}</p> {/* 渲染当前状态 */}
<button onClick={handleIncrement}>加 1</button> {/* 触发状态更新 */}
</div>
);
}
👉 关键逻辑:
- 组件第一次渲染时,
count是初始值0; - 点击按钮调用
setCount(count + 1),count变为1,React 重新渲染组件,页面显示最新的1; - 每次点击都会重复这个 "更新状态 → 重新渲染" 的流程。
2. 根据先前的 state 更新 state(函数式更新)
由于 setState 是异步的,当你需要 "基于上一次的状态" 计算新状态时(比如连续点击按钮多次),直接用 state 变量可能会拿到 "过时的值"(因为 React 批量处理更新时,state 还没来得及更新)。
此时需要用 "函数式更新":setState(prevState => newState),prevState 是 React 保证的 "最新的上一次状态"。
场景:连续点击按钮,确保每次都是基于最新计数加 1(避免漏更)。
function Counter() {
const [count, setCount] = useState(0);
// 函数式更新:prevCount 是最新的上一次状态
const handleIncrement = () => {
setCount(prevCount => prevCount + 1); // 推荐:基于 prevCount 计算
};
// 错误示例:如果快速点击多次,可能会漏更(比如点 3 次只加 1 次)
// const handleIncrement = () => {
// setCount(count + 1); // 直接用 count,可能拿到旧值
// };
return (
<div>
<p>计数:{count}</p>
<button onClick={handleIncrement}>快速点击加 1</button>
</div>
);
}
👉 关键原则:
- 只要新状态依赖 "上一次的状态",就用函数式更新(
prevState => newState); - 新状态不依赖旧状态(比如直接设为固定值
setCount(10)),可以直接传新值。
3. 更新状态中的对象和数组(不可变更新)
当 state 是对象或数组时,不能直接修改原对象 / 数组(React 依赖 "状态引用变化" 来检测更新,直接修改原数据不会触发重渲染),必须返回一个 "新的对象 / 数组"(即 "不可变更新")。
(1)更新对象状态
用 "扩展运算符(...)" 复制原对象,再修改需要更新的属性。
import { useState } from 'react';
function UserProfile() {
// 状态是对象
const [user, setUser] = useState({
name: '张三',
age: 25,
address: { city: '北京', district: '朝阳区' } // 嵌套对象
});
// 更新顶层属性(name)
const updateName = () => {
setUser(prevUser => ({
...prevUser, // 复制原对象的所有属性
name: '李四' // 覆盖要更新的属性
}));
};
// 更新嵌套对象(address.city)
const updateCity = () => {
setUser(prevUser => ({
...prevUser,
address: {
...prevUser.address, // 复制嵌套对象的所有属性
city: '上海' // 覆盖嵌套对象的属性
}
}));
};
return (
<div>
<p>姓名:{user.name}</p>
<p>年龄:{user.age}</p>
<p>城市:{user.address.city}</p>
<button onClick={updateName}>修改姓名</button>
<button onClick={updateCity}>修改城市</button>
</div>
);
}
(2)更新数组状态
用数组的 map、filter、concat 等方法(这些方法返回新数组),或扩展运算符,避免修改原数组。
import { useState } from 'react';
function TodoList() {
// 状态是数组
const [todos, setTodos] = useState([
{ id: 1, text: '学习 useState', done: false }
]);
// 1. 添加数组元素(用 concat 或扩展运算符)
const addTodo = () => {
const newTodo = { id: Date.now(), text: '新的待办', done: false };
setTodos(prevTodos => [...prevTodos, newTodo]); // 扩展运算符:新数组 = 原数组 + 新元素
};
// 2. 修改数组元素(用 map)
const toggleTodo = (todoId) => {
setTodos(prevTodos =>
prevTodos.map(todo =>
todo.id === todoId ? { ...todo, done: !todo.done } : todo
)
);
};
// 3. 删除数组元素(用 filter)
const deleteTodo = (todoId) => {
setTodos(prevTodos => prevTodos.filter(todo => todo.id !== todoId));
};
return (
<div>
<button onClick={addTodo}>添加待办</button>
<ul>
{todos.map(todo => (
<li key={todo.id} style={{ textDecoration: todo.done ? 'line-through' : 'none' }}>
{todo.text}
<button onClick={() => toggleTodo(todo.id)}>切换状态</button>
<button onClick={() => deleteTodo(todo.id)}>删除</button>
</li>
))}
</ul>
</div>
);
}
👉 关键禁忌:
- 不要用
obj.key = value直接修改对象; - 不要用
array.push()、array.splice()等修改原数组的方法; - 嵌套对象 / 数组要 "逐层复制",确保每一层的引用都变化,React 才能检测到更新。
4. 避免重复创建初始状态
如果 initialState 是 "代价昂贵的计算"(比如创建大数组、复杂对象、从本地存储读取并解析数据),直接写在 useState 里,会导致组件每次重渲染时都重新执行这个计算(虽然 React 会忽略重渲染时的新初始值,只在第一次渲染时使用,但仍会浪费性能)。
解决方案:把初始状态的计算逻辑包装成一个 "函数",传给 useState,React 会只在组件第一次渲染时执行这个函数,后续重渲染时跳过。
场景:初始状态是一个包含 10000 个元素的大数组(计算代价高)。
import { useState } from 'react';
function BigList() {
// 错误示例:每次重渲染都会创建新的大数组(浪费性能)
// const [list, setList] = useState(Array.from({ length: 10000 }, (_, i) => i));
// 正确示例:把计算逻辑包装成函数,传给 useState
const [list, setList] = useState(() => {
// 这个函数只在第一次渲染时执行
return Array.from({ length: 10000 }, (_, i) => i); // 生成 0-9999 的数组
});
return <div>数组长度:{list.length}</div>;
}
👉 关键语法:
- 当
initialState是函数时,React 会把它当作 "初始化函数",仅执行一次; - 如果初始状态是简单类型(数字、字符串、
null),无需包装函数,直接传入即可(比如useState(0)、useState(''))。
5. 使用 key 重置状态
React 中,key 是组件的 "身份标识"。当组件的 key 变化时,React 会认为这是一个 "新组件",会重新初始化组件的状态(包括 useState 的初始状态),相当于 "重置" 组件。
场景:切换标签页时,重置当前标签页的状态(比如输入框内容、计数)。
import { useState } from 'react';
// 子组件:有自己的状态(输入框内容)
function TabContent({ tabKey }) {
const [inputValue, setInputValue] = useState('');
return (
<div>
<input
type="text"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
placeholder="输入内容..."
/>
<p>当前输入:{inputValue}</p>
</div>
);
}
// 父组件:切换标签,通过 key 重置子组件状态
function TabSwitcher() {
const [activeTab, setActiveTab] = useState('tab1');
return (
<div>
<button onClick={() => setActiveTab('tab1')}>标签 1</button>
<button onClick={() => setActiveTab('tab2')}>标签 2</button>
{/* 关键:给子组件设置 key,key 变化时子组件状态重置 */}
<TabContent tabKey={activeTab} key={activeTab} />
</div>
);
}
👉 关键效果:
- 切换标签时,
activeTab变化 → 子组件的key变化 → React 销毁旧的TabContent组件,创建新的组件 → 新组件的inputValue重置为初始值''; - 如果不设置
key,切换标签时子组件不会被销毁,inputValue会保留之前的输入(这可能不是你想要的)。
6. 存储前一次渲染的信息
有时候你需要在组件中获取 "上一次渲染时的状态 /props"(比如显示 "从 XX 改为 XX"),可以用 useState 配合 useEffect 实现:用 useState 存储前一次的值,用 useEffect 在状态变化后更新这个 "前一次的值"。
场景:显示计数的 "上一次值" 和 "当前值"。
import { useState, useEffect } from 'react';
function CounterWithPrev() {
const [count, setCount] = useState(0);
const [prevCount, setPrevCount] = useState(0); // 存储前一次的 count
// 关键:count 变化后,更新 prevCount
useEffect(() => {
setPrevCount(prev => count); // 用函数式更新确保拿到最新的 count
}, [count]); // 依赖 count:count 变化时执行
return (
<div>
<p>上一次计数:{prevCount}</p>
<p>当前计数:{count}</p>
<button onClick={() => setCount(prev => prev + 1)}>加 1</button>
</div>
);
}
👉 关键逻辑:
useEffect监听count变化,当count更新后,useEffect执行,把当前的count赋值给prevCount;- 因为
useEffect是在组件渲染完成后执行,所以prevCount始终存储的是 "上一次渲染时的count"。
补充:也可以用 useRef 存储前一次的值(更高效,不触发额外重渲染),但 useState + useEffect 是更基础的实现方式,适合刚学习的场景。
三、关键补充:set 函数的核心特性
你提到的 setSomething(nextState)(即 setState 函数)有几个重要特性,必须掌握:
-
异步性 :
setState是 "异步批量更新" 的,调用后不能立刻拿到最新的state:const [count, setCount] = useState(0);
const handleClick = () => {
setCount(1);
console.log(count); // 输出 0(异步更新,还没生效)
};
如果需要在状态更新后执行逻辑,用 useEffect 监听状态变化:
useEffect(() => {
console.log('count 更新后的最新值:', count);
}, [count]);
-
幂等性 :多次调用相同的
setState不会触发多次重渲染,React 会合并成一次:const handleClick = () => {
setCount(1);
setCount(1);
setCount(1); // 只会触发一次重渲染
}; -
函数式更新的优先级:如果多次调用函数式更新,React 会按顺序执行,确保状态正确:
const handleClick = () => {
setCount(prev => prev + 1); // 1
setCount(prev => prev + 1); // 2
setCount(prev => prev + 1); // 3(最终 count 是 3,触发一次重渲染)
};
useEffect
一、基础语法
setup(副作用函数):你要执行的副作用逻辑(比如请求数据、绑定事件),还可以返回一个"清理函数"(比如解绑事件、取消请求)。dependencies(依赖数组,可选) :控制setup何时执行的"开关",React 会对比依赖项的前后值,只有变化时才重新运行setup。
-
- 不传依赖:组件每次渲染后都执行(包括初始渲染+更新渲染)。
- 传空数组
[]:只在组件初始渲染后执行一次 (类似类组件componentDidMount)。 - 传具体依赖(如
[count, props.id]):组件初始渲染后执行,且只有依赖项变化时重新执行。
二、核心用法
1. 连接到外部系统
指组件与 React 之外的系统交互(比如浏览器 API、第三方 SDK、WebSocket 连接等),需要在组件挂载时"连接",卸载时"断开",避免内存泄漏。
场景:监听浏览器窗口大小变化、连接 WebSocket 实时通讯。
import { useEffect, useState } from 'react';
function WindowSize() {
const [size, setSize] = useState({ width: window.innerWidth });
// 连接外部系统(浏览器 resize 事件)
useEffect(() => {
// 副作用:绑定事件监听(连接)
function handleResize() {
setSize({ width: window.innerWidth });
}
window.addEventListener('resize', handleResize);
// 清理函数:解绑事件(断开),组件卸载时执行
return () => window.removeEventListener('resize', handleResize);
}, []); // 空依赖:只连接一次
return <div>窗口宽度:{size.width}px</div>;
}
👉 关键:外部连接必须在清理函数中"断开"(比如解绑事件、关闭连接),否则会导致内存泄漏。
2. 在自定义 Hook 中封装 Effect
将重复的副作用逻辑抽成自定义 Hook,复用在多个组件中(这是 useEffect 最强大的复用方式)。
场景 :多个组件都需要"监听窗口大小",抽成 useWindowSize 自定义 Hook。
// 自定义 Hook:封装副作用逻辑
function useWindowSize() {
const [size, setSize] = useState({ width: window.innerWidth });
useEffect(() => {
function handleResize() {
setSize({ width: window.innerWidth });
}
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
return size; // 暴露结果给组件使用
}
// 组件A:复用自定义 Hook
function ComponentA() {
const size = useWindowSize();
return <div>A组件:{size.width}px</div>;
}
// 组件B:复用自定义 Hook
function ComponentB() {
const size = useWindowSize();
return <div>B组件:{size.width}px</div>;
}
👉 关键:自定义 Hook 命名必须以 use 开头,内部可以调用其他 Hook(如 useEffect、useState)。
3. 控制非 React 小部件
React 无法直接控制非 React 库的 DOM 元素(比如 jQuery 插件、Chart.js 图表、地图组件),useEffect 可以在组件挂载后初始化这些小部件,卸载时销毁。
场景 :例如,如果你有一个没有使用 React 编写的第三方地图小部件或视频播放器组件,你可以使用 Effect 调用该组件上的方法,使其状态与 React 组件的当前状态相匹配。此 Effect 创建了在 map-widget.js 中定义的 MapWidget 类的实例。当你更改 Map 组件的 zoomLevel prop 时,Effect 调用类实例上的 setZoom() 来保持同步:
import { useRef, useEffect } from 'react';
import { MapWidget } from './map-widget.js';
export default function Map({ zoomLevel }) {
const containerRef = useRef(null);
const mapRef = useRef(null);
useEffect(() => {
if (mapRef.current === null) {
mapRef.current = new MapWidget(containerRef.current);
}
const map = mapRef.current;
map.setZoom(zoomLevel);
}, [zoomLevel]);
return (
<div
style={{ width: 200, height: 200 }}
ref={containerRef}
/>
);
}
👉 关键:用 useRef 保存 DOM 节点和小部件实例,避免因组件重渲染导致重复初始化。
4. 使用 Effect 请求数据
这是最常见的用法:组件渲染后请求接口数据,拿到数据后更新状态(触发组件重渲染)。
场景:请求用户列表数据并展示。
import { useState, useEffect } from 'react';
import { fetchBio } from './api.js';
export default function Page() {
const [person, setPerson] = useState('Alice');
const [bio, setBio] = useState(null);
useEffect(() => {
let ignore = false;
setBio(null);
fetchBio(person).then(result => {
if (!ignore) {
setBio(result);
}
});
return () => {
ignore = true;
};
}, [person]);
// ...
export default function Page() {
const [person, setPerson] = useState('Alice');
const [bio, setBio] = useState(null);
useEffect(() => {
async function startFetching() {
setBio(null);
const result = await fetchBio(person);
if (!ignore) {
setBio(result);
}
}
let ignore = false;
startFetching();
return () => {
ignore = true;
}
}, [person]);
..........
👉 关键:
useEffect的setup不能是 async 函数(会返回 Promise,React 无法处理),需在内部定义 async 函数并调用。- 必要时用
AbortController取消请求(避免组件卸载后仍执行setState)。 - 注意,
ignore变量被初始化为false,并且在 cleanup 中被设置为true。这样可以确保 你的代码不会受到"竞争条件"的影响:网络响应可能会以与你发送的不同的顺序到达。
5. 指定响应式依赖项
dependencies 是 useEffect 的"触发开关",你必须显式声明 setup 中用到的所有 props/state(响应式值),否则会拿到"过时"的数据。
场景 :根据用户选择的分类(category 状态),请求对应的数据。
function ProductList() {
const [category, setCategory] = useState('phone'); // 响应式状态
const [products, setProducts] = useState([]);
useEffect(() => {
// setup 中用到了 category,必须加入依赖数组
async function fetchProducts() {
const res = await fetch(`https://api.example.com/products?category=${category}`);
const data = await res.json();
setProducts(data);
}
fetchProducts();
}, [category]); // 依赖 category:category 变化时重新请求
return (
<div>
<button onClick={() => setCategory('phone')}>手机</button>
<button onClick={() => setCategory('laptop')}>电脑</button>
<ul>{products.map(p => <li key={p.id}>{p.name}</li>)}</ul>
</div>
);
}
👉 关键:
- 依赖数组必须包含
setup中所有用到的响应式值(props/state),否则 React 会报警告,且可能出现逻辑错误。 - 不要漏写依赖,也不要写无关依赖(会导致不必要的重执行)。
6. 在 Effect 中根据先前 state 更新 state
因为 count 是一个响应式值,所以必须在依赖项列表中指定它。但是,这会导致 Effect 在每次 count 更改时再次执行 cleanup 和 setup。这并不理想。
当你需要基于"上一次的状态"更新当前状态时,用 setState(prevState => newState) 形式(无需将 prevState 加入依赖)。
场景 :点击按钮时,基于上一次的计数加 1(在 useEffect 中触发)。
import { useEffect, useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
// 3秒后,基于先前的 count 加 1(无需依赖 count)
const timer = setTimeout(() => {
setCount(prevCount => prevCount + 1); // 函数式更新:拿到最新的 prevCount
}, 3000);
return () => clearTimeout(timer);
}, []); // 空依赖:只执行一次(无需加 count)
return <div>Count: {count}</div>;
}
👉 关键:函数式更新(prevState => newState)能确保拿到最新的前一次状态,此时不需要将 state 加入依赖数组。
7. 删除不必要的对象依赖项
如果依赖项是"每次渲染都会重新创建的对象/数组"(比如 { a: 1 }、[1,2]),即使内容没变,React 也会认为依赖变化,导致 useEffect 重复执行。此时需要"稳定化"依赖。
错误示例 :依赖对象每次渲染都重建,导致 useEffect 无限执行。
function BadExample() {
// 每次渲染都会创建新对象 { limit: 10 }
const config = { limit: 10 };
useEffect(() => {
console.log('依赖变化,执行 Effect'); // 会无限触发,因为 config 每次都是新对象
fetch(`https://api.example.com/data?limit=${config.limit}`);
}, [config]); // 错误:依赖不稳定的对象
return <div>...</div>;
}
正确做法1 :用 useMemo 缓存对象,让其只在内容变化时重建。
import { useEffect, useMemo } from 'react';
function GoodExample() {
// 用 useMemo 缓存对象:只有依赖变化时才重新创建
const config = useMemo(() => ({ limit: 10 }), []); // 空依赖:永久缓存
useEffect(() => {
fetch(`https://api.example.com/data?limit=${config.limit}`);
}, [config]); // 现在 config 稳定,不会重复执行
return <div>...</div>;
}
关键:对象/数组依赖需用 useMemo 缓存,或直接解构出原始值(如 [config.limit]),避免不必要的重执行。
正确做法2:直接在Effect内部创建对象。
8. 删除不必要的函数依赖项
如果 setup 中用到的函数是组件内部定义的,默认每次渲染都会重新创建,导致 useEffect 重复执行。此时需要用 useCallback 缓存函数,稳定依赖。
错误示例 :函数每次渲染重建,导致 useEffect 重复执行。
function BadExample() {
// 每次渲染都会创建新函数 handleFetch
function handleFetch() {
fetch('https://api.example.com/data');
}
useEffect(() => {
handleFetch(); // 会重复执行,因为 handleFetch 每次都是新函数
}, [handleFetch]); // 错误:依赖不稳定的函数
}
正确做法1 :用 useCallback 缓存函数,让其只在依赖变化时重建。
import { useEffect, useCallback } from 'react';
function GoodExample() {
// 用 useCallback 缓存函数:只有依赖变化时才重新创建
const handleFetch = useCallback(() => {
fetch('https://api.example.com/data');
}, []); // 空依赖:永久缓存
useEffect(() => {
handleFetch();
}, [handleFetch]); // 现在 handleFetch 稳定,只执行一次
}
👉 关键:组件内部的函数作为依赖时,需用 useCallback 缓存,避免因函数重建导致 useEffect 无效重执行。
**正确做法2:**避免使用在渲染期间创建的函数作为依赖项,请在 Effect 内部声明它:
import { useEffect, useCallback } from 'react';
function GoodExample() {
useEffect(() => {
function handleFetch() {
fetch('https://api.example.com/data');
}
handleFetch();
}, [handleFetch]); // 现在 handleFetch 稳定,只执行一次
}
9. 从 Effect 读取最新的 props 和 state
默认情况下,在 Effect 中读取响应式值时,必须将其添加为依赖项。这样可以确保你的 Effect 对该值的每次更改都"作出响应"。对于大多数依赖项,这是你想要的行为。
然而,有时你想要从 Effect 中获取 最新的 props 和 state,而不"响应"它们。例如,假设你想记录每次页面访问时购物车中的商品数量:
function Page({ url, shoppingCart }) {
useEffect(() => {
logVisit(url, shoppingCart.length);
}, [url, shoppingCart]); // ✅ 所有声明的依赖项
// ...
}
如果你想在每次 url****更改后记录一次新的页面访问,而不是在 shoppingCart****更改后记录,该怎么办 ?你不能在不违反 响应规则 的情况下将 shoppingCart 从依赖项中移除。然而,你可以表达你 不希望 某些代码对更改做出"响应",即使它是在 Effect 内部调用的。使用 useEffectEvent Hook
,并将读取 shoppingCart 的代码移入其中:
function Page({ url, shoppingCart }) {
const onVisit = useEffectEvent(visitedUrl => {
logVisit(visitedUrl, shoppingCart.length)
});
useEffect(() => {
onVisit(url);
}, [url]); // ✅ 所有声明的依赖项
// ...
}
通过在 onVisit 中读取 shoppingCart,确保了 shoppingCart 不会使 Effect 重新运行。
useContext
一、基础 语法
- 用法:
useContext(SomeContext) - SomeContext:是通过
React.createContext(默认值)创建的 "上下文容器",用来存储要传递的数据(类似一个 "全局数据仓库")。 - useContext****作用 :在组件中调用
useContext(SomeContext),就能直接获取到最近的SomeContext.Provider提供的值(如果没有 Provider,则获取创建 Context 时的默认值)。 - 核心流程:
-
- 用
createContext创建 Context(指定默认值,可选); - 用
Context.Provider包裹组件树(通过value属性传递数据); - 深层组件用
useContext(Context)直接获取数据。
- 用
二、解释核心用法
1. 向组件树深层传递数据
这是 useContext 最基础的用法:当你需要给嵌套多层的组件传递数据时,不用一层一层写 props 传递,直接用 Context 跨层级传递。
场景:App 顶层有 "主题色" 数据,需要传递给嵌套在 3 层下的 Button 组件。
import { createContext, useContext } from 'react';
// 1. 创建 Context(默认值可选,这里设为 'light')
const ThemeContext = createContext('light');
// 顶层组件:用 Provider 包裹子组件树,传递数据
function App() {
const theme = 'dark'; // 要传递的深层数据
return (
{/* Provider 的 value 属性:指定要传递给后代的数据 */}
<ThemeContext.Provider value={theme}>
<Layout /> {/* 中间组件(无需传递 theme props) */}
</ThemeContext.Provider>
);
}
// 中间组件(无需关心 theme,直接透传子组件)
function Layout() {
return <Navbar />; // 第二层组件
}
function Navbar() {
return <Button />; // 第三层组件(需要 theme)
}
// 深层组件:用 useContext 获取数据
function Button() {
// 直接获取最近的 ThemeContext.Provider 传递的 value
const theme = useContext(ThemeContext);
return (
<button style={{ background: theme === 'dark' ? '#333' : '#fff' }}>
主题按钮
</button>
);
}
关键:中间组件(Layout、Navbar)完全不用处理 theme 数据,useContext 让深层组件直接 "跳过中间层" 获取顶层数据,解决了 prop drilling 痛点。
2. 通过 context 更新传递的数据
Context 不仅能传递 "静态数据",还能传递 "状态和修改状态的函数",实现深层组件修改顶层数据的效果(类似 "全局状态管理" 的简化版)。
场景:顶层有 "主题色" 状态,深层组件的按钮可以切换主题。
import { createContext, useContext, useState } from 'react';
// 1. 创建 Context(默认值可以设为 { theme: '', toggleTheme: () => {} },提示类型)
const ThemeContext = createContext({
theme: 'light',
toggleTheme: () => {},
});
// 顶层组件:管理状态 + 提供修改函数
function App() {
const [theme, setTheme] = useState('light');
// 定义修改状态的函数
const toggleTheme = () => {
setTheme(prev => prev === 'light' ? 'dark' : 'light');
};
// 传递给 Provider 的 value 包含"状态"和"修改函数"
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
<Layout />
</ThemeContext.Provider>
);
}
// 深层组件:调用 toggleTheme 修改顶层状态
function Button() {
const { theme, toggleTheme } = useContext(ThemeContext);
return (
<button
style={{ background: theme === 'dark' ? '#333' : '#fff', color: theme === 'dark' ? '#fff' : '#333' }}
onClick={toggleTheme} // 点击切换主题
>
当前主题:{theme}(点击切换)
</button>
);
}
👉 关键:Context 传递的 value 可以是对象,包含 state 和修改 state 的函数,深层组件调用函数就能触发顶层状态更新,进而让所有使用该 Context 的组件重新渲染。
3. 指定后备方案默认值
创建 Context 时可以传入 "默认值",当组件树中没有找到对应的 Context.Provider 时,useContext 会返回这个默认值(类似 "降级方案")。
场景:开发组件时,允许用户不提供 Provider,此时使用默认主题。
import { createContext, useContext } from 'react';
// 1. 创建 Context 时指定默认值:{ theme: 'light' }
const ThemeContext = createContext({ theme: 'light' });
// 组件 A:用户没有用 Provider 包裹
function ComponentA() {
return <Button />; // 没有 Provider 嵌套
}
// 深层组件:获取默认值
function Button() {
const { theme } = useContext(ThemeContext);
// 因为没有 Provider,所以 theme 是默认值 'light'
return <button style={{ background: theme }}>默认主题按钮</button>;
}
关键:
- 默认值只有在没有 Provider 时才会生效 ;如果有 Provider,无论 Provider 的
value是什么,都会覆盖默认值。 - 默认值的作用是 "兜底",避免组件因获取不到值而报错,适合开发可复用组件时使用。
4. 覆盖组件树一部分的 context
可以在组件树的某个分支上,再套一层 Context.Provider,覆盖父级 Provider 传递的值,实现 "局部数据覆盖"。
场景:整个 App 是深色主题,但某个模块需要单独使用浅色主题。
import { createContext, useContext, useState } from 'react';
const ThemeContext = createContext('light');
function App() {
const [theme, setTheme] = useState('dark'); // 全局主题:dark
return (
<ThemeContext.Provider value={theme}>
<GlobalComponent /> {/* 用全局主题 dark */}
{/* 局部覆盖:套一层 Provider,value 设为 'light' */}
<ThemeContext.Provider value='light'>
<LocalModule /> {/* 局部主题:light */}
</ThemeContext.Provider>
</ThemeContext.Provider>
);
}
// 全局组件:获取全局主题 dark
function GlobalComponent() {
const theme = useContext(ThemeContext);
return <div>全局主题:{theme}</div>; // 显示 dark
}
// 局部模块:获取局部覆盖的主题 light
function LocalModule() {
const theme = useContext(ThemeContext);
return <div>局部主题:{theme}</div>; // 显示 light
}
关键:Context.Provider 可以嵌套,内层 Provider 的值会覆盖外层。组件会获取 "最近的" Provider 传递的值(就近原则)。
5. 在传递对象和函数时优化重新渲染
当 Context 的 value 是对象或函数时,会有一个坑:每次顶层组件重新渲染,都会创建新的对象 / 函数,导致所有使用该 Context 的组件也跟着重新渲染(即使数据没变化)。此时需要优化,避免不必要的重渲染。
问题示例 :value 是对象,每次 App 重渲染都会创建新对象,导致 Button 无效重渲染。
function App() {
const [count, setCount] = useState(0);
// 每次 App 重渲染,都会创建新对象 { theme: 'dark' }
return (
<ThemeContext.Provider value={{ theme: 'dark' }}>
<button onClick={() => setCount(count + 1)}>计数:{count}</button>
<Button /> {/* 即使 theme 没变化,也会跟着重渲染 */}
</ThemeContext.Provider>
);
}
优化方案 :用 useMemo 缓存对象 / 函数,让 value 只在依赖变化时才更新。
import { createContext, useContext, useMemo, useState } from 'react';
const ThemeContext = createContext({ theme: 'light' });
function App() {
const [count, setCount] = useState(0);
const theme = 'dark';
// 用 useMemo 缓存对象:只有依赖(theme)变化时,才创建新对象
const contextValue = useMemo(() => ({
theme,
toggleTheme: () => { /* 修改主题的函数 */ }
}), [theme]); // 依赖只有 theme,count 变化时不会重新创建
return (
<ThemeContext.Provider value={contextValue}>
<button onClick={() => setCount(count + 1)}>计数:{count}</button>
<Button /> {/* 只有 theme 变化时才重渲染,count 变化时不重渲染 */}
</ThemeContext.Provider>
);
}
关键:
- 传递对象 / 函数时,必须用
useMemo缓存value(如果是函数,还可以用useCallback单独缓存函数); - 依赖数组只包含真正会变化的变量,避免因无关状态变化导致
value重建,进而引发无效重渲染
useReducer
一、基础语法
useReducer 接收 3 个参数,返回一个数组(类似 useState),结构如下:
const [state, dispatch] = useReducer(reducer, initialArg, init?);
逐个解释核心概念:
- reducer**(状态处理器函数)**:最核心的部分,是一个 "纯函数"(输入相同,输出一定相同,不产生副作用),负责根据 "动作(action)" 计算新的状态。
- 语法:
function reducer(state, action) { /* 计算并返回新状态 */ }
-
state:当前的状态值(类似useState的当前状态);action:描述 "要做什么" 的对象,必须包含type字段(动作类型,通常是字符串常量),可选包含payload字段(动作携带的数据);- 返回值:新的状态(React 会用这个新状态重新渲染组件)。
- initialArg**(初始状态参数)** :用于指定状态的初始值,具体含义取决于是否传了第 3 个参数
init:
-
- 没传
init:initialArg就是状态的初始值(直接使用); - 传了
init:initialArg是给init函数的 "参数",初始状态由init(initialArg)计算得出。
- 没传
- init?****(初始状态初始化函数,可选) :一个函数,用于 "延迟计算初始状态"(比如从本地存储读取、处理
initialArg后得到初始状态),调用时机是组件初始渲染时。 - dispatch**(动作分发函数)** :
useReducer返回的第二个值,是触发状态更新的 "触发器"。你通过调用dispatch(action)来发送一个 "动作",React 会自动调用reducer函数,传入当前state和这个action,计算出 新状态并更新组件。
二、核心用法
1. 向组件添加 reducer(基础用法:替代复杂 useState)
当你的状态修改逻辑需要多个 if/else 或状态之间相互依赖时,用 useReducer 替代 useState,让逻辑更清晰。
场景 :实现一个 "计数器",支持 "加、减、重置" 三种操作(比单纯的 count +1 复杂,适合 useReducer)。
import { useReducer } from 'react';
// 1. 定义 reducer 函数(纯函数:处理状态逻辑)
function countReducer(state, action) {
// 根据 action.type 决定做什么操作
switch (action.type) {
case 'INCREMENT': // 加 1
return state + 1; // 返回新状态(不要修改原 state!)
case 'DECREMENT': // 减 1
return state - 1;
case 'RESET': // 重置
return 0;
default:
// 遇到未知 action 时,抛出错误(避免笔误)
throw new Error(`未知的动作类型:${action.type}`);
}
}
// 2. 组件中使用 useReducer
function Counter() {
// 初始化状态:初始值为 0(没传 init,initialArg 直接作为初始状态)
const [count, dispatch] = useReducer(countReducer, 0);
return (
<div>
<p>当前计数:{count}</p>
{/* 3. 调用 dispatch 分发动作(触发状态更新) */}
<button onClick={() => dispatch({ type: 'INCREMENT' })}>加 1</button>
<button onClick={() => dispatch({ type: 'DECREMENT' })}>减 1</button>
<button onClick={() => dispatch({ type: 'RESET' })}>重置</button>
</div>
);
}
核心逻辑:
- 你不用直接修改
count,而是通过dispatch发送一个 "动作"(比如{ type: 'INCREMENT' }); - React 把当前
count和这个动作传给countReducer; reducer计算出 新count并返回,React 用新状态重渲染组件。
对比 useState:如果用 useState 实现,需要写 3 个独立的修改函数(setCount(count+1)、setCount(count-1)、setCount(0)),逻辑分散;而 useReducer 把所有状态修改逻辑集中在 reducer 里,更易维护。
2. 实现 reducer 函数(核心规则 + 复杂状态示例)
reducer 是 useReducer 的灵魂,必须遵守 "纯函数规则",同时要处理 "复杂状态"(比如对象、数组)时,注意 "不可变更新"(不要直接修改原 state,要返回新的状态对象 / 数组)。
(1)reducer 必须遵守的 3 个规则
- 纯函数:不修改入参(
state和action都不能直接改)、不产生副作用(不请求数据、不操作 DOM、不随机生成值); - 必须返回新状态:哪怕状态没变化,也不能返回
undefined(可以返回原state); - 动作类型(
action.type)建议用大写常量(比如'ADD_ITEM'),避免笔误。
(2)复杂状态示例:管理一个 "待办列表(todos)"
状态是数组对象([{ id: 1, text: '学习 useReducer', done: false }]),支持 "添加、切换完成状态、删除" 操作。
import { useReducer, useState } from 'react';
// 1. 定义动作类型常量(避免笔误)
const ADD_TODO = 'ADD_TODO';
const TOGGLE_TODO = 'TOGGLE_TODO';
const DELETE_TODO = 'DELETE_TODO';
// 2. 定义 reducer 函数(处理复杂状态:数组对象)
function todoReducer(state, action) {
switch (action.type) {
case ADD_TODO:
// 不可变更新:返回新数组(不修改原 state 数组)
return [
...state, // 复制原有待办项
{
id: Date.now(), // 唯一 ID(用时间戳)
text: action.payload, // 从 action.payload 拿待办文本
done: false
}
];
case TOGGLE_TODO:
// 不可变更新:映射数组,只修改目标项的 done 状态
return state.map(todo =>
todo.id === action.payload ? { ...todo, done: !todo.done } : todo
);
case DELETE_TODO:
// 不可变更新:过滤掉要删除的项,返回新数组
return state.filter(todo => todo.id !== action.payload);
default:
throw new Error(`未知动作:${action.type}`);
}
}
// 3. 组件中使用
function TodoList() {
const [todos, dispatch] = useReducer(todoReducer, []); // 初始状态是空数组
const [text, setText] = useState('');
const handleAdd = () => {
if (!text.trim()) return;
// 分发 ADD_TODO 动作,携带 payload(待办文本)
dispatch({ type: ADD_TODO, payload: text });
setText(''); // 清空输入框
};
return (
<div>
<input
type="text"
value={text}
onChange={(e) => setText(e.target.value)}
placeholder="输入待办..."
/>
<button onClick={handleAdd}>添加</button>
<ul>
{todos.map(todo => (
<li key={todo.id} style={{ textDecoration: todo.done ? 'line-through' : 'none' }}>
{todo.text}
<button onClick={() => dispatch({ type: TOGGLE_TODO, payload: todo.id })}>
{todo.done ? '取消完成' : '标记完成'}
</button>
<button onClick={() => dispatch({ type: DELETE_TODO, payload: todo.id })}>
删除
</button>
</li>
))}
</ul>
</div>
);
}
👉 关键注意点:
- 复杂状态(对象 / 数组)必须 "不可变更新":不能用
state.push()(修改原数组)、state.todo.done = true(修改原对象),要通过扩展运算符(...)、map、filter等方法返回新的状态; action.payload是灵活的:可以是字符串、数字、对象等,用来传递修改状态所需的数据(比如待办文本、待办 ID)。
3. 避免重新创建初始值(使用 init 函数)
如果你的初始状态需要 "复杂计算"(比如从本地存储读取、处理大量数据),直接把计算逻辑写在 initialArg 里,会导致组件每次重渲染时都重新计算一次初始值(虽然 React 会忽略,但浪费性能)。
此时用第 3 个参数 init 函数,让初始状态只计算一次(组件初始渲染时执行,重渲染时不执行)。
场景:从本地存储(localStorage)读取待办列表作为初始状态
import { useReducer } from 'react';
// 1. 定义 init 函数:计算初始状态(只执行一次)
function initTodoState(initialArg) {
// initialArg 是传入的参数(这里是 'todos',本地存储的 key)
const savedTodos = localStorage.getItem(initialArg);
// 如果有保存的待办,解析为数组;没有则返回默认空数组
return savedTodos ? JSON.parse(savedTodos) : [];
}
// 2. 复用之前的 todoReducer
function todoReducer(state, action) { /* ... 同上 ... */ }
function TodoList() {
// 3. 使用 init 函数:initialArg 是 'todos'(传给 init 函数的参数)
const [todos, dispatch] = useReducer(todoReducer, 'todos', initTodoState);
// 可选:监听 todos 变化,同步到本地存储(副作用用 useEffect)
useEffect(() => {
localStorage.setItem('todos', JSON.stringify(todos));
}, [todos]);
// ... 其余代码同上 ...
}
核心优势:
initTodoState只在组件第一次渲染时执行,后续组件重渲染(比如添加、删除待办)时,不会再执行,避免重复计算;- 如果初始状态不需要复杂计算,直接传
initialArg即可(不用init函数)。
三、关键补充:dispatch 函数的特性
dispatch 是 useReducer 返回的 "动作分发器",有两个重要特性你需要知道:
-
dispatch****函数是稳定的 :组件重渲染时,
dispatch不会重新创建(和useState的setState类似),所以可以安全地作为useEffect、useCallback的依赖,不用怕触发无效重渲染;useEffect(() => {
// 可以放心把 dispatch 加入依赖,不会频繁触发
console.log('dispatch 是稳定的');
}, [dispatch]); -
dispatch****可以传递给子组件 :和
setState一样,dispatch可以作为 props 传给子组件,让子组件也能触发状态更新(适合深层组件修改顶层状态);// 子组件:接收 dispatch 并使用
function TodoItem({ todo, dispatch }) {
return (
{todo.text}
<button onClick={() => dispatch({ type: TOGGLE_TODO, payload: todo.id })}>
切换状态
</button>
);
}
// 父组件:传递 dispatch
function TodoList() {
const [todos, dispatch] = useReducer(todoReducer, []);
return (
{todos.map(todo => (
<TodoItem key={todo.id} todo={todo} dispatch={dispatch} />
))}
);
}
四、useReducer vs useState:什么时候该用哪个?
很多时候两者都能实现需求,但选择的核心是 "状态逻辑的复杂度":
|--------|----------------------------------|--------------------------------|
| 场景 | 推荐用 useState | 推荐用 useReducer |
| 状态类型 | 简单值(数字、字符串、布尔值) | 复杂状态(对象、数组),或状态之间相互依赖 |
| 修改逻辑 | 简单(直接赋值,比如 setCount(count+1) ) | 复杂(多个 if/else 、多条件判断、多种操作类型) |
| 代码维护 | 逻辑简单,无需集中管理 | 逻辑分散时需要集中管理(比如多个组件需要修改同一状态) |
简单总结:简单状态用 useState**,复杂状态逻辑用** useReducer。
useCallback
一、先基础语法
useCallback 接收 2 个参数,返回一个 "记忆化的函数",结构如下:
const memoizedFn = useCallback(fn, dependencies);
逐个解释核心概念:
- fn**(要缓存的函数)**:你需要缓存的组件内定义的函数(比如事件处理函数、传给子组件的回调),可以是普通函数、箭头函数,也可以是异步函数。
- dependencies**(依赖数组)**:控制函数缓存是否失效的 "开关",React 会浅对比依赖项的前后值:
-
- 依赖项无变化:
useCallback返回之前缓存的函数(引用不变); - 依赖项有变化:
useCallback重新创建函数,返回新的引用; - 依赖数组必须包含
fn中用到的所有响应式值(props、state、组件内变量 / 函数),否则会拿到 "过时的闭包值"。
- 依赖项无变化:
- memoizedFn**(记忆化的函数)**:缓存后的函数,组件重渲染时若依赖没变化,引用始终不变。
二、核心用
1. 跳过组件的重新渲染(最常用场景)
React 组件默认会在 "自身 state 变化" 或 "接收的 props 变化" 时重新渲染。如果父组件传给子组件的 "函数 props" 每次渲染都重新创建(引用变化),哪怕子组件用 React.memo 包裹(浅对比 props),也会认为 props 变化而无效重渲染。
用 useCallback 缓存函数,让函数引用稳定,配合 React.memo,就能跳过子组件的无效重渲染。
场景:父组件传递事件处理函数给子组件,避免子组件频繁重渲染。
import { useCallback, useState, memo } from 'react';
// 子组件:用 memo 包裹,浅对比 props(只有 props 真正变化时才重渲染)
const ChildButton = memo(({ onClick, label }) => {
console.log(`子组件 "${label}" 渲染了`); // 仅在 onClick 或 label 变化时打印
return <button onClick={onClick}>{label}</button>;
});
function Parent() {
const [count, setCount] = useState(0);
// 用 useCallback 缓存函数:只有依赖变化时才重新创建函数
const handleClick = useCallback(() => {
console.log('点击了按钮');
}, []); // 空依赖:函数永久缓存,引用不变
return (
<div>
<p>父组件计数:{count}</p>
<button onClick={() => setCount(count + 1)}>父组件计数+1</button>
{/* 传递缓存后的函数给子组件 */}
<ChildButton onClick={handleClick} label="测试按钮" />
</div>
);
}
👉关键效果:
- 点击 "父组件计数 + 1" 时,父组件的
count变化导致父组件重渲染,但handleClick被useCallback缓存(依赖为空,引用不变); - 子组件
ChildButton用React.memo浅对比onClick引用没变化,所以不重新渲染,实现性能优化。
❌ 反例(不推荐):如果不用 useCallback,每次父组件重渲染都会创建新的 handleClick 函数(引用变化),哪怕函数逻辑没变,子组件也会跟着重渲染,造成无效开销。
2. 从记忆化回调中更新 state
当你需要在缓存的函数中更新 state,且 state 更新依赖 "前一次的 state" 时,有两种安全方式:要么用 "函数式更新"(无需依赖 state),要么把 state 加入 useCallback 的依赖数组。
场景:缓存一个 "计数 + 1" 的回调函数,依赖前一次的 count 状态。
方式 1:函数式更新(推荐,无需依赖 state)
如果 state 更新只依赖前一次的值,用 setState(prev => newState) 形式,此时不需要把 state 加入依赖数组,函数引用更稳定。
import { useCallback, useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
// 函数式更新:prevCount 是最新的前一次状态,无需依赖 count
const increment = useCallback(() => {
setCount(prevCount => prevCount + 1);
}, []); // 空依赖:函数永久缓存
return (
<div>
<p>计数:{count}</p>
<button onClick={increment}>加 1</button>
</div>
);
}
方式 2:依赖 state(当更新需要其他状态 /props 时)
如果 state 更新依赖多个值(比如 count 和 step),需要把这些依赖加入 useCallback 的依赖数组,确保函数拿到最新值。
import { useCallback, useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
const [step, setStep] = useState(1); // 步长状态
// 依赖 count 和 step:两者变化时,函数重新创建
const increment = useCallback(() => {
setCount(count + step); // 依赖 count 和 step
}, [count, step]); // 必须加入依赖数组
return (
<div>
<p>计数:{count},步长:{step}</p>
<button onClick={increment}>加 {step}</button>
<button onClick={() => setStep(step + 1)}>步长+1</button>
</div>
);
}
👉 关键注意点:
- 函数中用到的响应式值(state/props),必须加入
useCallback的依赖数组,否则会拿到 "过时的闭包值"(比如 count 已经变了,但函数里还是旧值); - 能用量化更新就尽量用(减少依赖,让函数更稳定)。
3. 防止频繁触发 Effect
useEffect 会在依赖项变化时执行,如果依赖项是 "每次渲染都重新创建的函数",会导致 useEffect 频繁触发(哪怕函数逻辑没变)。用 useCallback 缓存函数,让函数引用稳定,就能避免这种情况。
场景 :useEffect 依赖一个事件处理函数,只有函数逻辑相关的依赖变化时才触发 Effect。
import { useCallback, useState, useEffect } from 'react';
function DataLogger() {
const [data, setData] = useState('');
const [logCount, setLogCount] = useState(0);
// 用 useCallback 缓存日志函数:依赖 data
const logData = useCallback(() => {
console.log('当前数据:', data);
setLogCount(prev => prev + 1);
}, [data]); // 仅 data 变化时,函数重新创建
// useEffect 依赖缓存后的 logData
useEffect(() => {
console.log('Effect 触发:日志函数更新');
logData(); // 执行日志函数
}, [logData]); // 依赖稳定,仅 data 变化时触发 Effect
return (
<div>
<input
type="text"
value={data}
onChange={(e) => setData(e.target.value)}
placeholder="输入数据..."
/>
<p>日志触发次数:{logCount}</p>
</div>
);
}
👉 关键效果:
- 只有
data变化时,logData才会重新创建,useEffect才会触发; - 如果不用
useCallback,每次组件重渲染(比如输入框输入时)都会创建新的logData函数,useEffect会频繁触发,导致无效日志。
4. 优化自定义 Hook
自定义 Hook 中如果返回函数(比如事件回调、订阅函数),这些函数会在每次调用 Hook 时重新创建,导致使用 Hook 的组件可能出现无效重渲染。用 useCallback 缓存返回的函数,能让自定义 Hook 更高效、更稳定。
场景:封装一个 "监听窗口大小变化" 的自定义 Hook,返回 "手动刷新尺寸" 的回调函数,避免函数重复创建。
import { useCallback, useState, useEffect, useRef } from 'react';
// 自定义 Hook:监听窗口大小
function useWindowSize() {
const [size, setSize] = useState({ width: window.innerWidth });
const sizeRef = useRef(size); // 用 ref 存储最新尺寸(避免闭包问题)
// 同步尺寸到 ref(不触发重渲染)
useEffect(() => {
sizeRef.current = size;
}, [size]);
// 用 useCallback 缓存刷新函数:无依赖,永久稳定
const refreshSize = useCallback(() => {
setSize({ width: window.innerWidth });
console.log('手动刷新尺寸:', window.innerWidth);
}, []);
// 监听窗口 resize 事件
useEffect(() => {
function handleResize() {
setSize({ width: window.innerWidth });
}
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
return [size, refreshSize]; // 返回状态和缓存后的回调
}
// 组件使用自定义 Hook
function App() {
const [size, refreshSize] = useWindowSize();
console.log('App 渲染');
return (
<div>
<p>窗口宽度:{size.width}px</p>
<button onClick={refreshSize}>手动刷新尺寸</button>
</div>
);
}
👉 关键优化点:
- 自定义 Hook 返回的
refreshSize被useCallback缓存,引用稳定,使用 Hook 的组件(如 App)重渲染时,refreshSize不会重新创建; - 如果其他组件接收
refreshSize作为 props,配合React.memo就能避免无效重渲染,让自定义 Hook 更具复用性和性能。
三、关键补充:useCallback 的使用误区
- 不要滥用 useCallback:
-
useCallback本身有缓存开销(存储函数引用、对比依赖项),如果函数是组件内部自用(不传给子组件、不作为 Effect 依赖),且逻辑简单,没必要用useCallback------ 直接定义函数即可,反而更高效;- 只有当函数作为
props传给子组件(且子组件用React.memo包裹),或作为useEffect/ 其他 Hook 的依赖时,才需要用useCallback。
- 依赖数组不能漏写:
-
- 函数中用到的所有响应式值(state/props/ 组件内变量),必须加入依赖数组,否则会出现 "闭包陷阱"(函数里拿到的是旧值);
- 可以开启 ESLint 的
react-hooks/exhaustive-deps规则,自动检测漏写的依赖。
- useCallback 不能替代 useMemo:
-
useCallback缓存的是 "函数引用",useMemo缓存的是 "计算结果"(可以是任意类型);- 缓存函数用
useCallback,缓存其他值(对象、数组、数字)用useMemo。
- 异步函数的缓存:
-
-
异步函数(
async/await)也可以用useCallback缓存,只需确保依赖数组包含函数中用到的所有响应式值:const fetchData = useCallback(async () => {
const res = await fetch(/api/data?page=${page});
const data = await res.json();
setData(data);
}, [page]); // 依赖 page
-
useMemo
一、基础语法
useMemo 接收 2 个参数,返回 "缓存的计算结果",结构如下:
const memoizedValue = useMemo(calculateValue, dependencies);
逐个解释核心概念:
- calculateValue**(计算函数)** :你要执行的 "代价昂贵的计算逻辑",必须是一个纯函数 (输入相同则输出相同,无副作用),最终返回一个 "需要被缓存的值"(可以是数字、字符串、对象、数组、函数等)。❗ 注意:这个函数会在组件渲染期间执行 ,不要在里面写副作用(比如请求数据、操作 DOM)------ 副作用请用
useEffect。 - dependencies**(依赖数组)**:控制缓存是否失效的 "开关",React 会浅对比依赖项的前后值:
-
- 只有当依赖项中有任一值发生变化时,
calculateValue才会重新执行,返回新结果并更新缓存; - 依赖项没变化时,
useMemo直接返回之前缓存的结果,跳过计算; - 依赖数组必须包含
calculateValue中用到的所有响应式值(props、state、组件内定义的变量 / 函数),否则会拿到 "过时的缓存结果"。
- 只有当依赖项中有任一值发生变化时,
- memoizedValue**(缓存的结果)** :
calculateValue执行后的结果,会被 React 缓存起来,组件重渲染时若依赖没变化,直接复用这个值。
二、核心用法
1. 跳过代价昂贵的重新计算
这是 useMemo 最核心的用法:当你有 "耗时的计算逻辑"(比如遍历大数据、复杂数学运算、深层数据转换),组件重渲染时(比如无关状态变化),不需要重复执行这些计算,用 useMemo 缓存结果。
场景:过滤并排序一个包含 10000 条数据的列表(计算代价高),只有当 "原始数据" 或 "过滤条件" 变化时才重新计算。
import { useMemo, useState } from 'react';
function BigList({ data }) {
const [filterText, setFilterText] = useState('');
// 代价昂贵的计算:过滤 + 排序 10000 条数据
const filteredAndSortedData = useMemo(() => {
console.log('重新计算过滤排序结果(仅依赖变化时执行)');
return data
.filter(item => item.name.includes(filterText)) // 过滤
.sort((a, b) => a.age - b.age); // 排序
}, [data, filterText]); // 依赖:只有 data 或 filterText 变化时,才重新计算
return (
<div>
<input
type="text"
value={filterText}
onChange={(e) => setFilterText(e.target.value)}
placeholder="搜索名称..."
/>
<ul>
{filteredAndSortedData.map(item => (
<li key={item.id}>{item.name}({item.age}岁)</li>
))}
</ul>
</div>
);
}
// 模拟 10000 条测试数据
const mockData = Array.from({ length: 10000 }, (_, i) => ({
id: i,
name: `用户${i}`,
age: Math.floor(Math.random() * 50)
}));
function App() {
return <BigList data={mockData} />;
}
👉 关键效果:
- 当你输入过滤文本(
filterText变化)或data变化时,才会重新执行过滤排序; - 如果组件因其他原因重渲染(比如父组件传了无关 props),
useMemo直接返回缓存结果,跳过耗时计算,提升页面响应速度。
❌ 反例(不推荐):如果不用 useMemo,每次组件重渲染都会执行 data.filter(...).sort(...),哪怕 data 和 filterText 都没变化,会造成不必要的性能浪费。
2. 跳过组件的重新渲染
React 组件默认会在 "props 变化" 或 "自身 state 变化" 时重新渲染。如果父组件传递给子组件的 props 是 "每次渲染都会重新创建的对象 / 数组 / 函数"(比如 { a: 1 }、[1,2]),哪怕内容没变,子组件也会认为 props 变化而重新渲染。
此时用 useMemo 缓存 props 的值,让 props 只有在内容变化时才重新创建,配合子组件的 React.memo(浅对比 props),就能跳过不必要的子组件重渲染。
场景:父组件传递一个对象给子组件,避免子组件无效重渲染。
import { useMemo, useState, memo } from 'react';
// 子组件:用 memo 包裹,浅对比 props(只有 props 真正变化时才重渲染)
const Child = memo(({ userInfo }) => {
console.log('子组件渲染了'); // 仅在 userInfo 内容变化时打印
return <div>用户名:{userInfo.name},年龄:{userInfo.age}</div>;
});
function Parent() {
const [count, setCount] = useState(0);
const name = '张三';
const age = 25;
// 用 useMemo 缓存对象:只有 name/age 变化时,才重新创建 userInfo
const userInfo = useMemo(() => ({
name,
age
}), [name, age]); // 依赖:name/age 不变,userInfo 就不变
return (
<div>
<button onClick={() => setCount(count + 1)}>计数:{count}</button>
{/* 传递缓存后的 userInfo 给子组件 */}
<Child userInfo={userInfo} />
</div>
);
}
关键效果:
- 点击 "计数" 按钮时,父组件的
count变化导致父组件重渲染,但name和age没变化,userInfo从useMemo拿到缓存的旧对象(引用不变); - 子组件
Child用memo包裹,浅对比userInfo的引用没变化,所以不重新渲染,实现性能优化。
反例(不推荐):如果不用 useMemo,每次父组件重渲染都会创建新的 { name, age } 对象(引用变化),哪怕内容没变,子组件也会重新渲染,造成无效开销。
3. 防止过于频繁地触发 Effect
useEffect 会在依赖项变化时执行,如果依赖项是 "每次渲染都重新创建的对象 / 数组 / 函数",会导致 useEffect 频繁触发(哪怕内容没变)。用 useMemo 缓存依赖项,能避免这种情况。
场景 :useEffect 依赖一个对象,只有对象内容变化时才执行副作用(比如请求数据)。
import { useMemo, useState, useEffect } from 'react';
function DataFetcher() {
const [page, setPage] = useState(1);
const [limit, setLimit] = useState(10);
// 用 useMemo 缓存请求参数对象
const fetchParams = useMemo(() => ({
page,
limit
}), [page, limit]); // 仅 page/limit 变化时,才重新创建对象
// useEffect 依赖缓存后的 fetchParams
useEffect(() => {
console.log('请求数据(仅参数变化时执行)', fetchParams);
// 模拟请求数据:fetch(`/api/data?page=${page}&limit=${limit}`)
}, [fetchParams]); // 依赖项是缓存后的对象,引用稳定
return (
<div>
<button onClick={() => setPage(page + 1)}>下一页</button>
<button onClick={() => setLimit(limit + 5)}>增加每页条数</button>
</div>
);
}
关键效果:
- 只有
page或limit变化时,fetchParams才会重新创建,useEffect才会触发请求; - 如果不用
useMemo,每次组件重渲染都会创建新的{ page, limit }对象,useEffect会频繁触发,导致无效请求。
4. 记忆另一个 Hook 的依赖
很多 Hook(比如 useEffect、useCallback、useMemo 本身)都需要依赖数组,若依赖项是 "动态计算的值"(比如对象、数组),直接传入会导致依赖不稳定。用 useMemo 缓存这个依赖项,能让 Hook 正常工作。
场景 :useCallback 的依赖是一个动态计算的数组,用 useMemo 缓存数组。
import { useMemo, useCallback, useState } from 'react';
function Demo() {
const [ids, setIds] = useState([1, 2, 3]);
const [prefix, setPrefix] = useState('item_');
// 动态计算数组:给每个 id 加前缀
const prefixedIds = useMemo(() => {
return ids.map(id => `${prefix}${id}`);
}, [ids, prefix]); // 仅 ids/prefix 变化时重新计算
// useCallback 的依赖是缓存后的 prefixedIds(引用稳定)
const handleClick = useCallback(() => {
console.log('处理点击:', prefixedIds);
}, [prefixedIds]); // 依赖稳定,useCallback 不会频繁重建
return (
<div>
<button onClick={handleClick}>触发回调</button>
<button onClick={() => setPrefix('new_item_')}>修改前缀</button>
</div>
);
}
关键逻辑:
prefixedIds是动态计算的数组,用useMemo缓存后,引用稳定;useCallback依赖prefixedIds,只有prefixedIds真正变化时,handleClick才会重新创建,避免无效重渲染。
5. 记忆一个函数
虽然 useMemo 主要用于缓存 "计算结果",但也可以缓存函数(返回一个函数作为计算结果)。不过更推荐用 useCallback 缓存函数(useCallback 本质是 useMemo 的语法糖:useCallback(fn, deps) = useMemo(() => fn, deps)),仅在特殊场景下用 useMemo 记忆函数。
场景:缓存一个需要动态计算依赖的函数(比如函数内部用到动态数组)。
import { useMemo, useState } from 'react';
function Demo() {
const [list, setList] = useState([1, 2, 3]);
// 用 useMemo 缓存函数:函数内部依赖 list(动态数组)
const processList = useMemo(() => {
// 函数内部用到 list,list 变化时函数重新创建
return (factor) => {
return list.map(item => item * factor);
};
}, [list]); // 依赖 list
return (
<div>
<button onClick={() => console.log(processList(2))}>处理列表</button>
<button onClick={() => setList([4, 5, 6])}>更新列表</button>
</div>
);
}
👉 注意:
-
缓存函数优先用
useCallback,只有当函数需要 "基于动态依赖创建" 时,才考虑useMemo; -
上面的例子用
useCallback改写更简洁:const processList = useCallback((factor) => {
return list.map(item => item * factor);
}, [list]);
useRef
一、基础语法
useRef 接收 1 个参数 initialValue(初始值),返回一个不可变的 ref 对象,结构如下:
const refObj = useRef(initialValue);
核心特性:
- ref 对象的结构 :ref 对象只有一个公开属性
current,你可以通过refObj.current读取或修改存储的值(比如refObj.current = '新值'); - 跨渲染持久化 :组件每次重渲染时,
useRef返回的都是同一个 ref 对象(引用不变),current属性存储的值也会一直保留,不会被重置; - 不触发重渲染 :修改
ref.current的值不会导致组件重新渲染(这是和useState最大的区别 ------setState会触发重渲染); - 初始值可以是任意类型 :可以是 DOM 元素、数字、字符串、对象、函数等,甚至是
null(常用作 DOM 引用的初始值)。
二、核心用法
1. 使用 ref 引用一个值(跨渲染存储普通值)
当你需要存储一个 "跨组件渲染仍需保留" 的值,且修改这个值不需要触发重渲染时,用 useRef 比 useState 更合适(避免不必要的重渲染)。
常见场景:存储定时器 ID、前一次的状态 /props、临时计算结果等。
场景 1:存储定时器 ID(用于组件卸载时清除)
import { useRef, useEffect } from 'react';
function Timer() {
// 用 ref 存储定时器 ID(跨渲染保留,修改不触发重渲染)
const timerRef = useRef(null);
useEffect(() => {
// 启动定时器,将 ID 存入 ref.current
timerRef.current = setInterval(() => {
console.log('定时器运行中...');
}, 1000);
// 组件卸载时清除定时器(避免内存泄漏)
return () => clearInterval(timerRef.current);
}, []); // 空依赖:只启动一次定时器
return <div>定时器已启动(查看控制台)</div>;
}
关键逻辑:
- 定时器 ID 不需要触发组件重渲染,用
useRef存储比useState更高效; - 组件卸载时,通过
timerRef.current拿到定时器 ID,确保能正确清除。
场景 2:存储前一次的状态 /props
import { useRef, useState, useEffect } from 'react';
function Counter() {
const [count, setCount] = useState(0);
// 用 ref 存储前一次的 count(跨渲染保留)
const prevCountRef = useRef(0);
useEffect(() => {
// 每次 count 变化时,更新 ref 存储的前一次值
prevCountRef.current = count;
}, [count]); // 依赖 count:count 变化时执行
return (
<div>
<p>当前计数:{count}</p>
<p>前一次计数:{prevCountRef.current}</p>
<button onClick={() => setCount(count + 1)}>加 1</button>
</div>
);
}
关键逻辑:
prevCountRef.current存储前一次的count,修改它不会触发重渲染;- 通过
useEffect监听count变化,及时更新 ref 中的值,确保拿到正确的 "前一次状态"。
2. 通过 ref 操作 DOM(最常用场景)
useRef 最核心的用途之一是 "获取 DOM 元素的引用",从而直接操作 DOM(比如聚焦输入框、修改 DOM 样式、获取 DOM 尺寸等)------ 这是 React 中少数 "直接操作 DOM" 的合法场景。
核心流程:
- 用
useRef(null)创建 ref 对象; - 在目标 DOM 元素上添加
ref属性,值为创建的 ref 对象(React 会自动将 DOM 元素赋值给ref.current); - 在组件渲染完成后(比如
useEffect中),通过ref.current访问并操作 DOM。
场景 1:聚焦输入框(页面加载后自动聚焦)
import { useRef, useEffect } from 'react';
function InputFocus() {
// 1. 创建 ref 对象(初始值为 null)
const inputRef = useRef(null);
useEffect(() => {
// 2. 组件渲染完成后,inputRef.current 就是输入框 DOM 元素
inputRef.current.focus(); // 操作 DOM:聚焦输入框
}, []); // 空依赖:只执行一次(组件初始渲染后)
return (
// 3. 将 ref 绑定到 DOM 元素
<input ref={inputRef} placeholder="页面加载后自动聚焦..." />
);
}
场景 2:获取 DOM 元素的尺寸(比如宽度、高度)
import { useRef, useEffect, useState } from 'react';
function DOMSize() {
const [width, setWidth] = useState(0);
// 创建 ref 绑定到 div 元素
const divRef = useRef(null);
useEffect(() => {
// 组件渲染完成后,获取 DOM 尺寸
setWidth(divRef.current.offsetWidth);
// 可选:监听窗口 resize,更新尺寸
function handleResize() {
setWidth(divRef.current.offsetWidth);
}
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
return (
<div ref={divRef} style={{ width: '50%', height: '100px', background: '#f0f0f0' }}>
这个 div 的宽度是:{width}px
</div>
);
}
关键注意点:
- 必须在 "组件渲染完成后" 操作 DOM:
useEffect中执行(useEffect的回调在组件渲染到 DOM 后执行),如果直接在组件顶层访问ref.current,会得到null(此时 DOM 还没渲染); - 不要滥用 DOM 操作:React 推荐通过状态(
useState)控制 DOM,只有状态无法实现时(比如聚焦、获取尺寸),才用ref直接操作 DOM。
3. 避免重复创建 ref 的内容
如果 useRef 的初始值是 "代价昂贵的对象 / 数组 / 函数"(比如复杂对象、大数组),直接写在 initialValue 里,会导致组件每次重渲染时都重新创建这个初始值 (虽然 useRef 会忽略重渲染时的新初始值,只在第一次渲染时使用,但仍会造成不必要的性能浪费)。
解决方案:用 useMemo 或条件判断,确保初始值只创建一次。
场景:ref 初始值是复杂对象(避免重复创建)
import { useRef, useMemo } from 'react';
function ExpensiveRef() {
// 错误示例:每次重渲染都会创建新的复杂对象(浪费性能)
// const dataRef = useRef({ name: '张三', age: 25, list: Array(10000).fill(0) });
// 正确示例:用 useMemo 缓存初始值,只创建一次
const initialData = useMemo(() => {
return {
name: '张三',
age: 25,
list: Array(10000).fill(0) // 代价昂贵的大数组
};
}, []); // 空依赖:只创建一次
const dataRef = useRef(initialData);
return <div>ref 存储复杂对象</div>;
}
关键逻辑:
useMemo缓存初始值,确保复杂对象只在组件第一次渲染时创建,后续重渲染时复用;- 如果初始值是简单类型(数字、字符串、null),无需缓存,直接传入
useRef即可(比如useRef(0)、useRef(null))。