如何解決 React.useEffect() 的無限循環(huán)
useEffect() 主要用來管理副作用,比如通過網絡抓取、直接操作 DOM、啟動和結束計時器。
雖然useEffect() 和 useState(管理狀態(tài)的方法)是最常用的鉤子之一,但需要一些時間來熟悉和正確使用。
使用useEffect()時,你可能會遇到一個陷阱,那就是組件渲染的無限循環(huán)。在這篇文章中,會講一下產生無限循環(huán)的常見場景以及如何避免它們。
1. 無限循環(huán)和副作用更新狀態(tài)
假設我們有一個功能組件,該組件里面有一個 input 元素,組件是功能是計算 input 更改的次數。
我們給這個組件取名為 CountInputChanges,大概的內容如下:
- 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 狀態(tài)。
這里使用useEffect()更新count變量。每次由于用戶輸入而導致組件重新渲染時,useEffect(() => setCount(count + 1))就會更新計數器。
因為useEffect(() => setCount(count + 1))是在沒有依賴參數的情況下使用的,所以()=> setCount(count + 1)會在每次渲染組件后執(zhí)行回調。
你覺得這樣寫會有問題嗎?打開演示自己試試看:https://codesandbox.io/s/infinite-loop-9rb8c?file=/src/App.js
運行了會發(fā)現count狀態(tài)變量不受控制地增加,即使沒有在input中輸入任何東西,這是一個無限循環(huán)。 
問題在于useEffect()的使用方式:
- useEffect(() => setCount(count + 1));
 
它生成一個無限循環(huán)的組件重新渲染。
在初始渲染之后,useEffect()執(zhí)行更新狀態(tài)的副作用回調函數。狀態(tài)更新觸發(fā)重新渲染。重新渲染之后,useEffect()執(zhí)行副作用回調并再次更新狀態(tài),這將再次觸發(fā)重新渲染。

1.1通過依賴來解決
無限循環(huán)可以通過正確管理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]發(fā)生變化時,計數狀態(tài)變量才會更新。這樣做可以解決無限循環(huán)。

1.2 使用 ref
除了依賴,我們還可以通過 useRef() 來解決這個問題。
其思想是更新 Ref 不會觸發(fā)組件的重新渲染。
- 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++就會返回。引用更改本身不會觸發(fā)組件重新渲染。
2. 無限循環(huán)和新對象引用
即使正確設置了useEffect()依賴關系,使用對象作為依賴關系時也要小心。
例如,下面的組件CountSecrets監(jiān)聽用戶在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/infinite-loop-obj-dependency-7t26v?file=/src/App.js)自己試試,當前輸入 secret,secret.countSecrets的值就開始不受控制地增長。
這是一個無限循環(huán)問題。
為什么會這樣?
secret對象被用作useEffect(..., [secret])。在副作用回調函數中,只要輸入值等于secret,就會調用更新函數
- setSecret(s => ({...s, countSecrets: s.countSecrets + 1}));
 
這會增加countSecrets的值,但也會創(chuàng)建一個新對象。
secret現在是一個新對象,依賴關系也發(fā)生了變化。所以useEffect(..., [secret])再次調用更新狀態(tài)和再次創(chuàng)建新的secret對象的副作用,以此類推。
JavaScript 中的兩個對象只有在引用完全相同的對象時才相等。
2.1 避免將對象作為依賴項
解決由循環(huán)創(chuàng)建新對象而產生的無限循環(huán)問題的最好方法是避免在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!
 
修復
僅在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)是在組件渲染后執(zhí)行callback(副作用)的 Hook。如果不注意副作用的作用,可能會觸發(fā)組件渲染的無限循環(huán)。
生成無限循環(huán)的常見情況是在副作用中更新狀態(tài),沒有指定任何依賴參數
- useEffect(() => {
 - // Infinite loop!
 - setState(count + 1);
 - });
 
避免無限循環(huán)的一種有效方法是正確設置依賴項:
- useEffect(() => {
 - // No infinite loop
 - setState(count + 1);
 - }, [whenToUpdateValue]);
 
另外,也可以使用 Ref,更新 Ref 不會觸發(fā)重新渲染:
- useEffect(() => {
 - // No infinite loop
 - countRef.current++;
 - });
 
無限循環(huán)的另一種常見方法是使用對象作為useEffect()的依賴項,并在副作用中更新該對象(有效地創(chuàng)建一個新對象)
- useEffect(() => {
 - // Infinite loop!
 - setObject({
 - ...object,
 - prop: 'newValue'
 - })
 - }, [object]);
 
避免使用對象作為依賴項,只使用特定的屬性(最終結果應該是一個原始值):
- useEffect(() => {
 - // No infinite loop
 - setObject({
 - ...object,
 - prop: 'newValue'
 - })
 - }, [object.whenToUpdateProp]);
 
當使用useEffect()時,你還知道有其它方式會引起無限循環(huán)陷阱嗎?
~完,我是小智,我們下期見~
作者:Shadeed 譯者:前端小智 來源:dmitripavlutin
原文:https://dmitripavlutin.com/react-useeffect-infinite-loop/

















 
 
 








 
 
 
 