React Hooks 使用指南:完整示例教程
2019年初,React 16.8 引入 Hooks 后,彻底改变了开发者构建 React 应用的方式——函数组件无需编写类组件即可管理状态、副作用和上下文。掌握 React Hooks 的正确用法 已成为现代 React 开发者的必备技能,因为 Hooks 已成为"大多数 React 代码的唯一编写方式"。本指南将带你了解核心内置 Hooks,解释其使用规则,并提供可直接上手的实用示例。
你将学到什么
学完本指南后,你将能自信地在项目中使用 React 核心 Hooks——useState、useEffect、useContext、useRef、useReducer、useMemo 和 useCallback。你将理解何时选择特定 Hook,如何避免不必要的重渲染等常见陷阱,以及如何将可复用逻辑提取为自定义 Hooks。最重要的启示是:Hooks 本质上就是函数——学会组合它们才是掌握 React 的关键。
理解 Hooks 的使用规则
在深入单个 Hooks 之前,你需要了解由 eslint-plugin-react-hooks 包强制执行的两条不可协商规则:
只在顶层调用 Hooks。切勿在循环、条件语句或嵌套函数中调用 Hooks。React 依赖 Hooks 的调用顺序来保持渲染间的状态——如果顺序改变,React 将无法正确关联状态与 Hook。
只从 React 函数中调用 Hooks。你只能从函数组件或其他自定义 Hooks 中调用 Hooks。普通 JavaScript 函数不能调用
useState或useEffect。
第二条规则衍生出一个约定:任何调用 Hooks 的函数必须以 use 开头(例如 useCustomHook)。lint 工具利用这个前缀来正确执行规则。
日常最常用的核心 Hooks
useState:管理组件状态
useState 是最基础的 Hook,允许你为函数组件添加状态。它返回一个包含两个元素的数组:当前状态值和更新状态的函数。
import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0); // 初始状态为 0
return (
<button onClick={() => setCount(count + 1)}>
Clicked {count} times
</button>
);
}
当基于前一个值更新状态时,使用函数式更新形式可避免竞态条件,尤其是在 React 自动批处理场景下:
// 安全:使用前一个状态值
setCount(prevCount => prevCount + 1);
// 对象更新同样安全:展开前一个状态
setUser(prevUser => ({ ...prevUser, name: 'Jane' }));
如果计算初始状态开销较大,传入函数而非值——React 只会在初始渲染时执行它:
const [state, setState] = useState(() => {
const initialState = someExpensiveComputation(props);
return initialState;
});
useEffect:处理副作用
useEffect 让你在函数组件中执行副作用,例如获取数据、设置订阅或手动修改 DOM。默认情况下,副作用在每次渲染完成后执行,但你可以通过依赖数组控制其触发时机。
import { useEffect, useState } from 'react';
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
// 该副作用在组件挂载后以及 userId 变化时执行
fetchUser(userId).then(data => setUser(data));
// 可选的清理函数:在下一次副作用执行前和组件卸载时运行
return () => {
// 取消待处理的请求或清理订阅
};
}, [userId]); // 依赖数组:仅在 userId 变化时重新执行
return <div>{user?.name}</div>;
}
useEffect 的关键模式:
- 空依赖数组
[]:副作用仅在初始渲染(挂载)后执行一次。 - 无依赖数组:副作用在每次渲染后执行(很少需要)。
- 清理函数:通过清理订阅、定时器或待处理的网络请求防止内存泄漏。
重要提醒:并非每次状态变化都需要副作用。如果能在渲染期间直接计算值,应直接计算而非使用 useEffect:
// ❌ 不必要的副作用
const [fullName, setFullName] = useState('');
useEffect(() => {
setFullName(`${firstName} ${lastName}`);
}, [firstName, lastName]);
// ✅ 在渲染期间直接计算
const fullName = `${firstName} ${lastName}`;
useContext:无需属性逐层传递即可消费上下文
useContext 让你无需将组件包裹在 Consumer 组件中即可读取 React Context 的值:
import { useContext } from 'react';
// 创建上下文
const ThemeContext = React.createContext('light');
function ThemedButton() {
// 直接读取上下文值
const theme = useContext(ThemeContext);
return <button className={theme}>Click me</button>;
}
当上下文提供者的值发生变化时,任何使用 useContext 的组件都会自动重新渲染。
useRef:引用不触发重渲染的值
useRef 创建一个可变的 ref 对象,其 .current 属性在渲染间持久存在。与状态不同,更新 ref 不会导致重渲染。这使得 ref 非常适合:
- 存储 DOM 元素引用
- 跟踪定时器 ID
- 持久化不应触发 UI 更新的值
import { useRef, useEffect } from 'react';
function TextInput() {
const inputRef = useRef(null);
useEffect(() => {
// 组件挂载后聚焦输入框
inputRef.current.focus();
}, []);
return <input ref={inputRef} type="text" />;
}
useReducer:管理复杂状态逻辑
对于涉及多个子值或复杂转换的状态,useReducer 通常比 useState 更合适。它接受一个 reducer 函数和初始状态,返回当前状态和一个 dispatch 函数:
import { useReducer } from 'react';
const initialState = { count: 0 };
function reducer(state, action) {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
case 'reset':
return { count: action.payload };
default:
return state;
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
Count: {state.count}
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
<button onClick={() => dispatch({ type: 'decrement' })}>-</button>
<button onClick={() => dispatch({ type: 'reset', payload: 0 })}>Reset</button>
</>
);
}
性能优化 Hooks
useMemo:缓存昂贵计算
useMemo 缓存计算结果,仅在依赖变化时重新计算:
import { useMemo } from 'react';
function TodoList({ todos, filter }) {
// 仅在 todos 或 filter 变化时重新计算
const filteredTodos = useMemo(() => {
return todos.filter(todo => todo.status === filter);
}, [todos, filter]);
return (
<ul>
{filteredTodos.map(todo => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
);
}
useCallback:记忆化函数引用
useCallback 返回一个记忆化的回调函数版本,仅在依赖变化时更新。这在将回调传递给依赖引用相等性来避免不必要重渲染的优化子组件时特别有用:
import { useCallback } from 'react';
function ParentComponent() {
const handleClick = useCallback(() => {
console.log('Button clicked');
}, []); // 函数引用在渲染间保持稳定
return <ChildButton onClick={handleClick} />;
}
自定义 Hooks:跨组件复用逻辑
当你创建自定义 Hooks 来提取和复用状态逻辑时,Hooks 的真正威力才得以展现。自定义 Hook 就是一个名称以 use 开头的 JavaScript 函数,它可以调用其他 Hooks。
示例:useLocalStorage
这个自定义 Hook 将状态与 localStorage 同步,在页面刷新时持久化数据:
import { useState, useEffect } from 'react';
function useLocalStorage(key, initialValue) {
const [value, setValue] = useState(() => {
if (typeof window === 'undefined') return initialValue;
const stored = localStorage.getItem(key);
return stored !== null ? JSON.parse(stored) : initialValue;
});
useEffect(() => {
localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
return [value, setValue];
}
// 使用示例
function ThemeToggle() {
const [theme, setTheme] = useLocalStorage('theme', 'light');
return (
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
Switch to {theme === 'light' ? 'dark' : 'light'} mode
</button>
);
}
示例:useMediaQuery
这个 Hook 让你根据 CSS 媒体查询(如用户对减少动画的偏好)条件渲染组件:
import { useState, useEffect } from 'react';
function useMediaQuery(query) {
const [matches, setMatches] = useState(() => {
if (typeof window === 'undefined') return false;
return window.matchMedia(query).matches;
});
useEffect(() => {
const mediaQueryList = window.matchMedia(query);
const onChange = (event) => setMatches(event.matches);
mediaQueryList.addEventListener('change', onChange);
return () => mediaQueryList.removeEventListener('change', onChange);
}, [query]);
return matches;
}
// 使用示例
function MotionAwareComponent() {
const prefersReducedMotion = useMediaQuery('(prefers-reduced-motion: reduce)');
return prefersReducedMotion ? <StaticHero /> : <AnimatedHero />;
}
React 19:表单与异步数据的新 Hooks
React 19(2024年12月发布)引入了几个旨在简化表单处理和异步操作的新 Hooks。
useActionState:简化表单提交
useActionState 管理表单提交状态,返回最新结果、操作函数和待处理标志:
import { useActionState } from 'react';
async function subscribeAction(prevState, formData) {
const email = formData.get('email');
try {
await subscribe(email);
return { ok: true };
} catch (error) {
return { ok: false, error: error.message };
}
}
function NewsletterForm() {
const [state, formAction, isPending] = useActionState(
subscribeAction,
{ ok: false }
);
return (
<form action={formAction}>
<input name="email" type="email" required />
<button type="submit" disabled={isPending}>
{isPending ? 'Subscribing...' : 'Subscribe'}
</button>
{state.ok && <p>You're subscribed!</p>}
{state.error && <p style={{ color: 'red' }}>{state.error}</p>}
</form>
);
}
useFormStatus:访问父表单状态
useFormStatus 允许嵌套组件了解其父表单是否正在提交:
import { useFormStatus } from 'react';
function SubmitButton() {
const { pending } = useFormStatus();
return (
<button type="submit" disabled={pending}>
{pending ? 'Saving...' : 'Save'}
</button>
);
}
与 Redux 集成:useSelector Hook
当在 React 中使用 Redux 时,useSelector Hook 是访问 Redux store 状态的现代推荐方式。它取代了旧的 connect 高阶组件,并且与 TypeScript 配合更好。
import { useSelector, useDispatch } from 'react-redux';
function CounterComponent() {
// 从 Redux store 中提取特定数据
const count = useSelector((state) => state.counter.value);
const dispatch = useDispatch();
return (
<div>
<span>{count}</span>
<button onClick={() => dispatch(increment())}>Increment</button>
</div>
);
}
重要提示:useSelector 默认使用严格的 === 引用相等性。每次返回新对象都会强制重渲染。使用记忆化选择器或为单个字段多次调用 useSelector 来优化性能:
// ❌ 这会在每次操作时导致重渲染
const { count, user } = useSelector((state) => ({
count: state.count,
user: state.user
}));
// ✅ 为每个字段单独调用 useSelector
const count = useSelector((state) => state.count);
const user = useSelector((state) => state.user);
来源
- React 官方文档:内置 React Hooks
- React 官方文档:Hooks API 参考
- React Redux 文档:Hooks API
- Patterns.dev:Hooks 模式
- React 官方文档:使用 State Hook
— Editorial Team
暂无评论。