引言
自React 16.8引入Hooks以来,函数组件已经成为React开发的主流方式。Hooks让代码更简洁、逻辑复用更优雅,但使用不当也会带来性能问题和难以追踪的bug。本文将深入解析常用Hooks的原理,分享实战中的最佳实践和常见陷阱。
一、useState:状态管理的基石
1.1 基本用法
import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
点击次数:{count}
</button>
);
}
1.2 函数式更新(推荐)
当新状态依赖于旧状态时,使用函数式更新可以避免闭包陷阱:
// ❌ 可能有问题的写法
const handleClick = () => {
setCount(count + 1);
setCount(count + 1); // 这里的count还是旧值
};
// ✓ 推荐:函数式更新
const handleClick = () => {
setCount(prev => prev + 1);
setCount(prev => prev + 1); // 正确累加两次
};
1.3 状态初始化优化
// ❌ 每次渲染都执行expensiveCalculation
const [state, setState] = useState(expensiveCalculation());
// ✓ 推荐:惰性初始化(只执行一次)
const [state, setState] = useState(() => expensiveCalculation());
二、useEffect:副作用处理
2.1 依赖数组的正确使用
import { useEffect, useState } from 'react';
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
// ✓ 正确:userId在依赖数组中
useEffect(() => {
fetchUser(userId).then(setUser);
}, [userId]);
// ❌ 错误:缺少依赖,可能获取到旧数据
useEffect(() => {
fetchUser(userId).then(setUser);
}, []); // userId变化时不会重新执行
}
2.2 清理函数(Cleanup)
useEffect(() => {
const subscription = api.subscribe(handleData);
// 清理函数:组件卸载或依赖变化时执行
return () => {
subscription.unsubscribe();
};
}, [dependencies]);
2.3 避免无限循环
// ❌ 无限循环:对象引用每次都是新的
const [options, setOptions] = useState({ page: 1 });
useEffect(() => {
fetchData(options);
setOptions({ page: options.page + 1 }); // 新对象,触发重新渲染
}, [options]);
// ✓ 优化:使用状态更新函数
useEffect(() => {
fetchData(options);
}, [options]);
const loadMore = () => {
setOptions(prev => ({ ...prev, page: prev.page + 1 }));
};
三、useCallback: memoized函数
3.1 使用场景
useCallback用于缓存函数引用,主要在以下场景使用:
- 传递给
React.memo优化的子组件 - 作为
useEffect的依赖 - 作为其他Hooks的依赖
import { useCallback, useState } from 'react';
function Parent() {
const [count, setCount] = useState(0);
// ✓ 缓存函数引用,避免子组件不必要的重渲染
const handleClick = useCallback(() => {
console.log('clicked');
}, []);
return (
<>
<Child onClick={handleClick} />
<button onClick={() => setCount(c => c + 1)}>Count: {count}</button>
</>
);
}
// 子组件使用React.memo优化
const Child = React.memo(({ onClick }) => {
console.log('Child rendered');
return <button onClick={onClick}>Click me</button>;
});
3.2 常见误区
// ❌ 过度使用useCallback(增加复杂度,收益小)
const handleChange = useCallback((e) => {
setValue(e.target.value);
}, []);
// ✓ 普通函数即可(除非有特定优化需求)
const handleChange = (e) => {
setValue(e.target.value);
};
四、useMemo:缓存计算结果
4.1 性能优化场景
import { useMemo, useState } from 'react';
function ProductList({ products, filter }) {
const [search, setSearch] = useState('');
// ✓ 缓存昂贵的过滤和排序操作
const filteredProducts = useMemo(() => {
console.log('过滤和排序执行');
return products
.filter(p => p.name.includes(search))
.filter(p => p.category === filter)
.sort((a, b) => b.price - a.price);
}, [products, search, filter]);
return (
<ul>
{filteredProducts.map(p => (
<li key={p.id}>{p.name} - ${p.price}</li>
))}
</ul>
);
}
4.2 不要过早优化
// ❌ 不必要的useMemo(简单计算开销很小)
const fullName = useMemo(() => `${firstName} ${lastName}`, [firstName, lastName]);
// ✓ 直接计算即可
const fullName = `${firstName} ${lastName}`;
五、useRef:持久化引用
5.1 访问DOM元素
import { useRef, useEffect } from 'react';
function FocusInput() {
const inputRef = useRef(null);
useEffect(() => {
inputRef.current?.focus();
}, []);
return <input ref={inputRef} type="text" />;
}
5.2 保存可变值(不触发重渲染)
function Timer() {
const [count, setCount] = useState(0);
const intervalRef = useRef(null);
// ✓ 使用ref保存定时器ID,不触发重渲染
const start = () => {
intervalRef.current = setInterval(() => {
setCount(c => c + 1);
}, 1000);
};
const stop = () => {
clearInterval(intervalRef.current);
};
return (
<>
<p>{count}</p>
<button onClick={start}>开始</button>
<button onClick={stop}>停止</button>
</>
);
}
六、自定义Hooks:逻辑复用
6.1 创建自定义Hook
// hooks/useFetch.js
import { useState, useEffect } from 'react';
export function useFetch(url, options = {}) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
let cancelled = false;
const fetchData = async () => {
try {
setLoading(true);
const response = await fetch(url, options);
if (!response.ok) throw new Error('Request failed');
const result = await response.json();
if (!cancelled) setData(result);
} catch (err) {
if (!cancelled) setError(err);
} finally {
if (!cancelled) setLoading(false);
}
};
fetchData();
return () => {
cancelled = true;
};
}, [url, JSON.stringify(options)]);
return { data, loading, error };
}
6.2 使用自定义Hook
import { useFetch } from './hooks/useFetch';
function UserList() {
const { data: users, loading, error } = useFetch('/api/users');
if (loading) return <div>加载中...</div>;
if (error) return <div>错误:{error.message}</div>;
return (
<ul>
{users?.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
6.3 实用的自定义Hooks示例
// hooks/useLocalStorage.js
export 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 = (value) => {
try {
setStoredValue(value);
window.localStorage.setItem(key, JSON.stringify(value));
} catch (error) {
console.error(error);
}
};
return [storedValue, setValue];
}
// hooks/useDebounce.js
export function useDebounce(value, delay) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => clearTimeout(handler);
}, [value, delay]);
return debouncedValue;
}
七、useReducer:复杂状态管理
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 initialState;
default:
throw new Error('Unknown action');
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
<p>Count: {state.count}</p>
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
<button onClick={() => dispatch({ type: 'decrement' })}>-</button>
<button onClick={() => dispatch({ type: 'reset' })}>重置</button>
</>
);
}
八、性能优化最佳实践
8.1 组件拆分
// ❌ 大组件,任何状态变化都导致全部重渲染
function Parent() {
const [count, setCount] = useState(0);
const [text, setText] = useState('');
return (
<>
<button onClick={() => setCount(c => c + 1)}>Count: {count}</button>
<input value={text} onChange={e => setText(e.target.value)} />
<ExpensiveComponent count={count} />
</>
);
}
// ✓ 拆分为小组件,独立优化
function Parent() {
const [count, setCount] = useState(0);
const [text, setText] = useState('');
return (
<>
<CountButton count={count} onIncrement={() => setCount(c => c + 1)} />
<TextInput value={text} onChange={setText} />
<ExpensiveComponent count={count} />
</>
);
}
8.2 使用React.memo
const ExpensiveComponent = React.memo(({ data }) => {
console.log('ExpensiveComponent rendered');
return <div>{data}</div>;
});
九、常见陷阱与解决方案
9.1 闭包陷阱
// ❌ 闭包陷阱:setTimeout中的count是旧值
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const timer = setTimeout(() => {
console.log(count); // 始终是0
}, 3000);
return () => clearTimeout(timer);
}, []);
}
// ✓ 解决方案1:使用ref
function Counter() {
const [count, setCount] = useState(0);
const countRef = useRef(count);
countRef.current = count;
useEffect(() => {
const timer = setTimeout(() => {
console.log(countRef.current); // 最新值
}, 3000);
return () => clearTimeout(timer);
}, []);
}
// ✓ 解决方案2:使用函数式更新
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const timer = setTimeout(() => {
setCount(prev => {
console.log(prev); // 最新值
return prev;
});
}, 3000);
return () => clearTimeout(timer);
}, []);
}
9.2 依赖数组遗漏
使用ESLint插件自动检查:
// .eslintrc.js
{
"plugins": ["react-hooks"],
"rules": {
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "warn"
}
}
十、Hooks规则
- 只在最顶层使用Hooks - 不要在循环、条件或嵌套函数中调用
- 只在React函数中调用Hooks - 不要在普通JavaScript函数中调用
- 使用ESLint插件 - 自动检查规则遵守情况
总结
React Hooks让函数组件更强大,但需要正确使用:
- ✅
useState:简单状态用直接更新,依赖旧状态用函数式更新 - ✅
useEffect:正确设置依赖数组,记得清理副作用 - ✅
useCallback/useMemo:按需使用,不要过早优化 - ✅
useRef:保存可变值或访问DOM - ✅ 自定义Hooks:提取可复用逻辑
- ✅ 遵守Hooks规则,使用ESLint检查
掌握这些最佳实践,让你的React代码更优雅、更高效!
欢迎在评论区分享你的Hooks使用经验!
文章评论