React Hooks 最佳实践
深入理解 React Hooks,避免常见陷阱,写出更优雅的代码。
引言
React Hooks 自 2019 年发布以来,彻底改变了 React 组件的编写方式。函数组件配合 Hooks 已经成为 React 开发的主流范式。然而,Hooks 看似简洁的 API 背后,隐藏着不少容易踩坑的细节。本文将从实际项目经验出发,系统梳理 React Hooks 的最佳实践,帮助你写出更可靠、更优雅的代码。
useState vs useReducer:如何选择状态管理方案
useState 和 useReducer 都用于管理组件状态,但它们适用于不同的场景。选择正确的工具可以大幅提升代码的可维护性。
useState 适用场景
当状态逻辑简单、状态之间相互独立时,useState 是最佳选择:
function SearchBar() {
const [query, setQuery] = useState('');
const [isFocused, setIsFocused] = useState(false);
return (
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
onFocus={() => setIsFocused(true)}
onBlur={() => setIsFocused(false)}
className={isFocused ? 'ring-2 ring-cyan-400' : ''}
/>
);
}
useReducer 适用场景
当状态之间存在复杂关联、状态更新逻辑需要集中管理时,useReducer 更加合适。它通过将状态更新逻辑抽离到 reducer 函数中,使组件逻辑更加清晰:
interface FormState {
values: Record<string, string>;
errors: Record<string, string>;
isSubmitting: boolean;
isValid: boolean;
}
type FormAction =
| { type: 'SET_FIELD'; field: string; value: string }
| { type: 'SET_ERROR'; field: string; error: string }
| { type: 'SUBMIT_START' }
| { type: 'SUBMIT_SUCCESS' }
| { type: 'SUBMIT_FAILURE'; errors: Record<string, string> }
| { type: 'RESET' };
function formReducer(state: FormState, action: FormAction): FormState {
switch (action.type) {
case 'SET_FIELD':
return {
...state,
values: { ...state.values, [action.field]: action.value },
errors: { ...state.errors, [action.field]: '' },
isValid: true, // 清除错误后重新验证
};
case 'SUBMIT_START':
return { ...state, isSubmitting: true };
case 'SUBMIT_SUCCESS':
return { ...state, isSubmitting: false, values: {}, errors: {} };
case 'SUBMIT_FAILURE':
return { ...state, isSubmitting: false, errors: action.errors };
case 'RESET':
return initialState;
default:
return state;
}
}
const initialState: FormState = {
values: {},
errors: {},
isSubmitting: false,
isValid: true,
};
function ComplexForm() {
const [state, dispatch] = useReducer(formReducer, initialState);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
dispatch({ type: 'SUBMIT_START' });
try {
await submitForm(state.values);
dispatch({ type: 'SUBMIT_SUCCESS' });
} catch (err) {
dispatch({ type: 'SUBMIT_FAILURE', errors: (err as Error).message });
}
};
// ...
}
一个简单的判断标准:如果你的组件中有 3 个以上相关的 useState,且它们的更新经常一起发生,那么考虑使用 useReducer 来替代。
useEffect 清理模式
useEffect 的清理函数是 Hooks 中最容易被忽视但极其重要的部分。不正确的清理会导致内存泄漏、竞态条件和状态更新已卸载组件等问题。
基本清理模式
每个 useEffect 都应该清理它创建的"副作用"。以下是几种常见的清理场景:
// 1. 清理定时器
useEffect(() => {
const timer = setInterval(() => {
fetchLatestData();
}, 5000);
return () => clearInterval(timer); // 组件卸载时清除定时器
}, []);
// 2. 清理事件监听器
useEffect(() => {
const handleResize = () => setWidth(window.innerWidth);
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
// 3. 清理 AbortController(防止竞态条件)
useEffect(() => {
const controller = new AbortController();
async function fetchData() {
try {
const res = await fetch('/api/data', {
signal: controller.signal,
});
const data = await res.json();
setData(data);
} catch (err) {
if ((err as Error).name !== 'AbortError') {
setError(err as Error);
}
}
}
fetchData();
return () => controller.abort(); // 依赖变化或卸载时取消请求
}, [endpoint]);
竞态条件处理
当 useEffect 的依赖发生变化时,旧的异步操作可能在新操作之后才返回结果。使用 AbortController 或标志变量来解决这个问题:
function UserProfile({ userId }: { userId: string }) {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
let cancelled = false;
setLoading(true);
fetchUser(userId).then((data) => {
if (!cancelled) {
setUser(data);
setLoading(false);
}
});
return () => {
cancelled = true; // 防止过期数据覆盖新数据
};
}, [userId]);
if (loading) return <UserSkeleton />;
return <UserCard user={user!} />;
}
自定义 Hooks:复用逻辑的利器
自定义 Hooks 是 React 中复用逻辑的最佳方式。通过将通用的状态逻辑抽取为自定义 Hook,可以大幅减少代码重复。
useLocalStorage
一个实用的自定义 Hook,将 localStorage 读写封装为响应式状态:
function useLocalStorage<T>(key: string, initialValue: T) {
const [storedValue, setStoredValue] = useState<T>(() => {
if (typeof window === 'undefined') return initialValue;
try {
const item = window.localStorage.getItem(key);
return item ? (JSON.parse(item) as T) : initialValue;
} catch {
return initialValue;
}
});
const setValue = useCallback(
(value: T | ((val: T) => T)) => {
const valueToStore =
value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
window.localStorage.setItem(key, JSON.stringify(valueToStore));
},
[key, storedValue]
);
return [storedValue, setValue] as const;
}
// 使用示例
function ThemeSettings() {
const [theme, setTheme] = useLocalStorage('theme', 'dark');
return (
<select value={theme} onChange={(e) => setTheme(e.target.value)}>
<option value="dark">暗色模式</option>
<option value="light">亮色模式</option>
</select>
);
}
useDebounce
另一个常用的自定义 Hook,用于防抖处理高频触发的值变化:
function useDebounce<T>(value: T, delay: number): T {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const timer = setTimeout(() => setDebouncedValue(value), delay);
return () => clearTimeout(timer);
}, [value, delay]);
return debouncedValue;
}
// 使用示例:搜索输入防抖
function SearchComponent() {
const [query, setQuery] = useState('');
const debouncedQuery = useDebounce(query, 300);
useEffect(() => {
if (debouncedQuery) {
searchAPI(debouncedQuery).then(setResults);
}
}, [debouncedQuery]);
return <input value={query} onChange={(e) => setQuery(e.target.value)} />;
}
性能优化 Hooks:useMemo 和 useCallback
useMemo 和 useCallback 是 React 提供的两个性能优化 Hooks。但过度使用它们反而会导致性能下降(因为记忆化本身也有成本)。理解何时使用它们至关重要。
useMemo
useMemo 用于缓存计算结果,避免每次渲染都重新执行昂贵的计算:
function ProductList({ products, filter }: Props) {
// 只在 products 或 filter 变化时重新计算
const filteredProducts = useMemo(() => {
return products
.filter((p) => p.category === filter)
.sort((a, b) => b.rating - a.rating);
}, [products, filter]);
return (
<ul>
{filteredProducts.map((product) => (
<ProductItem key={product.id} product={product} />
))}
</ul>
);
}
useCallback
useCallback 用于缓存函数引用,常与 React.memo 配合使用,避免子组件不必要的重新渲染:
const TodoList = React.memo(function TodoList({
todos,
onToggle,
}: {
todos: Todo[];
onToggle: (id: string) => void;
}) {
return (
<ul>
{todos.map((todo) => (
<li key={todo.id} onClick={() => onToggle(todo.id)}>
{todo.text}
</li>
))}
</ul>
);
});
function App() {
const [todos, setTodos] = useState<Todo[]>([]);
// 使用 useCallback 保持函数引用稳定
const handleToggle = useCallback((id: string) => {
setTodos((prev) =>
prev.map((todo) =>
todo.id === id ? { ...todo, done: !todo.done } : todo
)
);
}, []); // 空依赖数组,因为使用了函数式更新
return <TodoList todos={todos} onToggle={handleToggle} />;
}
何时不需要优化
以下场景不需要使用 useMemo 或 useCallback:
- 简单的计算(如字符串拼接、数字加减):记忆化的开销大于计算本身
- 不会作为 props 传递给子组件的函数:没有子组件重新渲染的问题
- 原始类型的值(字符串、数字、布尔值):React 已经对它们做了优化
总结
React Hooks 的最佳实践可以归纳为以下几点:
- 选择正确的状态管理工具:简单状态用
useState,复杂关联状态用useReducer - 始终正确处理 useEffect 清理:每个副作用都应该有对应的清理逻辑
- 善用自定义 Hooks:将可复用的状态逻辑抽取为自定义 Hook
- 合理使用性能优化 Hooks:先测量,再优化,不要过早使用
useMemo和useCallback
掌握这些最佳实践,你将能够编写出更加健壮、可维护和高效的 React 代码。在后续的文章中,我将继续深入探讨 React 18 的并发特性以及 React Server Components 等前沿话题。