最近React团队发布了Hooks的alpha预览版(预计明年正式发布)这是一个革命性的新特性。
Hooks能让函数组件拥有状态和生命周期的能力不需要再写类组件。这意味着我们可以用更简洁的函数式的方式写React组件代码更短更清晰更易复用。
我第一时间体验了Hooks用它重构了一些项目踩了很多坑。因为是alpha预览版文档还不够完善很多东西需要自己摸索。
今天想记录一下我在React Hooks实战中踩过的那些坑和解决方法。希望这些经验能帮大家在使用Hooks的时候,少踩坑。
一、Hooks简介
先简单介绍一下Hooks。
Hooks是React 16.7的新特性(alpha预览版)它让你在函数组件中使用状态和,其他React特性不需要写类组件。
Hooks的核心思想是让函数组件也能拥有类组件的能力,同时保持函数式编程的简洁和优雅。
常用的Hooks有:
- useState:让函数组件拥有状态。
- useEffect:让函数组件拥有副作用的能力类似生命周期。
- useContext:让函数组件使用Context。
- useReducer:让函数组件用reducer的方式管理复杂状态。
- useCallback:缓存回调函数避免不必要的重新渲染。
- useMemo:缓存计算结果避免重复计算。
- useRef:让函数组件拥有ref。
- useImperativeMethods:让父组件能调用子组件的方法。
这些Hooks覆盖了类组件的大部分能力用它们基本能替代类组件。
二、useState的坑
第一个坑是useState的坑。
useState是最基础的Hook用来给函数组件添加状态。
用法很简单:
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>+1</button>
</div>
);
}看起来很简单,但是实际使用的时候,踩了一些坑。
坑1:setState的异步更新:
第一个坑是setState的异步更新。
在类组件中setState是异步的多次调用setState会被合并。在函数组件中useState返回的setCount也是异步的多次调用也会被合并。
比如:
function Counter() {
const [count, setCount] = useState(0);
const handleClick = () => {
setCount(count + 1);
setCount(count + 1);
setCount(count + 1);
};
return (
<div>
<p>Count: {count}</p>
<button onClick={handleClick}>+3</button>
</div>
);
}你可能以为点击按钮count会增加3但是实际上,只增加1。因为三次setCount都用的是同一个count值(0)所以三次都设置为1合并后就是1。
解决方法是用函数式更新:
const handleClick = () => {
setCount(prev => prev + 1);
setCount(prev => prev + 1);
setCount(prev => prev + 1);
};这样每次setCount都用的是上一次的结果三次调用后count会增加3。
这个坑和类组件的setState异步更新是一样的,但是,因为函数组件的写法不同容易忽略。
坑2:初始值只在第一次渲染时生效:
第二个坑是useState的初始值只在第一次渲染时生效。
比如:
function Counter(props) {
const [count, setCount] = useState(props.initialCount);
// ...
}你可能以为props.initialCount变化的时候,候count会跟着变化。但是实际上useState的初始值只在第一次渲染时生效后续props.initialCount变化count不会跟着变。
如果需要根据props更新state需要用useEffect监听props的变化,然后更新state。
或者,如果不需要state和props同步只是用props作为初始值那没问题。
坑3:复杂状态的管理:
第三个坑是复杂状态的管理。
如果组件的状态比较复杂有很多字段用多个useState会比较乱也不好管理。
比如:
function UserForm() {
const [name, setName] = useState('');
const [age, setAge] = useState(0);
const [email, setEmail] = useState('');
const [address, setAddress] = useState('');
const [phone, setPhone] = useState('');
// ...
}这样写很多useState代码比较乱也不好批量更新。
解决方法是用useReducer管理复杂状态,或者把相关的状态合并成一个对象用一个useState。
比如:
function UserForm() {
const [user, setUser] = useState({
name: '',
age: 0,
email: '',
address: '',
phone: ''
});
const handleChange = (field, value) => {
setUser(prev => ({ ...prev, [field]: value }));
};
// ...
}这样代码更清晰更好管理。
三、useEffect的坑
useEffect是最常用的Hook之一用来处理副作用,比如数据获取订阅DOM操作等等。
但是useEffect也是最容易踩坑的Hook。
坑1:依赖数组的问题:
第一个坑是依赖数组的问题。
useEffect接受两个参数第一个是副作用函数第二个是依赖数组。依赖数组决定了useEffect什么时候执行。
如果不传依赖数组useEffect每次渲染后都会执行。如果传空数组useEffect只在第一次渲染后执行类似componentDidMount。如果传有值的数组useEffect在数组中的值变化时执行。
很多人容易在依赖数组上踩坑。
比如忘记传依赖数组导致useEffect每次渲染都执行造成死循环,或者性能问题。
比如:
function DataFetcher() {
const [data, setData] = useState(null);
useEffect(() => {
fetch('/api/data')
.then(res => res.json())
.then(data => setData(data));
}); // 没传依赖数组每次渲染都执行
return <div>{data ? JSON.stringify(data) : 'Loading...'}</div>;
}这个组件每次渲染后都会执行useEffect获取数据,然后setData触发重新渲染,然后又执行useEffect造成死循环。
解决方法是传空数组让useEffect只在第一次渲染后执行:
useEffect(() => {
fetch('/api/data')
.then(res => res.json())
.then(data => setData(data));
}, []); // 空数组只在第一次渲染后执行另一个常见的坑是依赖数组漏了依赖导致useEffect用了旧的值。
比如:
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const timer = setInterval(() => {
console.log(count); // 这里的count永远是0
}, 1000);
return () => clearInterval(timer);
}, []); // 依赖数组是空的count变化不会触发重新执行
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>+1</button>
</div>
);
}这个例子中useEffect的依赖数组是空的,所以只在第一次渲染后执行。setInterval的回调中用的count是第一次渲染时的count(0)所以,即使count变化了console.log打印的还是0。
解决方法是把count加到依赖数组中:
useEffect(() => {
const timer = setInterval(() => {
console.log(count);
}, 1000);
return () => clearInterval(timer);
}, [count]); // count变化时重新执行这样count变化时useEffect会重新执行清除旧的timer创建新的timer用新的count。
但是这样每次count变化都会清除和重新创建timer有点浪费。更好的方法是用useRef保存最新的count或者用函数式更新。
坑2:清理函数的问题:
第二个坑是清理函数的问题。
useEffect可以返回一个清理函数在组件卸载时,或者下一次useEffect执行前调用用来清理副作用,比如清除timer取消订阅等等。
很多人容易忘记写清理函数导致内存泄漏,或者其他问题。
比如:
function DataFetcher() {
const [data, setData] = useState(null);
useEffect(() => {
fetch('/api/data')
.then(res => res.json())
.then(data => setData(data));
// 没有清理函数
}, []);
return <div>{data ? JSON.stringify(data) : 'Loading...'}</div>;
}这个例子中,如果组件在fetch完成前卸载了fetch完成后还会调用setData但是组件已经卸载了会导致警告甚至内存泄漏。
解决方法是用一个变量标记组件是否卸载了:
useEffect(() => {
let isMounted = true;
fetch('/api/data')
.then(res => res.json())
.then(data => {
if (isMounted) {
setData(data);
}
});
return () => {
isMounted = false;
};
}, []);这样组件卸载后isMounted为false不会再调用setData。
对于timer订阅等副作用一定要写清理函数,否则会导致内存泄漏。
坑3:useEffect的执行时机:
第三个坑是useEffect的执行时机。
useEffect是在渲染完成后异步执行的不会阻塞浏览器的绘制。这和componentDidMountcomponentDidUpdate类似,但是有细微的差别。
比如useEffect中读取DOM的尺寸可能会导致闪烁,因为useEffect是在浏览器绘制后执行的这时候用户已经看到了旧的布局,然后useEffect修改布局会导致闪烁。
如果需要在浏览器绘制前执行副作用可以用useLayoutEffect它是,同步执行的在浏览器绘制前执行。
但是useLayoutEffect会阻塞浏览器绘制可能影响性能,所以大部分情况用useEffect就够了,只有需要同步修改DOM的时候,才用useLayoutEffect。
四、useContext的坑
useContext让函数组件能使用Context不需要Consumer组件嵌套。
用法很简单:
const ThemeContext = React.createContext('light');
function App() {
return (
<ThemeContext.Provider value="dark">
<Toolbar />
</ThemeContext.Provider>
);
}
function Toolbar() {
const theme = useContext(ThemeContext);
return <div>Theme: {theme}</div>;
}看起来很简单,但是也有坑。
坑:Context value的引用变化导致重新渲染:
useContext的组件会在Context的value变化时重新渲染。如果Context的value是一个对象每次渲染都创建新的对象,即使对象的内容没变也会导致所有useContext的组件重新渲染。
比如:
function App() {
const [count, setCount] = useState(0);
const value = { count, setCount }; // 每次渲染都创建新对象
return (
<ThemeContext.Provider value={value}>
<Child />
</ThemeContext.Provider>
);
}这个例子中每次App渲染都会创建新的value对象导致所有useContext(ThemeContext)的组件都重新渲染,即使count没变。
解决方法是用useMemo缓存value:
function App() {
const [count, setCount] = useState(0);
const value = useMemo(() => ({ count, setCount }), [count]);
return (
<ThemeContext.Provider value={value}>
<Child />
</ThemeContext.Provider>
);
}这样,只有count变化时才创建新的value对象避免不必要的重新渲染。
五、useReducer的坑
useReducer用来管理复杂的状态类似Redux的reducer。
用法:
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 };
default:
return state;
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<div>
<p>Count: {state.count}</p>
<button onClick={() => dispatch({ type: 'increment' })}>+1</button>
<button onClick={() => dispatch({ type: 'decrement' })}>-1</button>
</div>
);
}useReducer的坑相对少一些,但是也有。
坑:reducer必须是纯函数:
reducer必须是纯函数不能有副作用不能修改原state必须返回新的state。
如果reducer不是纯函数会导致各种奇怪的问题,比如状态不更新,或者更新异常。
比如不要在reducer中直接修改state:
// 错误
function reducer(state, action) {
switch (action.type) {
case 'increment':
state.count++; // 直接修改原state错误
return state;
default:
return state;
}
}
// 正确
function reducer(state, action) {
switch (action.type) {
case 'increment':
return { ...state, count: state.count + 1 }; // 返回新的state
default:
return state;
}
}六、自定义Hooks的坑
Hooks的一大优势是可以把组件逻辑抽取成自定义Hooks复用。
但是自定义Hooks也有一些坑。
坑1:Hooks的调用规则:
Hooks有调用规则:只能在函数组件的顶层调用不能在循环条件嵌套函数中调用。
自定义Hooks也要遵守这个规则。而且自定义Hooks的名字必须以use开头这样React才能识别它是Hook检查调用规则。
坑2:自定义Hooks的状态隔离:
每个组件调用自定义Hook都会创建独立的状态不会共享。如果需要共享状态需要用Context或者其他方式。
比如:
function useCounter() {
const [count, setCount] = useState(0);
const increment = () => setCount(count + 1);
return { count, increment };
}
function ComponentA() {
const { count, increment } = useCounter();
// ...
}
function ComponentB() {
const { count, increment } = useCounter();
// ComponentA和ComponentB的count是独立的不共享
}如果需要共享状态需要把状态提升到父组件,或者用Context。
七、Hooks的最佳实践
最后总结一下Hooks的最佳实践。
1. 遵守Hooks的调用规则:
只能在函数组件的顶层调用Hooks不能在循环条件嵌套函数中调用。可以用eslint-plugin-react-hooks插件检查调用规则。
2. 正确设置依赖数组:
useEffectuseCallbackuseMemo的依赖数组要正确设置不要漏了依赖也不要多加不必要的依赖。可以用eslint-plugin-react-hooks的exhaustive-deps规则检查。
3. 及时清理副作用:
useEffect中的副作用,比如timer订阅请求等等要及时清理避免内存泄漏。
4. 合理拆分状态:
不要把所有状态都放一个useState也不要拆得太碎。相关的状态可以放一起用一个useState或者useReducer。不相关的状态分开用多个useState。
5. 用useCallback和useMemo优化性能:
对于传递给子组件的回调函数用useCallback缓存避免子组件不必要的重新渲染。对于复杂的计算用useMemo缓存结果避免重复计算。
但是不要滥用useCallback和useMemo它们本身也有开销,只有确实需要优化的时候,才用。
6. 抽取自定义Hooks复用逻辑:
把可复用的组件逻辑抽取成自定义Hooks提高代码复用率和可维护性。
八、写在最后
以上就是我在React Hooks实战中踩过的那些坑和总结的最佳实践。
Hooks是一个非常优秀的新特性它让React组件的写法更简洁更优雅更函数式。但是,因为是新特性,而且目前还是alpha预览版文档不够完善很多东西需要自己摸索容易踩坑。
希望我的这些经验能帮大家在使用Hooks的时候,少踩坑更快上手。
当然Hooks还在快速发展中正式版发布的时候,可能会有一些变化。大家要关注官方文档和更新。
最后用一句话结束这篇文章:"Hooks是React的未来早学早受益。"
愿大家都能用好Hooks写出更优雅的React代码。
评论(0)
暂无评论,快来抢沙发~
评论功能仅对会员开放,请先登录
登录