如何解決 React.useEffect() 的無限循環

做者:Shadeed
譯者:前端小智
來源:dmitripavlutin

點贊再看,微信搜索大遷世界,B站關注前端小智這個沒有大廠背景,但有着一股向上積極心態人。本文 GitHub https://github.com/qq44924588... 上已經收錄,文章的已分類,也整理了不少個人文檔,和教程資料。javascript

最近開源了一個 Vue 組件,還不夠完善,歡迎你們來一塊兒完善它,也但願你們能給個 star 支持一下,謝謝各位了。html

github 地址:https://github.com/qq44924588...前端

useEffect() 主要用來管理反作用,好比經過網絡抓取、直接操做 DOM、啓動和結束計時器。vue

雖然useEffect()useState(管理狀態的方法)是最經常使用的鉤子之一,但須要一些時間來熟悉和正確使用。java

使用useEffect()時,你可能會遇到一個陷阱,那就是組件渲染的無限循環。在這篇文章中,會講一下產生無限循環的常見場景以及如何避免它們。react

1. 無限循環和反作用更新狀態

假設咱們有一個功能組件,該組件裏面有一個 input 元素,組件是功能是計算 input 更改的次數。git

咱們給這個組件取名爲 CountInputChanges,大概的內容以下:github

function CountInputChanges() {
  const [value, setValue] = useState('');
  const [count, setCount] = useState(-1);

 useEffect(() => setCount(count + 1));
  const onChange = ({ target }) => setValue(target.value);

  return (
    <div>
 <input type="text" value={value} onChange={onChange} />
 <div>Number of changes: {count}</div>
 </div>
  )
}

<input type =「 text」 value = {value} onChange = {onChange} />是受控組件。value變量保存着 input 輸入的值,當用戶輸入輸入時,onChange事件處理程序更新 value 狀態。面試

這裏使用useEffect()更新count變量。每次因爲用戶輸入而致使組件從新渲染時,useEffect(() => setCount(count + 1))就會更新計數器。微信

由於useEffect(() => setCount(count + 1))是在沒有依賴參數的狀況下使用的,因此()=> setCount(count + 1)會在每次渲染組件後執行回調。

你以爲這樣寫會有問題嗎?打開演示本身試試看:https://codesandbox.io/s/infi...

運行了會發現count狀態變量不受控制地增長,即便沒有在input中輸入任何東西,這是一個無限循環。

image

問題在於useEffect()的使用方式:

useEffect(() => setCount(count + 1));

它生成一個無限循環的組件從新渲染。

在初始渲染以後,useEffect()執行更新狀態的反作用回調函數。狀態更新觸發從新渲染。從新渲染以後,useEffect()執行反作用回調並再次更新狀態,這將再次觸發從新渲染。

image.png

1.1經過依賴來解決

無限循環能夠經過正確管理useEffect(callback, dependencies)依賴項參數來修復。

由於咱們但願count在值更改時增長,因此能夠簡單地將value做爲反作用的依賴項。

import { useEffect, useState } from 'react';

function CountInputChanges() {
  const [value, setValue] = useState('');
  const [count, setCount] = useState(-1);

 useEffect(() => setCount(count + 1), [value]);
  const onChange = ({ target }) => setValue(target.value);

  return (
    <div>
 <input type="text" value={value} onChange={onChange} />
 <div>Number of changes: {count}</div>
 </div>
  );
}

添加[value]做爲useEffect的依賴,這樣只有當[value]發生變化時,計數狀態變量纔會更新。這樣作能夠解決無限循環。

image.png

1.2 使用 ref

除了依賴,咱們還能夠經過 useRef() 來解決這個問題。

其思想是更新 Ref 不會觸發組件的從新渲染。

import { useEffect, useState, useRef } from "react";

function CountInputChanges() {
  const [value, setValue] = useState("");
  const countRef = useRef(0);

 useEffect(() => countRef.current++);
  const onChange = ({ target }) => setValue(target.value);

  return (
    <div>
 <input type="text" value={value} onChange={onChange} />
 <div>Number of changes: {countRef.current}</div>
 </div>
  );
}

useEffect(() => countRef.current++) 每次因爲value的變化而從新渲染後,countRef.current++就會返回。引用更改自己不會觸發組件從新渲染。

image.png

2. 無限循環和新對象引用

即便正確設置了useEffect()依賴關係,使用對象做爲依賴關係時也要當心。

例如,下面的組件CountSecrets監聽用戶在input中輸入的單詞,一旦用戶輸入特殊單詞'secret',統計 'secret' 的次數就會加 1。

