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 argumentreturn '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 OKuseEffect(() => {const url = getFetchUrl('react');// ... Fetch data and do something ...}, [getFetchUrl]); // ✅ Effect deps are OKuseEffect(() => {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} />;}
