为什么使用 Hooks
React Hooks 是 React 16.8 引入的新特性,它让我们在函数组件中也能使用状态和其他 React 特性。在 Hooks 出现之前,如果组件需要管理状态或执行副作用,就必须使用类组件。Hooks 的出现使得函数组件变得更加强大,同时也简化了组件间的逻辑复用。本文将重点介绍四个最常用的 Hook:useState、useEffect、useCallback 和 useMemo。
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>
<button onClick={() => setCount(prev => prev - 1)}>-1</button>
</div>
);
}
// 使用函数初始化(适用于计算开销较大的初始值)
function ExpensiveComponent() {
const [data, setData] = useState(() => {
return computeExpensiveInitialValue();
});
return <div>{data}</div>;
}
// 管理对象状态
function UserForm() {
const [form, setForm] = useState({
name: '',
email: '',
age: 0,
});
const handleChange = (field: string, value: string | number) => {
setForm(prev => ({ ...prev, [field]: value }));
};
return (
<form>
<input
value={form.name}
onChange={e => handleChange('name', e.target.value)}
placeholder="姓名"
/>
<input
value={form.email}
onChange={e => handleChange('email', e.target.value)}
placeholder="邮箱"
/>
</form>
);
}
需要注意的是,useState 的更新函数在处理对象或数组时,必须创建新的引用而非直接修改原有数据,因为 React 通过引用比较来判断状态是否变化。
useEffect:副作用处理
useEffect 用于处理组件中的副作用操作,比如数据获取、订阅、手动修改 DOM 等。它相当于类组件中 componentDidMount、componentDidUpdate 和 componentWillUnmount 的组合。
import { useState, useEffect } from 'react';
function UserProfile({ userId }: { userId: number }) {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
let cancelled = false;
async function fetchUser() {
setLoading(true);
try {
const response = await fetch(`/api/users/${userId}`);
const data = await response.json();
if (!cancelled) {
setUser(data);
}
} catch (error) {
console.error('获取用户信息失败:', error);
} finally {
if (!cancelled) {
setLoading(false);
}
}
}
fetchUser();
// 清除函数:组件卸载或 userId 变化时执行
return () => {
cancelled = true;
};
}, [userId]); // 依赖数组:仅在 userId 变化时重新执行
if (loading) return <p>加载中...</p>;
if (!user) return <p>用户不存在</p>;
return <div>{user.name}</div>;
}
// 事件监听的副作用
function WindowSize() {
const [size, setSize] = useState({
width: window.innerWidth,
height: window.innerHeight,
});
useEffect(() => {
const handleResize = () => {
setSize({
width: window.innerWidth,
height: window.innerHeight,
});
};
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []); // 空依赖数组:仅在挂载和卸载时执行
return <p>窗口大小:{size.width} x {size.height}</p>;
}
useEffect 的依赖数组非常重要:传入空数组表示仅在组件挂载时执行一次;传入具体依赖则在依赖变化时重新执行;不传依赖数组则每次渲染后都会执行。合理管理依赖数组是避免无限循环和性能问题的关键。
useCallback:函数缓存
useCallback 用于缓存函数引用,避免在每次渲染时创建新的函数实例。当回调函数作为 prop 传递给子组件时,配合 React.memo 可以有效减少不必要的重渲染。
import { useState, useCallback, memo } from 'react';
// 子组件使用 memo 包裹
const TodoItem = memo(function TodoItem({
todo,
onToggle,
}: {
todo: { id: number; text: string; done: boolean };
onToggle: (id: number) => void;
}) {
console.log(`TodoItem ${todo.id} 渲染`);
return (
<li onClick={() => onToggle(todo.id)}>
{todo.done ? '✅' : '⬜'} {todo.text}
</li>
);
});
function TodoList() {
const [todos, setTodos] = useState([
{ id: 1, text: '学习 React', done: false },
{ id: 2, text: '写技术博客', done: false },
]);
// 使用 useCallback 缓存函数
const handleToggle = useCallback((id: number) => {
setTodos(prev =>
prev.map(todo =>
todo.id === id ? { ...todo, done: !todo.done } : todo
)
);
}, []); // 依赖为空,函数引用始终不变
return (
<ul>
{todos.map(todo => (
<TodoItem key={todo.id} todo={todo} onToggle={handleToggle} />
))}
</ul>
);
}
useMemo:计算结果缓存
useMemo 用于缓存计算结果,避免在每次渲染时重复执行开销较大的计算。它接收一个工厂函数和依赖数组,只有当依赖发生变化时才会重新计算。
import { useState, useMemo } from 'react';
function ProductList({ products }: { products: Product[] }) {
const [filter, setFilter] = useState('');
const [sortBy, setSortBy] = useState<'name' | 'price'>('name');
// 使用 useMemo 缓存过滤和排序的结果
const filteredAndSorted = useMemo(() => {
console.log('重新计算过滤结果');
return products
.filter(p => p.name.includes(filter))
.sort((a, b) => {
if (sortBy === 'name') return a.name.localeCompare(b.name);
return a.price - b.price;
});
}, [products, filter, sortBy]);
return (
<div>
<input
value={filter}
onChange={e => setFilter(e.target.value)}
placeholder="搜索商品"
/>
<select
value={sortBy}
onChange={e => setSortBy(e.target.value as 'name' | 'price')}
>
<option value="name">按名称排序</option>
<option value="price">按价格排序</option>
</select>
<ul>
{filteredAndSorted.map(p => (
<li key={p.id}>{p.name} - ¥{p.price}</li>
))}
</ul>
</div>
);
}
使用建议
最后总结几条使用 Hooks 的建议:
- 不要过度优化:不是所有函数都需要 useCallback,不是所有计算都需要 useMemo。只在确实存在性能问题时才使用。
- 保持 Hook 调用顺序一致:不要在条件语句或循环中调用 Hook,确保每次渲染时 Hook 的调用顺序相同。
- 合理拆分 useEffect:如果一个 useEffect 中处理了多个不相关的副作用,应该拆分成多个 useEffect。
- 自定义 Hook 复用逻辑:当多个组件之间存在相似的状态逻辑时,可以将其提取为自定义 Hook。
掌握好这几个核心 Hooks,就能应对大部分 React 开发场景。随着项目经验的积累,可以进一步学习 useReducer、useContext、useRef 等其他 Hooks,以及自定义 Hook 的设计模式。