2023年了~useEffect你真的会用嘛

前言

这篇文章是我依据a-complete-guide-to-useeffect本人总结进去的一些重点~

依照程序顺次如下:

  • React单向数据流的渲染
  • Effect的执行机会
  • 不要对Effect扯谎:依赖数组要怎么设置
  • 应用setStateuseReducer将上报行为和状态更新解耦
  • 函数是否能够作为Effect的依赖呢?

同时咱们能够在浏览的时候时刻问本人以下问题:

  • 如何用useEffect模仿componentDidMount生命周期?
  • 如何正确地在useEffect里申请数据?[]又是什么?
  • 我应该把函数当做effect的依赖吗?
  • 为什么有时候会呈现有限反复申请的问题?
  • 为什么有时候在effect里拿到的是旧的state或prop?

每一次渲染都有它本人的props和state

function Counter() {  const [count, setCount] = useState(0);  return (    <div>      <p>You clicked {count} times</p>+      <button onClick={() => setCount(count + 1)}>        Click me      </button>    </div>  );}

上述例子中,对于<p>You clicked {count} times</p>中的count,可能react的初学者会认为其是响应式的,也就是会主动监听并且更新渲染。

但实际上,它只是一个一般的变量count,没有任何的data bounding。咱们在每一次调用setCount的时候,实际上是React应用新的count值来重复渲染组件。第一次是0,第二次是1,以此类推。。。

除了变量,那么事件处理函数呢?

function Example() {  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>  );}

咱们设想一下:

  • 首先间断点击 Click me 两次
  • 再点击一次 Show alert
  • 持续点击 Click me

在点击alert的3秒过后,其显示的值会是什么呢?

答案是2!!,这也证实了咱们上述的论断。即事件处理函数跟变量一样,也是独立属于以后的渲染。

每次渲染都有它独立的Effect

咱们来看看官网上的例子:

function Counter() {  const [count, setCount] = useState(0);  useEffect(() => {    document.title = `You clicked ${count} times`;  });  return (    <div>      <p>You clicked {count} times</p>      <button onClick={() => setCount(count + 1)}>        Click me      </button>    </div>  );}

这里依据之前的学习咱们晓得,并不是「不变的Effect中count的值发现了变动,而是Effect每一次渲染都不雷同,每一次Effect都捕捉了以后渲染的count

<div style="color:#d2568c;border-left:5px solid;padding:6px"><span style="font-weight:bold;">「React会记住你提供的effect函数,并且会在每次更改作用于DOM并让浏览器绘制屏幕后去调用它。」</span></div>

useEffect

每一次渲染都有它本人的......所有

function Counter() {  const [count, setCount] = useState(0);  useEffect(() => {    setTimeout(() => {      console.log(`You clicked ${count} times`);    }, 3000);  });  return (    <div>      <p>You clicked {count} times</p>      <button onClick={() => setCount(count + 1)}>        Click me      </button>    </div>  );}

咱们来思考一下上述代码,咱们给每一次渲染都增加一个延时函数。当咱们疯狂点击Click me的时候,会产生什么呢?

答案是:「程序打印0,1,2,3...」

当然,你可能会想,如果我就是想要获取最新的值要怎么办的?应用useRef

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);  });  // ...

此时当咱们疯狂点击后,最终打印进去的全都是最新的count

Effect中的清理又是怎么样的?

思考官网的案例:

useEffect(() => {    ChatAPI.subscribeToFriendStatus(props.id, handleStatusChange);    return () => {      ChatAPI.unsubscribeFromFriendStatus(props.id, handleStatusChange);    };  });

假如第一次渲染的时候props是{id: 10},第二次渲染的时候是{id: 20}。你可能会认为产生了上面的这些事:

  • React 革除了 {id: 10} 的effect。
  • React 渲染 {id: 20} 的UI。
  • React 运行 {id: 20} 的effect。

事实上并不是这样的。React只会在浏览器渲染结束后才会去执行Effect,这并不会阻塞渲染使得你的利用更加晦涩。

