a-complete-guide-to-useeffect 详细介绍了useEffect的使用
一、和class组件对比
每一次渲染都有它自己的 Props and State,关键的点在于任意一次渲染中的count
常量都不会随着时间改变。渲染输出会变是因为我们的组件被一次次调用,而每一次调用引起的渲染中,它包含的count
值独立于其他渲染。
示例:
function Counter() {
const [count, setCount] = useState(0);
function handleAlertClick() {
setTimeout(() => {
alert('You clicked on: ' + count);
}, 3000);
}
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
<button onClick={handleAlertClick}>
Show alert
</button>
</div>
);
}
- 点击增加counter到3
- 点击一下 “Show alert”
点击增加 counter到5并且在定时器回调触发前完成
最终alert的是3还是5? 答案是:3
解析:在每次点击通过setCount时,都会重新调用Counter函数渲染,每次的Counter函数都会保存每次渲染时的state和props,并且在整个渲染过程中state和props保持不变,每次的渲染过程的count保持独立。
延伸:同样的代码使用class形式写出,this.state.count每次都是拿到最新值。因为class组件只是一个实例,修改state时,在多次渲染的情况下,setTimeout执行的时候获取到的是最新的state值。而hooks通过闭包保存了每次渲染的state值。因此使用hooks时,在组件内什么时候去读取props或者state是无关紧要的。因为它们不会改变。在单次渲染的范围内,props和state始终保持不变。(解构赋值的props使得这一点更明显。)
二、captrue values
三、useEffect
在DOM渲染完成之后调用,这一点和componentDidMount不同
effect 函数本身在每一次渲染中都不相同,每一个effect版本“看到”的count
值都来自于它属于的那次渲染。
React会记住你提供的effect函数,并且会在每次更改作用于DOM并让浏览器绘制屏幕后去调用它,概念上,你可以想象effects是渲染结果的一部分。
但是,有时候你可能想在effect的回调函数里读取最新的值而不是捕获的值。最简单的实现方法是使用refs。
React只会在浏览器绘制后运行effects。这使得你的应用更流畅因为大多数effects并不会阻塞屏幕的更新。
上一次的effect返回的清除函数,会在下一次浏览器绘制完成之后运行,然后再去执行本次的effect函数。
deps参数:
示例:
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setCount(count + 1);
}, 1000);
return () => clearInterval(id);
}, []);
return <h1>{count}</h1>;
}
count值总是为1。在第一次运行setCount后,就会重新执行counter函数,并且因为effect的deps依赖为空,因此effect函数并不会执行,setInterval获取的count总是第一次的0,因此每次执行setCount,都是setCount(0+1)。
方案1:
useEffect(() => {
const id = setInterval(() => {
setCount(count + 1);
}, 1000);
return () => clearInterval(id);
}, [count]);
这种虽然解决了问题,但是每次都重新清除和设定计数器。
方案2:
useEffect(() => {
const id = setInterval(() => {
setCount(c => c + 1);
}, 1000);
return () => clearInterval(id);
}, []);
使用了setCount的函数形式,这种情况下,只是告诉react每次count加1,这个时候计数器读取的就不是第一次渲染时的count值,而是最新的count值,而恰恰react是知道当前最新的count值的。
然而,即使是setCount(c => c + 1)也并不完美。它看起来有点怪,并且非常受限于它能做的事。举个例子,如果我们有两个互相依赖的状态,或者我们想基于一个prop来计算下一次的state,它并不能做到。幸运的是, setCount(c => c + 1)有一个更强大的姐妹模式,它的名字叫useReducer。
四、useReducer
当你想更新一个状态,并且这个状态更新依赖于另一个状态的值时,你可能需要用useReducer去替换它们。
当你写类似setSomething(something => …)这种代码的时候,也许就是考虑使用reducer的契机。reducer可以让你把组件内发生了什么(actions)和状态如何响应并更新分开表述。
示例:
import React, { useState, useEffect, useReducer } from 'react';
interface InitState {
count: number;
step: number;
res: number;
}
const reducer = (state: InitState, action: { type: string }) => {
const { count, step } = state;
switch (action.type) {
case 'plusCount':
return { ...state, count: count + 1 };
case 'plusStep':
return { ...state, step: step + 1 };
case 'plus':
return { ...state, count: step + count };
}
}
export function ReducerCounter() {
const [state, dispatch] = useReducer(reducer, { count: 1, step: 1 });
const { count, step } = state;
useEffect(() => {
const id = setInterval(() => {
dispatch({ type: 'plus' });
}, 1000);
return () => clearInterval(id);
}, [dispatch]);
return (<div>
<h1>count:{count}</h1>
<h1>step:{step}</h1>
<button onClick={() => dispatch({ type: 'plusCount' })}>plus count</button>
<button onClick={() => dispatch({ type: 'plusStep' })}>plus step</button>
</div>)
}
使用reducer来解决effect出现的问题,将动作和状态解耦。
示例2:
function Counter({ step }) {
const [count, dispatch] = useReducer(reducer, 0);
function reducer(state, action) {
if (action.type === 'tick') {
return state + step;
} else {
throw new Error();
}
}
useEffect(() => {
const id = setInterval(() => {
dispatch({ type: 'tick' });
}, 1000);
return () => clearInterval(id);
}, [dispatch]);
return <h1>{count}</h1>;
}
这种模式会使一些优化失效,所以你应该避免滥用它,不过如果你需要你完全可以在reducer里面访问props。
即使是在这个例子中,React也保证dispatch
在每次渲染中都是一样的。 所以你可以在依赖中去掉它。它不会引起effect不必要的重复执行。
你可能会疑惑:这怎么可能?在之前渲染中调用的reducer怎么“知道”新的props?答案是当你dispatch
的时候,React只是记住了action - 它会在下一次渲染中再次调用reducer。在那个时候,新的props就可以被访问到,而且reducer调用也不是在effect里。
这就是为什么我倾向认为useReducer
是Hooks的“作弊模式”。它可以把更新逻辑和描述发生了什么分开。结果是,这可以帮助我移除不必需的依赖,避免不必要的effect调用。
五、useRef
与state和props相比,每次渲染的ref都是最新的,因此,当我们需要每次在effect的一些闭包情况下需要拿到最新的变量,我们可以使用ref来解决。
六、useCallback
function SearchResults() {
const [query, setQuery] = useState('react');
const getFetchUrl = useCallback(() => { // No query argument
return 'https://hn.algolia.com/api/v1/search?query=' + query;
}, [query]);
// ✅ Preserves identity when its own deps are the same const getFetchUrl = useCallback((query) => { return 'https://hn.algolia.com/api/v1/search?query=' + query; }, []); // ✅ Callback deps are OK
useEffect(() => {
const url = getFetchUrl('react');
// ... Fetch data and do something ...
}, [getFetchUrl]); // ✅ Effect deps are OK
useEffect(() => {
const url = getFetchUrl('redux');
// ... Fetch data and do something ...
}, [getFetchUrl]); // ✅ Effect deps are OK
// ...
}
useCallback
本质上是添加了一层依赖检查。它以另一种方式解决了问题 - 我们使函数本身只在需要的时候才改变,而不是去掉对函数的依赖。
七、useMemo
类似于useCallback,但是useMemo是对复杂对象做类似的事情。
示例:
function ColorPicker() {
const [color, setColor] = useState('pink');
const style = useMemo(() => ({ color }), [color]);
return <Child style={style} />;
}