《useEffect完全指南》笔记

本文详细探讨了React Hooks中的useEffect,包括它与componentDidMount的区别、函数定位、渲染过程、运行时机、清除逻辑、更新条件、useRef的应用、state管理和数据获取策略。文章通过实例解释了如何正确使用依赖数组以及何时使用useReducer,同时讨论了竞态问题的解决方案,旨在帮助开发者更好地理解和运用useEffect。
摘要由CSDN通过智能技术生成

本篇是个人笔记,并非教程,建议看原文,虽然比较长,但看个两三遍试用一下还是非常有提升的,有问题可以一起交流~

原文地址:https://overreacted.io/zh-hans/a-complete-guide-to-useeffect/
作者简介:https://overreacted.io/zh-hans/my-decade-in-review/

useEffect(fn, [])和componentDidMount的区别

前者会捕获props和state,所以在回调函数中不管等多久后,拿到的props和state依旧是初始值(定时器例子)。如果需要拿到实时数据,则可使用ref或后文提到的其它方式。

函数存放的最佳位置

  1. 和state和props无关,放到组件外
  2. 只和某个useEffect有关,放到这个里面
  3. effect使用到以上两种之外的函数,用useCallback包裹起来。

每次渲染

  1. state和props是每帧完全独立的,对于本次的渲染来说就是个常量,包括对象类型,当然我们不能放弃setState而直接修改对象的属性
    函数也会每帧独立渲染,被调用时传入的参数就是当前帧的数值,不会随着组件更新而变化。
  2. 都有它自己的Effects,即Effects内部保存了当次渲染时的state和props,可以理解为effects是组件渲染的一部分。

effects运行时机

React只会在浏览器绘制后运行effects。

延时的情况

  1. 如果effect中有延时,则执行时输出的state和props仍然是当初调用时的值,不会随着组件更新而改变(原理是闭包);但在class组件的didUpdate里却随时更新。即如下两种写法等价
function Example(props) {
 useEffect(() => {
   setTimeout(() => {
     console.log(props.counter);
   }, 1000);
 });
 // ...
}
function Example(props) {
 const counter = props.counter;
   useEffect(() => {
       setTimeout(() => {
         console.log(counter);
       }, 1000);
 });
 // ...
}

useRef 在effect中使用实时的state和effect

如下实现相当于模拟了class中的行为:

function Example() {
 const [count, setCount] = useState(0);
 const latestCount = useRef(count);
 useEffect(() => {
      // Set the mutable latest value
      latestCount.current = count; 
      setTimeout(() => {
      // Read the mutable latest value 
          console.log(`You clicked ${latestCount.current} times`); 
     }, 3000);
 });
// ...

effects中的清除

effects的return:每次浏览器渲染完后运行effects,首先调用return中定义的函数进行一些清除工作,而函数里的state或props是上一次的值,即定义时保存的数据。假设每次订阅一个id再清除这个id,如果第一次id为10,第二次id为20,则本以为react的执行顺序是:

  1. 清除id为10的effect
  2. 渲染id为20的UI
  3. 运行id为20的effect

而事实上是

  1. 渲染id为20的UI
  2. 清除id为10的effect
  3. 浏览器绘制。我们在屏幕上看到{id: 20}的UI。
  4. 运行id为20的effect

即先渲染本帧后清除上一帧。

useEffect 更新条件

提供给useEffect一个依赖数组参数(deps),相当于告诉react这个effect只用到了这些参数,当组件更新时所有参数都没有变化,react就会跳过执行此次effect。如果有一个参数变化,则此次会同步所有参数。
如何写更新条件?

“将诚实地告知effect依赖作为一条硬性规则,并且要列出所以依赖”

  1. 第一种策略是在依赖中包含所有effect中用到的组件内的值。
  2. 第二种策略是修改effect内部的代码以确保它包含的值只会在需要的时候发生变更。

setState的函数形式

当我们想要根据前一个状态更新状态的时候,我们可以使用setState的函数形式

useEffect(() => {
    const id = setInterval(() => {
          setCount(c => c + 1);
    }, 1000);
    return () => clearInterval(id);
}, []);

useReducer

一个原则:只在effect中传递最小的信息,如上。有时上述方法并不能完全解决问题,比如下面的例子中step改变时count会更新步长,定时器被清除后会重启。

function Counter() {
  const [count, setCount] = useState(0);
  const [step, setStep] = useState(1);

  useEffect(() => {
    const id = setInterval(() => {
      setCount(c => c + step);
    }, 1000);
    return () => clearInterval(id);
  }, [step]);
  return (
    <>
      <h1>{count}</h1>
      <input value={step} onChange={e => setStep(Number(e.target.value))} />
    </>
  );
}

如果希望不用重启定时器,就需要用到useReducer。

当你想更新一个状态,并且这个状态更新依赖于另一个状态的值时,你可能需要用useReducer去替换它们。当你写类似setSomething(something => …)这种代码的时候,也许就是考虑使用reducer的契机。reducer可以让你把组件内发生了什么(actions)和状态如何响应并更新分开表述。

我们用一个dispatch依赖去替换effect的step依赖:

import React, { useReducer, useEffect } from 'react';
const initState = {
    count: 0,
    step: 1
}
const reducer = (state, action) => {
    const { count, step } = state
    if (action.type === 'tick') {
        return { count: count + step, step }
    } else if (action.type === 'step') {
        return { count, step: action.step }
    }
}
const Reducer01 = () => {
    const [state, dispatch] = useReducer(reducer, initState)
    const {count, step} = state

    useEffect(() => {
        const t = setInterval(() => {
            dispatch({ type: 'tick' })
        }, 1000)
        return ()=>[
            clearInterval(t)
        ]
    }, [dispatch]) // 实际上,这个依赖可以不写,因为dispatch在整个组件的生命周期中是不变的
    
    return (
        <div>
            <h3>使用reducer</h3>
            <p>{count}</p>
            <input onChange={e=>dispatch({type:'step',step:Number(e.target.value)})} />
        </div>
    );
}

export default Reducer01;

来自于props的step

如果上例中的step来自于props,则可以把reducer写到组件内获取step。但这种模式会使一些优化失效,所以应该避免滥用——reducer随着每次组件渲染会生成一份新的。useReducer可以理解为是Hooks的“作弊模式”。它可以把更新逻辑和描述发生了什么分开。好处是,这可以帮助我移除不必需的依赖,避免不必要的effect调用。

const Reducer02 = ({ step }) => {
    const reducer = (state, action) => {
       const { count } = state
        if (action.type === 'tick') {
            return { count: count + step, step }
        } else if (action.type === 'step') {
            return { count, step: action.step }
        }
    }
    
    const [state, dispatch] = useReducer(reducer, initState)
    const { count } = state
    
    useEffect(() => {
        const t = setInterval(() => {
            dispatch({ type: 'tick' })
        }, 1000)
        return () => [
            clearInterval(t)
        ]
    }, [])
    return (……);
}

获取数据

根据查询参数自动获取结果

关于获取数据的一个例子:查询参数query改变时,useEffect自动执行拉取数据的操作。

function SearchResults() {
  const [query, setQuery] = useState('react');

  useEffect(() => {
    function getFetchUrl() {
      return 'https://hn.algolia.com/api/v1/search?query=' + query;    
    }

    async function fetchData() {
      const result = await axios(getFetchUrl());
      setData(result.data);
    }

    fetchData();
  }, [query]); // ✅ Deps are OK
  // ...
}

复用获取查询地址的方法

当上述例子中需要复用getFetchUrl逻辑时,将其写在useEffect外面的组件内,同时将函数作为依赖,这样会造成每次刷新都会请求数据。

function SearchResults() {
  // 🔴 Re-triggers all effects on every render  
  function getFetchUrl(query) { 
     return 'https://hn.algolia.com/api/v1/search?query=' + query; 
  }
  useEffect(() => {
    const url = getFetchUrl('react');
    // ... Fetch data and do something ...
  }, [getFetchUrl]); // 🚧 Deps are correct but they change too often

  useEffect(() => {
    const url = getFetchUrl('redux');
    // ... Fetch data and do something ...
  }, [getFetchUrl]); // 🚧 Deps are correct but they change too often

  // ...
}

解决方案论述如下:

  1. 将函数写在组件外部,前提是这个函数没有使用组件内的值
function getFetchUrl(query) {
  return 'https://hn.algolia.com/api/v1/search?query=' + query;
}
function SearchResults() {
  useEffect(() => {
    const url = getFetchUrl('react');
    // ... Fetch data and do something ...
  }, []); // ✅ Deps are OK

  useEffect(() => {
    const url = getFetchUrl('redux');
    // ... Fetch data and do something ...
  }, []); // ✅ Deps are OK
  // ...
}
  1. 使用useCallback以及将函数作为依赖。
function SearchResults() {
  // ✅ 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

  // ...
}
  1. 如果query是个其他地方可以改变的组件内state,则可以设置为依赖
function SearchResults() {
  const [query, setQuery] = useState('react');

  // ✅ Preserves identity until query changes
  const getFetchUrl = useCallback(() => {
     return 'https://hn.algolia.com/api/v1/search?query=' + query;  
  }, [query]);  // ✅ Callback deps are OK
  useEffect(() => {
    const url = getFetchUrl();
    // ... Fetch data and do something ...
  }, [getFetchUrl]); // ✅ Effect deps are OK

  // ...
}
  1. 如果函数是从父组件传入,比如子组件发现父组件的query改变后就去请求数据,仍然适用于上面的方法
function Parent() {
  const [query, setQuery] = useState('react');

  // ✅ Preserves identity until query changes
  const fetchData = useCallback(() => {
      const url = 'https://hn.algolia.com/api/v1/search?query=' + query;    // ... Fetch data and return it ...
  }, [query]);  // ✅ Callback deps are OK
  
  return <Child fetchData={fetchData} />
}

function Child({ fetchData }) {
  let [data, setData] = useState(null);

  useEffect(() => {
    fetchData().then(setData);
  }, [fetchData]); // ✅ Effect deps are OK

  // ...
}

需要注意的是这种情况不能放入class组件中使用,因为函数在整个生命周期中只有一份,地址永远不变,无法驱动query变动后的刷新。解法是封装一个子组件去请求数据,然后把query传给子组件,在子组件中判断query来决定是否要更新。此时query就相当于只为了让子组件做diff才传入的,而在hooks中,effect是参与数据流的,就能更好的解决这个问题。

  1. 类似的,使用useMemo可以对复杂对象做更多的事情,下面例子中使用useMemo封装了一个dom需要的stlye格式,只要color改变,dom就跟随渲染了。
function ColorPicker() {
  // Doesn't break Child's shallow equality prop check
  // unless the color actually changes.
  const [color, setColor] = useState('pink');
  const style = useMemo(() => ({ color }), [color]);
  return <Child style={style} />;
}
  1. 需要注意点是,作者说 “到处使用useCallback是件挺笨拙的事。在上面的例子中,我更倾向于把fetchData放在我的effect里(它可以抽离成一个自定义Hook)或者是从顶层引入。我想让effects保持简单,而在里面调用回调会让事情变得复杂” 。

竞态问题

定义:“请求结果返回的顺序不能保证一致。比如我先请求 {id: 10}时的数据,然后更新到{id: 20},但{id: 20}的请求更先返回。请求更早但返回更晚的情况会错误地覆盖状态值。” 解决的方法是使用一个布尔值跟踪状态。

function Article({ id }) {
  const [article, setArticle] = useState(null);

  useEffect(() => {
    let didCancel = false;
    async function fetchData() {
      // 当这一步等待时间长未返回数据时下一次请求发生了,
      // 则在effect清除工作中会置didCancel为true,
      // 即使这次结果返回了也会被丢弃,不再影响到当前的数据
      const article = await API.fetchArticle(id);
      if (!didCancel) {
          setArticle(article);
      }
    }

    fetchData();

    return () => {
          didCancel = true;
    };
  }, [id]);

  // ...
}

结束

本文讲述的effect基本都是初级使用水平,而社区将会推出一些基于effect的hooks,以减少我们的频繁手动创建effect。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值