import { useEffect, useState } from "react";

function CountSecrets() {
  const [secret, setSecret] = useState({ value: "", countSecrets: 0 });

  useEffect(() => {
    if (secret.value === 'secret') {
 setSecret(s => ({...s, countSecrets: s.countSecrets + 1}));    }
 }, [secret]);
  const onChange = ({ target }) => {
    setSecret(s => ({ ...s, value: target.value }));
  };

  return (
    <div>
 <input type="text" value={secret.value} onChange={onChange} />
 <div>Number of secrets: {secret.countSecrets}</div>
 </div>
  );
}

打開演示(https://codesandbox.io/s/infi...)本身試試,當前輸入 secretsecret.countSecrets的值就開始不受控制地增加。

這是一個無限循環問題。

爲何會這樣?

secret對象被用做useEffect(..., [secret])。在反作用回調函數中,只要輸入值等於secret,就會調用更新函數

setSecret(s => ({...s, countSecrets: s.countSecrets + 1}));

這會增長countSecrets的值,但也會建立一個新對象。

secret如今是一個新對象,依賴關係也發生了變化。因此useEffect(..., [secret])再次調用更新狀態和再次建立新的secret對象的反作用,以此類推。

JavaScript 中的兩個對象只有在引用徹底相同的對象時才相等。

2.1 避免將對象做爲依賴項

解決由循環建立新對象而產生的無限循環問題的最好方法是避免在useEffect()dependencies參數中使用對象引用。

let count = 0;

useEffect(() => {
  // some logic
}, [count]); // Good!
let myObject = {
  prop: 'Value'
};

useEffect(() => {
  // some logic
}, [myObject]); // Not good!
useEffect(() => {
  // some logic
}, [myObject.prop]); // Good!

修復<CountSecrets>組件的無限循環問題,能夠將useEffect(..., [secret])) 變爲 useEffect(..., [secret.value])

僅在secret.value更改時調用反作用回調就足夠了,下面是修復後的代碼:

import { useEffect, useState } from "react";

function CountSecrets() {
  const [secret, setSecret] = useState({ value: "", countSecrets: 0 });

  useEffect(() => {
    if (secret.value === 'secret') {
      setSecret(s => ({...s, countSecrets: s.countSecrets + 1}));
    }
 }, [secret.value]);
  const onChange = ({ target }) => {
    setSecret(s => ({ ...s, value: target.value }));
  };

  return (
    <div>
 <input type="text" value={secret.value} onChange={onChange} />
 <div>Number of secrets: {secret.countSecrets}</div>
 </div>
  );
}

3 總結

useEffect(callback, deps)是在組件渲染後執行callback(反作用)的 Hook。若是不注意反作用的做用,可能會觸發組件渲染的無限循環。

生成無限循環的常見狀況是在反作用中更新狀態,沒有指定任何依賴參數

useEffect(() => {
  // Infinite loop!
  setState(count + 1);
});

避免無限循環的一種有效方法是正確設置依賴項:

useEffect(() => {
  // No infinite loop
  setState(count + 1);
}, [whenToUpdateValue]);

另外,也可使用 Ref,更新 Ref 不會觸發從新渲染:

useEffect(() => {
  // No infinite loop
  countRef.current++;
});

無限循環的另外一種常見方法是使用對象做爲useEffect()的依賴項,並在反作用中更新該對象(有效地建立一個新對象)

useEffect(() => {
  // Infinite loop!
  setObject({
    ...object,
    prop: 'newValue'
  })
}, [object]);

避免使用對象做爲依賴項,只使用特定的屬性(最終結果應該是一個原始值):

useEffect(() => {
  // No infinite loop
  setObject({
    ...object,
    prop: 'newValue'
  })
}, [object.whenToUpdateProp]);

當使用useEffect()時,你還知道有其它方式會引發無限循環陷阱嗎?

~完,我是小智,咱們下期見~


代碼部署後可能存在的BUG無法實時知道,過後爲了解決這些BUG,花了大量的時間進行log 調試,這邊順便給你們推薦一個好用的BUG監控工具 Fundebug

原文:https://dmitripavlutin.com/re...

交流

文章每週持續更新,能夠微信搜索「 大遷世界 」第一時間閱讀和催更(比博客早一到兩篇喲),本文 GitHub https://github.com/qq449245884/xiaozhi 已經收錄,整理了不少個人文檔,歡迎Star和完善,你們面試能夠參照考點複習,另外關注公衆號,後臺回覆福利,便可看到福利,你懂的。

相關文章
相關標籤/搜索