React 常用 Hooks 函数及使用方法完全指南(useState / useEffect / useRef / useContext / useCallback / useMemo / useReducer)
前言
React Hooks 自 React 16.8 引入以来,已经彻底改变了我们编写 React 组件的方式。Hooks 让我们在函数组件中使用状态和生命周期能力,告别了类组件的繁琐写法。本文将从最常用的几个 Hook 入手,详细介绍它们的使用方法、最佳实践和常见陷阱。
如果你刚开始接触 React Hooks,或者想系统地梳理一遍常用 Hook 的用法,这篇文章应该能帮到你。
一、useState — 组件状态管理
useState 是最基础、最常用的 Hook,用于在函数组件中声明和管理状态。
基本用法
import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>计数:{count}</p>
<button onClick={() => setCount(count + 1)}>+1</button>
</div>
);
}
useState 接收一个初始值,返回一个长度为 2 的数组:当前状态 和 更新状态的函数。
更新状态的两种方式
方式一:直接传入新值
setCount(count + 1);
方式二:传入更新函数(推荐,当新值依赖旧值时)
setCount(prev => prev + 1);
第二种方式可以避免闭包陷阱。如果你的 setCount 在异步回调中调用,使用函数式更新能确保拿到最新的状态值。
状态更新的异步性
很多人刚接触时会被这个问题困扰:setState 之后立刻读取状态,发现值没变。
const [count, setCount] = useState(0);
setCount(1);
console.log(count); // 还是 0,不是 1
这是因为 React 的状态更新是异步且批量的。实际的重新渲染会在当前事件循环结束后进行。
复杂状态:多个字段
如果状态是一个对象,更新时要手动合并:
const [form, setForm] = useState({ name: '', age: 0 });
// 错误:会丢失 name 字段
setForm({ age: 18 });
// 正确:需要手动合并
setForm(prev => ({ ...prev, age: 18 }));
💡 如果你的状态逻辑比较复杂(多个子字段、相互依赖),考虑用
useReducer替代。
二、useEffect — 副作用处理
useEffect 用于处理组件中的副作用:数据请求、DOM 操作、订阅、计时器等。
基本用法
import { useEffect } from 'react';
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(data => setUser(data));
}, [userId]); // 依赖项
return <div>{user?.name}</div>;
}
理解依赖数组
依赖数组是 useEffect 的灵魂,它决定了 effect 何时执行:
| 依赖数组 | 执行时机 |
|---|---|
undefined (不传) |
每次渲染后执行 |
[] (空数组) |
只在挂载后执行一次 |
[a, b] |
当 a 或 b 发生变化时执行 |
清理函数
如果 effect 产生了订阅、计时器等需要清理的资源,返回一个清理函数:
useEffect(() => {
const timer = setInterval(() => {
console.log('tick');
}, 1000);
return () => {
clearInterval(timer); // 组件卸载时清理
};
}, []);
这里的 return () => clearInterval(timer) 就是清理函数,会在组件卸载时执行,防止内存泄漏。
常见陷阱
陷阱一:忘记添加依赖
useEffect(() => {
fetch(`/api/users/${userId}`).then(...)
}, []); // ❌ userId 变了也不会重新请求
陷阱二:不必要的依赖导致死循环
useEffect(() => {
setCount(count + 1); // ❌ count 变化 → 重新渲染 → effect 触发 → count 变化 → 死循环
}, [count]);
陷阱三:在 effect 中使用旧值(闭包问题)
function Timer() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setCount(count + 1); // ❌ count 永远是 0
}, 1000);
return () => clearInterval(id);
}, []); // 没有依赖 count
}
解决方案:使用函数式更新 setCount(c => c + 1),或者把 count 加入依赖数组并清理/重建定时器。
useEffect 与 useLayoutEffect 的区别
-
useEffect:异步执行,在浏览器绘制之后触发。适合数据请求、事件绑定等不需要阻塞视觉更新的操作。 -
useLayoutEffect:同步执行,在 DOM 更新后、浏览器绘制前触发。适合需要读取 DOM 布局的场景(如测量元素尺寸)。
绝大多数情况下用 useEffect 就够了,只有当你遇到闪烁(flicker)问题时才考虑使用 useLayoutEffect。
三、useRef — 引用 DOM 和可变值
useRef 有两个主要用途:引用 DOM 元素 和 存储可变值(不触发重新渲染)。
引用 DOM 元素
import { useRef, useEffect } from 'react';
function AutoFocusInput() {
const inputRef = useRef(null);
useEffect(() => {
inputRef.current?.focus(); // 组件挂载后自动聚焦
}, []);
return <input ref={inputRef} />;
}
存储可变值(改变不触发重渲染)
function Timer() {
const [count, setCount] = useState(0);
const countRef = useRef(count);
// 始终保持 ref 与 state 同步
countRef.current = count;
useEffect(() => {
const id = setInterval(() => {
console.log('当前 count:', countRef.current); // 能拿到最新值
}, 1000);
return () => clearInterval(id);
}, []); // 空依赖,定时器只创建一次
}
这个模式常用于解决闭包陷阱——ref.current 永远指向最新的值,因为它是一个可变对象。
useRef vs useState 的关键区别
| 特性 | useState | useRef |
|---|---|---|
| 修改触发重渲染 | ✅ 是 | ❌ 否 |
| 跨渲染周期保存数据 | ✅ 是 | ✅ 是 |
| 在异步回调中获取最新值 | ❌ 闭包问题 | ✅ 始终最新 |
| 修改方式 | setState(newVal) |
ref.current = newVal |
四、useContext — 跨组件数据共享
useContext 让你在不使用 props 层层传递的情况下,在组件树中共享数据。
三步使用法
第一步:创建 Context
import { createContext } from 'react';
const ThemeContext = createContext('light');
第二步:使用 Provider 提供数据
function App() {
const [theme, setTheme] = useState('light');
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
<ThemedComponent />
</ThemeContext.Provider>
);
}
第三步:子组件消费数据
import { useContext } from 'react';
function ThemedComponent() {
const { theme, setTheme } = useContext(ThemeContext);
return (
<div className={theme}>
当前主题:{theme}
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
切换主题
</button>
</div>
);
}
性能注意点
当 Provider 的 value 发生变化时,所有使用 useContext 的子组件都会重新渲染。如果 value 是一个对象,每次父组件渲染都会创建新引用,导致所有消费者重渲染。
解决方案:用 useMemo 包裹 value,或者将 Context 拆分为多个(读写分离)。
// 分离读和写,避免不必要的重渲染
const ThemeContext = createContext('light');
const ThemeUpdateContext = createContext(() => {});
function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
const toggleTheme = useCallback(
() => setTheme(t => (t === 'light' ? 'dark' : 'light')),
[]
);
return (
<ThemeContext.Provider value={theme}>
<ThemeUpdateContext.Provider value={toggleTheme}>
{children}
</ThemeUpdateContext.Provider>
</ThemeContext.Provider>
);
}
五、useCallback — 缓存函数引用
useCallback 用于缓存函数的引用,避免因函数重新创建导致子组件不必要的重新渲染。
import { useCallback } from 'react';
function Parent() {
const [count, setCount] = useState(0);
// 每次 Parent 渲染都会创建新的函数引用
const handleClick = () => setCount(c => c + 1);
// useCallback 缓存函数,只有依赖变化时才重建
const handleClickCached = useCallback(
() => setCount(c => c + 1),
[]
);
return <ExpensiveChild onClick={handleClickCached} />;
}
什么时候用 useCallback
不是所有函数都需要包裹 useCallback。过度使用反而会降低可读性和性能(因为 useCallback 本身也有开销)。
适合的场景:
- 函数作为 props 传给使用了
React.memo的子组件 - 函数作为其他 Hook 的依赖项(比如 useEffect 的依赖)
- 函数在自定义 Hook 中返回给外部使用
💡 一句话法则:当你确定不缓存会导致不必要的性能问题时再用。初期先正常写函数,遇到性能瓶颈再优化。
六、useMemo — 缓存计算结果
useMemo 用于缓存复杂计算的结果,避免每次渲染都重复执行。
import { useMemo } from 'react';
function Dashboard({ transactions }) {
// 复杂计算:过滤 + 聚合
const summary = useMemo(() => {
return transactions
.filter(t => t.amount > 0)
.reduce((acc, t) => ({
total: acc.total + t.amount,
count: acc.count + 1,
avg: (acc.total + t.amount) / (acc.count + 1)
}), { total: 0, count: 0, avg: 0 });
}, [transactions]);
return <div>总金额:{summary.total},平均:{summary.avg}</div>;
}
useMemo vs useCallback
-
useCallback(fn, deps)等价于useMemo(() => fn, deps) -
useCallback缓存的是函数本身 -
useMemo缓存的是计算的结果
不要滥用 useMemo
和 useCallback 一样,useMemo 也不是免费的。简单的计算(如数组 map、filter)其开销可能还不如 useMemo 的对比开销大。
适合 useMemo 的场景:
- 计算复杂度较高(O(n²) 及以上)
- 计算结果作为 props 传给
React.memo子组件 - 计算结果作为其他 Hook 的依赖项
七、useReducer — 复杂状态管理
当状态逻辑变得复杂(多个子值、相互依赖、多层次更新),useState 就不太够用了。这时 useReducer 是更好的选择。
import { useReducer } from 'react';
// 1. 定义 reducer 函数
function todoReducer(state, action) {
switch (action.type) {
case 'ADD':
return [...state, { id: Date.now(), text: action.payload, done: false }];
case 'TOGGLE':
return state.map(t =>
t.id === action.payload ? { ...t, done: !t.done } : t
);
case 'DELETE':
return state.filter(t => t.id !== action.payload);
default:
return state;
}
}
function TodoApp() {
// 2. 使用 useReducer
const [todos, dispatch] = useReducer(todoReducer, []);
return (
<div>
<button onClick={() => dispatch({ type: 'ADD', payload: '新任务' })}>
添加
</button>
{todos.map(todo => (
<div key={todo.id}>
<span style={{ textDecoration: todo.done ? 'line-through' : 'none' }}>
{todo.text}
</span>
<button onClick={() => dispatch({ type: 'TOGGLE', payload: todo.id })}>
完成
</button>
</div>
))}
</div>
);
}
useReducer vs useState 的选择
| 场景 | 推荐 |
|---|---|
| 独立、简单的状态 | useState |
| 包含多个子值的复杂状态 | useReducer |
| 下一个状态依赖前一个 |
useReducer(或函数式 setState) |
| 更新逻辑在组件外可独立测试 | useReducer |
| 只需浅层更新表单字段 | useState |
八、自定义 Hooks — 逻辑复用
自定义 Hook 是 React Hooks 的精髓之一。当你发现多个组件中有相似的逻辑时,可以提取成一个自定义 Hook。
import { useState, useEffect } from 'react';
// 自定义 Hook:获取数据
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
let cancelled = false;
setLoading(true);
fetch(url)
.then(res => {
if (!res.ok) throw new Error('请求失败');
return res.json();
})
.then(data => {
if (!cancelled) {
setData(data);
setLoading(false);
}
})
.catch(err => {
if (!cancelled) {
setError(err);
setLoading(false);
}
});
return () => { cancelled = true; }; // 清理:防止竞态
}, [url]);
return { data, loading, error };
}
// 使用
function UserList() {
const { data, loading, error } = useFetch('/api/users');
if (loading) return <div>加载中...</div>;
if (error) return <div>出错了:{error.message}</div>;
return <ul>{data.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
}
自定义 Hook 的命名规则
- 必须以
use开头(React 官方约定,linter 也会检查) - 内部可以调用其他 Hook
- 普通函数不能调用 Hook,但自定义 Hook 可以
九、Hooks 使用规则
最后,牢记 React Hooks 的两条铁律:
规则一:只在顶层调用 Hooks
不要在循环、条件语句或嵌套函数中调用 Hook。
// ❌ 错误:在条件中调用
if (isLoading) {
useEffect(() => { ... }, []);
}
// ✅ 正确:始终在顶层
useEffect(() => {
if (!isLoading) { ... }
}, [isLoading]);
规则二:只在 React 函数中调用 Hooks
- 在函数组件中调用 ✅
- 在自定义 Hook 中调用 ✅
- 在普通函数中调用 ❌
- 在类组件中调用 ❌
- 在回调中调用 ❌
总结
本文介绍了 React 中最常用的 7 个核心 Hook:
| Hook | 用途 |
|---|---|
useState |
声明和管理组件状态 |
useEffect |
处理副作用(请求、订阅、DOM 操作) |
useRef |
引用 DOM 元素、存储可变值 |
useContext |
跨组件层级共享数据 |
useCallback |
缓存函数引用 |
useMemo |
缓存计算结果 |
useReducer |
管理复杂状态逻辑 |
记住:Hooks 是工具,不是目的。不要在不需要的地方强行使用性能优化 Hook(useCallback、useMemo),先写出清晰的代码,遇到性能问题再针对性地优化。
希望这篇文章能帮你更好地理解和运用 React Hooks。有什么问题欢迎在评论区交流讨论~