a-complete-guide-to-useeffect 详细介绍了useEffect的使用

一、和class组件对比

每一次渲染都有它自己的 Props and State,关键的点在于任意一次渲染中的count常量都不会随着时间改变。渲染输出会变是因为我们的组件被一次次调用,而每一次调用引起的渲染中,它包含的count值独立于其他渲染。

示例:

  1. function Counter() {
  2. const [count, setCount] = useState(0);
  3. function handleAlertClick() {
  4. setTimeout(() => {
  5. alert('You clicked on: ' + count);
  6. }, 3000);
  7. }
  8. return (
  9. <div>
  10. <p>You clicked {count} times</p>
  11. <button onClick={() => setCount(count + 1)}>
  12. Click me
  13. </button>
  14. <button onClick={handleAlertClick}>
  15. Show alert
  16. </button>
  17. </div>
  18. );
  19. }
  • 点击增加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参数:
示例:

  1. function Counter() {
  2. const [count, setCount] = useState(0);
  3. useEffect(() => {
  4. const id = setInterval(() => {
  5. setCount(count + 1);
  6. }, 1000);
  7. return () => clearInterval(id);
  8. }, []);
  9. return <h1>{count}</h1>;
  10. }

count值总是为1。在第一次运行setCount后,就会重新执行counter函数,并且因为effect的deps依赖为空,因此effect函数并不会执行,setInterval获取的count总是第一次的0,因此每次执行setCount,都是setCount(0+1)。

方案1:

  1. useEffect(() => {
  2. const id = setInterval(() => {
  3. setCount(count + 1);
  4. }, 1000);
  5. return () => clearInterval(id);
  6. }, [count]);

这种虽然解决了问题,但是每次都重新清除和设定计数器。

方案2:

  1. useEffect(() => {
  2. const id = setInterval(() => {
  3. setCount(c => c + 1);
  4. }, 1000);
  5. return () => clearInterval(id);
  6. }, []);

使用了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)和状态如何响应并更新分开表述。

示例:

  1. import React, { useState, useEffect, useReducer } from 'react';
  2. interface InitState {
  3. count: number;
  4. step: number;
  5. res: number;
  6. }
  7. const reducer = (state: InitState, action: { type: string }) => {
  8. const { count, step } = state;
  9. switch (action.type) {
  10. case 'plusCount':
  11. return { ...state, count: count + 1 };
  12. case 'plusStep':
  13. return { ...state, step: step + 1 };
  14. case 'plus':
  15. return { ...state, count: step + count };
  16. }
  17. }
  18. export function ReducerCounter() {
  19. const [state, dispatch] = useReducer(reducer, { count: 1, step: 1 });
  20. const { count, step } = state;
  21. useEffect(() => {
  22. const id = setInterval(() => {
  23. dispatch({ type: 'plus' });
  24. }, 1000);
  25. return () => clearInterval(id);
  26. }, [dispatch]);
  27. return (<div>
  28. <h1>count:{count}</h1>
  29. <h1>step:{step}</h1>
  30. <button onClick={() => dispatch({ type: 'plusCount' })}>plus count</button>
  31. <button onClick={() => dispatch({ type: 'plusStep' })}>plus step</button>
  32. </div>)
  33. }

使用reducer来解决effect出现的问题,将动作和状态解耦。

示例2:

  1. function Counter({ step }) {
  2. const [count, dispatch] = useReducer(reducer, 0);
  3. function reducer(state, action) {
  4. if (action.type === 'tick') {
  5. return state + step;
  6. } else {
  7. throw new Error();
  8. }
  9. }
  10. useEffect(() => {
  11. const id = setInterval(() => {
  12. dispatch({ type: 'tick' });
  13. }, 1000);
  14. return () => clearInterval(id);
  15. }, [dispatch]);
  16. return <h1>{count}</h1>;
  17. }

这种模式会使一些优化失效,所以你应该避免滥用它,不过如果你需要你完全可以在reducer里面访问props。
即使是在这个例子中,React也保证dispatch在每次渲染中都是一样的。 所以你可以在依赖中去掉它。它不会引起effect不必要的重复执行。
你可能会疑惑:这怎么可能?在之前渲染中调用的reducer怎么“知道”新的props?答案是当你dispatch的时候,React只是记住了action - 它会在下一次渲染中再次调用reducer。在那个时候,新的props就可以被访问到,而且reducer调用也不是在effect里。
这就是为什么我倾向认为useReducer是Hooks的“作弊模式”。它可以把更新逻辑和描述发生了什么分开。结果是,这可以帮助我移除不必需的依赖,避免不必要的effect调用。

五、useRef

与state和props相比,每次渲染的ref都是最新的,因此,当我们需要每次在effect的一些闭包情况下需要拿到最新的变量,我们可以使用ref来解决。

六、useCallback

  1. function SearchResults() {
  2. const [query, setQuery] = useState('react');
  3. const getFetchUrl = useCallback(() => { // No query argument
  4. return 'https://hn.algolia.com/api/v1/search?query=' + query;
  5. }, [query]);
  6. // ✅ 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
  7. useEffect(() => {
  8. const url = getFetchUrl('react');
  9. // ... Fetch data and do something ...
  10. }, [getFetchUrl]); // ✅ Effect deps are OK
  11. useEffect(() => {
  12. const url = getFetchUrl('redux');
  13. // ... Fetch data and do something ...
  14. }, [getFetchUrl]); // ✅ Effect deps are OK
  15. // ...
  16. }

useCallback本质上是添加了一层依赖检查。它以另一种方式解决了问题 - 我们使函数本身只在需要的时候才改变,而不是去掉对函数的依赖。

七、useMemo

示例:useMemo和useCallback的使用区别

类似于useCallback,但是useMemo是对复杂对象做类似的事情。

示例:

  1. function ColorPicker() {
  2. const [color, setColor] = useState('pink');
  3. const style = useMemo(() => ({ color }), [color]);
  4. return <Child style={style} />;
  5. }