React Hooks 最佳实践指南
Created on
React Hooks 自 16.8 版本推出以来,彻底改变了 React 的开发方式。然而,Hooks 的使用门槛并不低——闭包陷阱、无限循环、不必要的重渲染……这些问题在日常开发中屡见不鲜。
本文将从错误用法 → 正确用法的对比视角,系统梳理最常用几个 Hooks 的最佳实践。
useState:状态更新的陷阱
陷阱一:直接修改状态对象
// ❌ 错误:直接修改 state,React 检测不到变化,不会触发重渲染
function BadComponent() {
const [user, setUser] = useState({ name: "Alice", age: 25 });
const handleClick = () => {
user.age = 26; // 直接修改了原对象
setUser(user); // 引用没有变,React 不会重渲染
};
}
// ✅ 正确:创建新对象
function GoodComponent() {
const [user, setUser] = useState({ name: "Alice", age: 25 });
const handleClick = () => {
setUser({ ...user, age: 26 }); // 新对象,触发重渲染
};
}
陷阱二:依赖旧状态时不用函数式更新
// ❌ 错误:在异步场景下,count 可能是旧值(闭包捕获的值)
function BadCounter() {
const [count, setCount] = useState(0);
const handleClick = () => {
setTimeout(() => {
setCount(count + 1); // count 是闭包里的旧值
}, 1000);
};
}
// ✅ 正确:函数式更新,总是基于最新的状态值
function GoodCounter() {
const [count, setCount] = useState(0);
const handleClick = () => {
setTimeout(() => {
setCount((prev) => prev + 1); // prev 始终是最新值
}, 1000);
};
}
useEffect:副作用的正确处理
陷阱一:无限循环
// ❌ 错误:每次渲染都会创建新的 options 对象,导致 useEffect 无限触发
function BadComponent({ userId }) {
const options = { method: "GET", credentials: "include" }; // 每次渲染都是新对象
useEffect(() => {
fetch(`/api/users/${userId}`, options);
}, [userId, options]); // options 每次都不同,无限循环
}
// ✅ 方案一:将对象移入 useEffect 内部
function GoodComponent({ userId }) {
useEffect(() => {
const options = { method: "GET", credentials: "include" };
fetch(`/api/users/${userId}`, options);
}, [userId]); // 只依赖 userId
}
陷阱二:忘记清理副作用
// ❌ 错误:组件卸载后定时器仍在运行,可能导致内存泄漏和警告
function BadTimer() {
const [count, setCount] = useState(0);
useEffect(() => {
const timer = setInterval(() => {
setCount((prev) => prev + 1);
}, 1000);
// 忘记返回清理函数
}, []);
}
// ✅ 正确:返回清理函数
function GoodTimer() {
const [count, setCount] = useState(0);
useEffect(() => {
const timer = setInterval(() => {
setCount((prev) => prev + 1);
}, 1000);
return () => clearInterval(timer); // 组件卸载时清理
}, []);
}
陷阱三:在 useEffect 里处理事件处理函数
// ❌ 错误:每次渲染后都重新注册监听器
function BadEventListener() {
const [mousePos, setMousePos] = useState({ x: 0, y: 0 });
useEffect(() => {
const handleMove = (e) => {
setMousePos({ x: e.clientX, y: e.clientY });
};
window.addEventListener("mousemove", handleMove);
return () => window.removeEventListener("mousemove", handleMove);
}); // ❌ 没有依赖数组,每次渲染都执行
}
// ✅ 正确:空依赖数组,只挂载一次
function GoodEventListener() {
const [mousePos, setMousePos] = useState({ x: 0, y: 0 });
useEffect(() => {
const handleMove = (e) => {
setMousePos({ x: e.clientX, y: e.clientY });
};
window.addEventListener("mousemove", handleMove);
return () => window.removeEventListener("mousemove", handleMove);
}, []); // ✅ 空数组,只在挂载/卸载时执行
}
useMemo:性能优化,但别滥用
useMemo 用于缓存计算结果,只有依赖变化时才重新计算。
// ❌ 不必要的 useMemo:简单计算不值得缓存,反而增加开销
function BadComponent({ items }) {
const count = useMemo(() => items.length, [items]); // 完全没必要
}
// ❌ 错误:在 useMemo 内部有副作用
function BadComponent2({ data }) {
const result = useMemo(() => {
console.log("计算中..."); // ❌ 副作用不该放在 useMemo 里
return data.filter((item) => item.active);
}, [data]);
}
// ✅ 正确:只对真正昂贵的计算使用 useMemo
function GoodComponent({ orders }) {
// 假设这是一个需要遍历大量数据的聚合计算
const statistics = useMemo(() => {
return orders.reduce(
(acc, order) => {
acc.total += order.amount;
acc.count += 1;
if (order.status === "completed") acc.completed += 1;
return acc;
},
{ total: 0, count: 0, completed: 0 }
);
}, [orders]);
return (
<div>
总金额:{statistics.total},完成率:
{statistics.completed / statistics.count}
</div>
);
}
useCallback:稳定函数引用
useCallback 用于缓存函数引用,主要用于:
- 传给
React.memo包裹的子组件,避免子组件不必要的重渲染 - 作为其他 Hook 的依赖项时保持稳定
// ❌ 问题:每次父组件渲染,onDelete 都是新函数,导致 ListItem 重渲染
const ListItem = React.memo(({ item, onDelete }) => {
console.log(`渲染 ${item.name}`);
return <button onClick={() => onDelete(item.id)}>{item.name}</button>;
});
function BadList({ items }) {
const [filter, setFilter] = useState("");
// 每次渲染都创建新函数
const handleDelete = (id) => {
console.log("删除", id);
};
return (
<>
<input value={filter} onChange={(e) => setFilter(e.target.value)} />
{items.map((item) => (
<ListItem key={item.id} item={item} onDelete={handleDelete} />
))}
</>
);
}
// ✅ 正确:useCallback 保持函数引用稳定,filter 改变时 ListItem 不会重渲染
function GoodList({ items }) {
const [filter, setFilter] = useState("");
const handleDelete = useCallback((id) => {
console.log("删除", id);
}, []); // 依赖为空,函数永远不变
return (
<>
<input value={filter} onChange={(e) => setFilter(e.target.value)} />
{items.map((item) => (
<ListItem key={item.id} item={item} onDelete={handleDelete} />
))}
</>
);
}
useRef:不只是操作 DOM
useRef 除了用于获取 DOM 元素,还有一个重要用途:保存不需要触发重渲染的可变值。
// 场景一:操作 DOM(最常见用法)
function VideoPlayer() {
const videoRef = useRef(null);
const handlePlay = () => {
videoRef.current?.play();
};
return (
<>
<video ref={videoRef} src="/video.mp4" />
<button onClick={handlePlay}>播放</button>
</>
);
}
// 场景二:保存定时器 ID,用于后续清理
function Debounced({ onSearch }) {
const timerRef = useRef(null);
const handleChange = (e) => {
clearTimeout(timerRef.current);
timerRef.current = setTimeout(() => {
onSearch(e.target.value);
}, 300);
};
return <input onChange={handleChange} placeholder="搜索..." />;
}
// 场景三:记录上一次的 props/state 值
function usePreivous(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
});
return ref.current; // 返回上一次渲染时的值
}
function Component({ count }) {
const prevCount = usePreivous(count);
return (
<p>
当前:{count},上一次:{prevCount}
</p>
);
}
封装自定义 Hook
自定义 Hook 是 React 中复用逻辑的最佳方式。遵循 use 前缀命名规范,可以将复杂的状态逻辑封装成可读性高、可复用的单元。
示例一:useLocalStorage
function useLocalStorage(key, initialValue) {
const [storedValue, setStoredValue] = useState(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
return initialValue;
}
});
const setValue = useCallback(
(value) => {
try {
const valueToStore =
value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
window.localStorage.setItem(key, JSON.stringify(valueToStore));
} catch (error) {
console.error(error);
}
},
[key, storedValue]
);
return [storedValue, setValue];
}
// 使用方式和 useState 完全一致
function Settings() {
const [theme, setTheme] = useLocalStorage("theme", "light");
return (
<button onClick={() => setTheme((t) => (t === "light" ? "dark" : "light"))}>
切换主题:{theme}
</button>
);
}
示例二:useFetch
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
let cancelled = false; // 防止组件卸载后 setState
setLoading(true);
setError(null);
fetch(url)
.then((res) => {
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
})
.then((data) => {
if (!cancelled) setData(data);
})
.catch((err) => {
if (!cancelled) setError(err.message);
})
.finally(() => {
if (!cancelled) setLoading(false);
});
return () => {
cancelled = true; // 组件卸载时取消更新
};
}, [url]);
return { data, loading, error };
}
// 使用
function UserProfile({ userId }) {
const { data, loading, error } = useFetch(`/api/users/${userId}`);
if (loading) return <p>加载中...</p>;
if (error) return <p>出错了:{error}</p>;
return <p>{data?.name}</p>;
}
总结
| Hook | 核心用途 | 常见陷阱 |
|---|---|---|
useState | 管理组件状态 | 直接修改对象、没用函数式更新 |
useEffect | 处理副作用 | 无限循环、忘记清理 |
useMemo | 缓存计算结果 | 滥用导致反向优化 |
useCallback | 稳定函数引用 | 在无需优化的场景滥用 |
useRef | DOM 引用 / 存储可变值 | 误用 ref 替代 state |
记住一个原则:Hooks 的优化(useMemo/useCallback)应该在发现性能问题后才加入,而不是在每个地方都预先使用。过早优化会让代码变复杂而收益甚微。