Hooks最佳实践 #
一、Hooks 规则 #
1.1 只在最顶层调用 #
jsx
// ❌ 错误:在条件语句中调用
function BadExample({ isLoading }) {
if (isLoading) {
const [data, setData] = useState(null); // 错误!
}
return <div>...</div>;
}
// ✅ 正确:在顶层调用
function GoodExample({ isLoading }) {
const [data, setData] = useState(null);
if (isLoading) {
return <Loading />;
}
return <Data data={data} />;
}
1.2 只在 Preact 函数中调用 #
jsx
// ✅ 正确:在组件中
function Component() {
const [state, setState] = useState(0);
}
// ✅ 正确:在自定义 Hook 中
function useCustomHook() {
const [state, setState] = useState(0);
}
// ❌ 错误:在普通函数中
function helper() {
const [state, setState] = useState(0); // 错误!
}
// ❌ 错误:在类组件中
class Component extends Component {
method() {
const [state, setState] = useState(0); // 错误!
}
}
二、依赖数组 #
2.1 正确设置依赖 #
jsx
// ❌ 错误:缺少依赖
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
fetchUser(userId).then(setUser);
}, []); // userId 变化时不会重新获取
return <div>{user?.name}</div>;
}
// ✅ 正确:包含所有依赖
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
fetchUser(userId).then(setUser);
}, [userId]); // userId 变化时重新获取
return <div>{user?.name}</div>;
}
2.2 依赖检查工具 #
javascript
// .eslintrc.js
module.exports = {
rules: {
'react-hooks/rules-of-hooks': 'error',
'react-hooks/exhaustive-deps': 'warn'
}
};
2.3 函数依赖 #
jsx
// ❌ 问题:函数每次渲染都重新创建
function Component({ userId }) {
const fetchData = () => {
fetch(`/api/users/${userId}`);
};
useEffect(() => {
fetchData();
}, [fetchData]); // 每次渲染都会执行
}
// ✅ 解决方案一:将函数放入 useEffect
function Component({ userId }) {
useEffect(() => {
const fetchData = () => {
fetch(`/api/users/${userId}`);
};
fetchData();
}, [userId]);
}
// ✅ 解决方案二:使用 useCallback
function Component({ userId }) {
const fetchData = useCallback(() => {
fetch(`/api/users/${userId}`);
}, [userId]);
useEffect(() => {
fetchData();
}, [fetchData]);
}
// ✅ 解决方案三:函数式更新
function Component() {
const [data, setData] = useState(null);
useEffect(() => {
fetchData().then(newData => {
setData(prev => ({ ...prev, ...newData }));
});
}, []); // 不需要依赖 setData
}
三、性能优化 #
3.1 useMemo 缓存计算 #
jsx
// ❌ 每次渲染都重新计算
function ExpensiveList({ items, filter }) {
const filteredItems = items.filter(item =>
item.name.includes(filter)
); // 每次渲染都执行
return <List items={filteredItems} />;
}
// ✅ 使用 useMemo 缓存
function ExpensiveList({ items, filter }) {
const filteredItems = useMemo(() => {
console.log('Filtering...');
return items.filter(item => item.name.includes(filter));
}, [items, filter]); // 只在依赖变化时重新计算
return <List items={filteredItems} />;
}
3.2 useCallback 缓存函数 #
jsx
// ❌ 每次渲染创建新函数
function Parent() {
const [count, setCount] = useState(0);
const handleClick = () => {
console.log('clicked');
}; // 每次渲染都是新函数
return (
<div>
<button onClick={() => setCount(c => c + 1)}>{count}</button>
<Child onClick={handleClick} />
</div>
);
}
// ✅ 使用 useCallback
function Parent() {
const [count, setCount] = useState(0);
const handleClick = useCallback(() => {
console.log('clicked');
}, []); // 函数引用保持不变
return (
<div>
<button onClick={() => setCount(c => c + 1)}>{count}</button>
<Child onClick={handleClick} />
</div>
);
}
// 配合 memo 使用
const Child = memo(function Child({ onClick }) {
console.log('Child rendered');
return <button onClick={onClick}>Click</button>;
});
3.3 避免过度优化 #
jsx
// ❌ 不必要的 useMemo
function SimpleComponent({ name }) {
const greeting = useMemo(() => `Hello, ${name}`, [name]);
return <div>{greeting}</div>;
}
// ✅ 简单计算不需要优化
function SimpleComponent({ name }) {
return <div>Hello, {name}</div>;
}
// ✅ 复杂计算才需要 useMemo
function ComplexComponent({ data }) {
const result = useMemo(() => {
return heavyComputation(data);
}, [data]);
return <div>{result}</div>;
}
四、状态管理 #
4.1 状态最小化 #
jsx
// ❌ 冗余状态
function UserForm({ user }) {
const [name, setName] = useState(user.name);
const [email, setEmail] = useState(user.email);
const [fullName, setFullName] = useState(`${user.name} (${user.email})`);
// fullName 是冗余的
return <div>{fullName}</div>;
}
// ✅ 派生状态
function UserForm({ user }) {
const [name, setName] = useState(user.name);
const [email, setEmail] = useState(user.email);
const fullName = `${name} (${email})`; // 派生
return <div>{fullName}</div>;
}
4.2 状态合并 #
jsx
// ❌ 多个相关状态
function Form() {
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const [email, setEmail] = useState('');
const [phone, setPhone] = useState('');
// ...
}
// ✅ 合并为对象
function Form() {
const [form, setForm] = useState({
firstName: '',
lastName: '',
email: '',
phone: ''
});
const updateField = (field, value) => {
setForm(prev => ({ ...prev, [field]: value }));
};
}
4.3 状态提升 #
jsx
// 当多个组件需要共享状态时,提升到共同父组件
function App() {
const [user, setUser] = useState(null);
return (
<div>
<Header user={user} onLogout={() => setUser(null)} />
<Main user={user} />
<Footer user={user} />
</div>
);
}
五、副作用处理 #
5.1 清理副作用 #
jsx
// ❌ 没有清理
function Timer() {
const [seconds, setSeconds] = useState(0);
useEffect(() => {
setInterval(() => {
setSeconds(s => s + 1);
}, 1000);
}, []); // 内存泄漏!
return <div>{seconds}</div>;
}
// ✅ 正确清理
function Timer() {
const [seconds, setSeconds] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setSeconds(s => s + 1);
}, 1000);
return () => clearInterval(interval);
}, []);
return <div>{seconds}</div>;
}
5.2 取消请求 #
jsx
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
let cancelled = false;
async function fetchUser() {
const response = await fetch(`/api/users/${userId}`);
const data = await response.json();
if (!cancelled) {
setUser(data);
}
}
fetchUser();
return () => {
cancelled = true;
};
}, [userId]);
return <div>{user?.name}</div>;
}
5.3 分离关注点 #
jsx
// ✅ 不同副作用分离到不同的 useEffect
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [windowSize, setWindowSize] = useState({ width: 0, height: 0 });
// 用户数据获取
useEffect(() => {
fetchUser(userId).then(setUser);
}, [userId]);
// 窗口大小监听
useEffect(() => {
const handleResize = () => {
setWindowSize({
width: window.innerWidth,
height: window.innerHeight
});
};
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
return <div>{/* ... */}</div>;
}
六、自定义 Hook #
6.1 提取可复用逻辑 #
jsx
// ❌ 重复逻辑
function ComponentA() {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
fetch('/api/a').then(/* ... */);
}, []);
}
function ComponentB() {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
fetch('/api/b').then(/* ... */);
}, []);
}
// ✅ 提取为自定义 Hook
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
let cancelled = false;
async function fetchData() {
try {
setLoading(true);
const response = await fetch(url);
const json = await response.json();
if (!cancelled) setData(json);
} catch (e) {
if (!cancelled) setError(e);
} finally {
if (!cancelled) setLoading(false);
}
}
fetchData();
return () => { cancelled = true; };
}, [url]);
return { data, loading, error };
}
function ComponentA() {
const { data, loading, error } = useFetch('/api/a');
}
function ComponentB() {
const { data, loading, error } = useFetch('/api/b');
}
6.2 命名约定 #
jsx
// ✅ 以 use 开头
function useWindowSize() { }
function useLocalStorage() { }
function useDebounce() { }
七、常见陷阱 #
7.1 闭包陷阱 #
jsx
// ❌ 闭包陷阱
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
console.log(count); // 永远是 0
}, 1000);
return () => clearInterval(id);
}, []);
return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}
// ✅ 使用 ref
function Counter() {
const [count, setCount] = useState(0);
const countRef = useRef(count);
useEffect(() => {
countRef.current = count;
}, [count]);
useEffect(() => {
const id = setInterval(() => {
console.log(countRef.current); // 正确
}, 1000);
return () => clearInterval(id);
}, []);
return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}
// ✅ 使用函数式更新
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setCount(c => {
console.log(c); // 正确
return c;
});
}, 1000);
return () => clearInterval(id);
}, []);
return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}
7.2 无限循环 #
jsx
// ❌ 无限循环
function BadComponent() {
const [count, setCount] = useState(0);
useEffect(() => {
setCount(count + 1); // 触发重新渲染
}, [count]); // count 变化又触发 effect
return <div>{count}</div>;
}
// ✅ 避免在 effect 中更新依赖
function GoodComponent() {
const [count, setCount] = useState(0);
useEffect(() => {
// 只在特定条件下更新
if (count < 10) {
setCount(c => c + 1);
}
}, [count]);
return <div>{count}</div>;
}
7.3 对象依赖 #
jsx
// ❌ 每次渲染都是新对象
function Component({ user }) {
useEffect(() => {
console.log('User changed');
}, [{ name: user.name }]); // 每次都是新对象
return <div>{user.name}</div>;
}
// ✅ 使用原始值
function Component({ user }) {
useEffect(() => {
console.log('User changed');
}, [user.name]); // 原始值
return <div>{user.name}</div>;
}
// ✅ 或使用 useMemo
function Component({ user }) {
const userData = useMemo(() => ({ name: user.name }), [user.name]);
useEffect(() => {
console.log('User changed');
}, [userData]);
return <div>{user.name}</div>;
}
八、总结 #
| 要点 | 说明 |
|---|---|
| 规则 | 顶层调用,只在 Preact 函数中 |
| 依赖 | 正确设置,使用 ESLint 检查 |
| 性能 | 合理使用 useMemo/useCallback |
| 副作用 | 记得清理,分离关注点 |
| 状态 | 最小化,避免冗余 |
核心原则:
- 遵循 Hooks 规则
- 正确管理依赖
- 及时清理副作用
- 提取可复用逻辑
- 避免过度优化
最后更新:2026-03-28