Effect的清理实际上也被延后了。上一次Effecet的清理会在UI渲染完结后被执行。

  • React 渲染 {id: 20} 的UI。
  • 浏览器绘制 咱们在屏幕上看到 {id: 20} 的UI。
  • React 革除 {id: 10} 的effect。
  • React 运行 {id: 20} 的effect。

通知react如何去比对useEffect

简略来说,你的Effect可能会因为其余状态变动了而造成了不必要的调用。
为了解决这个问题,能够通过依赖数组来通知React

如果以后渲染中的这些依赖项和上一次运行这个effect的时候值一样,因为没有什么须要同步,React会主动跳过这次effect。

如果依赖项设置谬误会怎么样呢?

举个例子,咱们来写一个每秒递增的计数器。在Class组件中,咱们的直觉是:“开启一次定时器,革除也是一次”

当咱们天经地义地把它用useEffect的形式翻译,直觉上咱们会设置依赖为[]。因为“我只想运行一次effect“。

function Counter() {  const [count, setCount] = useState(0);  useEffect(() => {    const id = setInterval(() => {      setCount(count + 1);    }, 1000);    return () => clearInterval(id);  }, []);  return <h1>{count}</h1>;}

在这个例子中,·count的值永远都不会超过1因为定时器只会设置一次,而此时的count永远都是0,即永远都在执行setCount(0+1)

咱们必须扭转咱们的想法。所谓「依赖数组」是咱们用来通知React该Effect用到了什么状态。在这个例子中,咱们对React扯谎了,通知React说没有用到任何组件内的值。但实际上依赖了count

要诚恳的通知React咱们正确的依赖的两种方法

1. 将所有Effect用到的依赖都通知给React

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

当初依赖正确了,然而会有一个问题,就是每一次执行setCount的时候,都会导致计数器的革除和新建。这并不合乎咱们的初衷。

2. 批改effect外部的代码以确保它蕴含的值只会在须要的时候产生变更。

咱们不想告知谬误的依赖 - 咱们只是批改effect使得依赖更少。

持续看会下面的示例(将count传入依赖数组)。咱们须要思考,咱们为什么须要用到count?咱们想要通过setCount来更新count的值。

但其实在这个场景,咱们并不需要获取count的值,咱们只须要获取上一次的状态进行解决即可。因而能够写成

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

函数式更新

对于Effect来说,咱们能够用「同步」的思维去了解。

以下面这个例子来说,同步的一个乏味的中央就是将同步信息和状态解耦。相似于setCount(c => c + 1)这样的更新模式比setCount(count + 1)传递了更少的信息,因为它不再被以后的count值“净化”。它只是表白了一种行为(“递增”)。

然而,setCount(c => c + 1)并不完满,例如咱们有两个状态相互依赖,即下一次状态不仅仅依赖上一次状态,它就不能做到了。

这时候,useReducer大哥就要出场了。

useReducer

咱们对下面例子做一些批改:

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))} />    </>  );}

当初count的值不仅仅依赖本人上一次的值,还依赖step状态。

雷同的,目前咱们并没有对React说谎,因而它能够正确运行。但问题在于每一次扭转step后,计时器都会被销毁重建。

当你想更新一个状态,并且这个状态更新依赖于另一个状态的值时,你可能须要用useReducer去替换它们。

当你写相似setSomething(something => ...)这种代码的时候,兴许就是思考应用reducer的契机。reducer能够让你把组件内产生了什么(actions)和状态如何响应并更新离开表述。

