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

1.1通過(guò)依賴(lài)來(lái)解決
無(wú)限循環(huán)可以通過(guò)正確管理useEffect(callback, dependencies)依賴(lài)項(xiàng)參數(shù)來(lái)修復(fù)。
因?yàn)槲覀兿M鹀ount在值更改時(shí)增加,所以可以簡(jiǎn)單地將value作為副作用的依賴(lài)項(xiàng)。
- 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的依賴(lài),這樣只有當(dāng)[value]發(fā)生變化時(shí),計(jì)數(shù)狀態(tài)變量才會(huì)更新。這樣做可以解決無(wú)限循環(huán)。

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