function Counter() {  const [state, dispatch] = useReducer(reducer, initialState);  const { count, step } = state;  useEffect(() => {    const id = setInterval(() => {      dispatch({ type: 'tick' }); // 更新count    }, 1000);    return () => clearInterval(id);  }, [dispatch]);  return (    <>      <h1>{count}</h1>      <input value={step} onChange={e => {      // 更新step        dispatch({          type: 'step',          step: Number(e.target.value)        });      }} />    </>  );}function 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 };  } else {    throw new Error();  }}

应用useReducer能够完满解决上述问题。这是为什么呢?

答案在于<span style="color:#d2568c;font-weight:bold;">「React会保障dispatch在组件的申明周期内放弃不变」</span>。所以下面例子中不再须要从新订阅定时器。

(你能够从依赖中去除dispatch, setState, 和useRef包裹的值因为React会确保它们是动态的。不过你设置了它们作为依赖也没什么问题。)

与其在Effect中去获取状态,不如只是dispatch一个action来形容行为,这使得Effect与状态解耦,Effect再也不必关怀具体的状态了~

应用useRuducer真的是在舞弊

当咱们的step是props传进来的又会怎么呢?咱们还是能够应用useReducer,但这种状况下咱们的reducer函数就得写进组件中,因为要获取props。

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呢?

答案是:Effect只是dispatch了行为 - 它会在下一次渲染中去调用reducer,这是就能够获取到最新的props了。留神:「reducer不是在Effect中调用的」

永远要记住useEffect的执行机会

useEffect的执行机会在Dom渲染结束后。而且它是异步执行的,不会阻塞的。因而在上面例子中,会呈现页面闪动的成果。(0 -> 12 -> 2)

export default function FuncCom () {    const [counter, setCounter] = useState(0);    useEffect(() => {        if (counter === 12) {            // 为了演示,这里同步设置一个延时函数 500ms            delay()            setCounter(2)        }    });    return (        <div style={{            fontSize: '100px'        }}>            <div onClick={() => setCounter(12)}>{counter}</div>        </div>    )}

换成了 useLayoutEffect 后,屏幕上只会呈现 0 和 2,这是因为 useLayoutEffect同步个性,会在浏览器渲染之前同步更新react DOM 数据,哪怕是屡次的操作,也会在渲染前一次性解决完,再交给浏览器绘制。这样不会导致闪屏景象产生。

将函数放进Effect中

首先抛出问题:函数真的不应该成为依赖项嘛?

咱们来看一个许多程序员都会写的一个案例:

function SearchResults() {  const [data, setData] = useState({ hits: [] });  async function fetchData() {    const result = await axios(      'https://hn.algolia.com/api/v1/search?query=react',    );    setData(result.data);  }  useEffect(() => {    fetchData();  }, []); // Is this okay?  // ...}

该代码是能够失常运行的,然而它的拓展性十分差。试想一下,在我的项目代码一直增长下,咱们可能会拆散出很多个很长的函数,可能会有其余的依赖:

function SearchResults() {  const [query, setQuery] = useState('react');  // Imagine this function is also long  function getFetchUrl() {    return 'https://hn.algolia.com/api/v1/search?query=' + query;  }  // Imagine this function is also long  async function fetchData() {    const result = await axios(getFetchUrl());    setData(result.data);  }  useEffect(() => {    fetchData();  }, []);  // ...}

如果咱们遗记更新Effect的依赖数组的话,这不仅仅坑骗了React,而且导致Effect不会同步所依赖的state和props。

一个最简略的解决方案是,如果某些函数仅在effect中调用,你能够把它们的定义移到effect中:。这样的话程序员不须要思考这些“间接依赖”,而且在增加query状态的时候,意识到Effect依赖于它。

function SearchResults() {  const [query, setQuery] = useState('react');  useEffect(() => {  // ❇️ get together    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  // ...}

咱们必须意识到:·useEffect设计用意就是要强制你去关注数据流的变动,而后决定怎么去让Effect同步!!

但我不能将函数放进Effect中

举个例子,比方某函数被多个Effect应用到了。

首先咱们要明确一点,在组件中定义的函数每一次渲染都是变动的,但这个事实也给咱们带来了问题。比方:

function SearchResults() {  function getFetchUrl(query) {    return 'https://hn.algolia.com/api/v1/search?query=' + query;  }  useEffect(() => {    const url = getFetchUrl('react');    // ... Fetch data and do something ...  }, []); //  Missing dep: getFetchUrl  useEffect(() => {    const url = getFetchUrl('redux');    // ... Fetch data and do something ...  }, []); //  Missing dep: getFetchUrl  // ...}

一方面咱们不想在每一个Effect中都复制雷同的代码,但这又是对React的不诚实~,另一方面假如咱们将getFetchUrl当作依赖项,但它每一次渲染都在变,咱们的依赖数组就没有施展用途了。

这里有两个解决办法:

  1. 如果函数并没有用到组件内的值,咱们能够大胆地将其移到组件内部!
  2. 应用useCallBackhook包裹。

useCallBack实质上只是加了一层依赖查看,使得函数自身只有在须要时才扭转,这样咱们也不须要去掉函数依赖。

当函数用到了组件内的值,比方query能够通过输入框由用户去扭转,咱们能够这样做:

function SearchResults() {  const [query, setQuery] = useState('');  const getFetchUrl = useCallback(() => {    return 'https://hn.algolia.com/api/v1/search?query=' + query;  }, [query]); // ✅ Deps are OK  useEffect(() => {    const url = getFetchUrl();    // ... Fetch data and do something ...  }, [getFetchUrl]); // ✅ Deps are OK  useEffect(() => {    const url = getFetchUrl();    // ... Fetch data and do something ...  }, [getFetchUrl]); // ✅ Deps are OK  // ...}

这才是拥抱React数据流和同步思维的最终后果。query扭转 -> getFetchUrl扭转 -> Effect从新执行。反之,如果query不变,则Effect将不会从新执行

函数是数据流的一部分吗?

乏味的是,这种模式在class组件中是行不通的。

class Parent extends Component {  state = {    query: 'react'  };  fetchData = () => {    const url = 'https://hn.algolia.com/api/v1/search?query=' + this.state.query;    // ... Fetch data and do something ...  };  render() {    return <Child fetchData={this.fetchData} />;  }}class Child extends Component {  state = {    data: null  };  componentDidMount() {    this.props.fetchData();  }  componentDidUpdate(prevProps) {    //  This condition will never be true    if (this.props.fetchData !== prevProps.fetchData) {      this.props.fetchData();    }  }  render() {    // ...  }}

在class组件中,类办法this.fetchData是永远不会变的

为了解决这个问题,咱们必须将query参数传给Child组件即便它并没有间接应用到query

class Child extends Component {  componentDidUpdate(prevProps) {    //  This condition will never be true    if (this.props.query !== prevProps.query) {      this.props.fetchData();    }}
在class组件中,函数属性自身并不是数据流的一部分。组件的办法中蕴含了可变的this变量导致咱们不能确定无疑地认为它是不变的。因而,即便咱们只须要一个函数,咱们也必须把一堆数据传递上来仅仅是为了做“diff”。咱们无奈晓得传入的this.props.fetchData 是否依赖状态,并且不晓得它依赖的状态是否扭转了。

然而感激useCallback,它让函数自身也参加进了React数据流,咱们能够说函数的输出变了,那么函数自身就变了。(函数的输出须要咱们来定义)

须要强调的是:当咱们须要将函数传递给子组件并且在子组件的Effect中使用的话,最好应用useCallback将其包裹。

对于竞态

上面是一个class组件发申请的案例

class Article extends Component {  state = {    article: null  };  componentDidMount() {    this.fetchData(this.props.id);  }  async fetchData(id) {    const article = await API.fetchArticle(id);    this.setState({ article });  }  // ...}

心细的小伙伴可能发现了,下面代码并没有思考到id更新的状况,于是:

 componentDidUpdate(prevProps) {    if (prevProps.id !== this.props.id) {      this.fetchData(this.props.id);    }  }

然而这真的能够完满解决所有问题嘛?

咱们能够试想一下,比方我先申请 {id: 10},而后更新到{id: 20},但{id: 20}的申请更先返回。申请更早但返回更晚的状况会谬误地笼罩状态值。这就是「竞态」

最好的权宜之计就是利用一个boolean来去记录以后申请状态。

更多相干常识能够看这篇文章

结尾

我是<span style="color:#87481f;font-weight:bold">「盐焗乳鸽还要香锅」</span>,喜爱我的文章欢送关注噢

  • github 链接https://github.com/1360151219
  • 博客链接是 strk2.cn
  • 掘金账号、知乎账号、简书《盐焗乳鸽还要香锅》
  • 思否账号《天天摸鱼真的爽》

本文由mdnice多平台公布