初學(xué)React useEffect Hook

React Hooks 是從功能組件訪(fǎng)問(wèn) React 的狀態(tài)和生命周期方法的最佳方式。 useEffect Hook 是一個(gè)在渲染之后和每次 DOM 更新時(shí)運(yùn)行的函數(shù)(效果)。在本文中,將討論一些技巧以更好地使用 useEffect Hook。
通過(guò)項(xiàng)目來(lái)發(fā)現(xiàn)問(wèn)題,加深對(duì)其理解應(yīng)用到項(xiàng)目中。
項(xiàng)目GITHUB:https://github.com/QuintionTang/react-giant
開(kāi)始之前先簡(jiǎn)單來(lái)理解一下 useEffect 設(shè)計(jì)。
useEffect 設(shè)計(jì)
React 提供了一個(gè) useEffect 鉤子函數(shù)來(lái)設(shè)置在更新后的回調(diào):
const Title = () => {
useEffect(() => {
window.title = "Hello World";
return () => {
window.title = "NoTitle";
};
}, []);
};
useEffect 函數(shù)采用名為 create 的回調(diào)函數(shù)作為其第一個(gè)輸入?yún)?shù)來(lái)定義效果。上面的代碼,Effect 在安裝組件時(shí)將 window.title 設(shè)置為 Hello World。
create 函數(shù)可以返回一個(gè)名為 destroy 的函數(shù)來(lái)執(zhí)行清理。這里有趣的是 destroy 函數(shù)由 create 函數(shù)的返回值提供。在前面的示例中,清理將 window.title 對(duì)象在卸載時(shí)設(shè)置為 NoTitle。
useEffect 參數(shù)列表中的第二個(gè)參數(shù)是一個(gè)名為 deps 的依賴(lài)項(xiàng)數(shù)組。如果未設(shè)置 deps,則在每次更新期間每次都會(huì)調(diào)用 Effect,而當(dāng)給出 deps 時(shí),Effect 只會(huì)在 deps 數(shù)組發(fā)生更改時(shí)調(diào)用。
子組件 Effects 優(yōu)先觸發(fā)
將 useEffect Hook 視為 componentDidMount、componentDidUpdate 和 componentWillUnmount 的組合。所以 useEffect Hook 的行為類(lèi)似于類(lèi)生命周期方法。需要注意的一種行為是子回調(diào)在父回調(diào)之前觸發(fā)。
function ParentComponent() {
useEffect(() => {
console.log("我是父組件");
});
return <ChildComponent />;
}
function ChildComponent({ fetchProduct }) {
useEffect(() => {
console.log("我是子組件");
});
}
假設(shè)必須自動(dòng)觸發(fā)付款。這段代碼寫(xiě)在 render 之后運(yùn)行的子組件中,但是實(shí)際支付所需的詳細(xì)信息(總金額、折扣等)是在父組件的 effect 中獲取的。在這種情況下,由于在設(shè)置所需的詳細(xì)信息之前觸發(fā)了付款,因此就會(huì)出現(xiàn)實(shí)現(xiàn)邏輯不對(duì)。
因此在構(gòu)建代碼的時(shí)候需要考慮子組件的
useEffect會(huì)優(yōu)先執(zhí)行。
依賴(lài)數(shù)組
從基礎(chǔ)開(kāi)始。 useEffect Hook 接受第二個(gè)參數(shù),稱(chēng)為依賴(lài)數(shù)組,以控制回調(diào)何時(shí)觸發(fā)。
對(duì)每個(gè) DOM 更新運(yùn)行效果
不傳遞依賴(lài)項(xiàng)數(shù)組將在每次 DOM 更新時(shí)運(yùn)行回調(diào)。
useEffect(() => {
console.log("每次DOM更新時(shí),我都會(huì)被調(diào)用");
});
在初始渲染上運(yùn)行效果
傳入空數(shù)組僅在初始渲染后運(yùn)行效果。至此,狀態(tài)已更新為初始值。 DOM 中的進(jìn)一步更新不會(huì)調(diào)用此效果。
useEffect(() => {
console.log("我只在初始渲染后被調(diào)用一次");
}, []);
這類(lèi)似于 componentDidMount 和 componentWillUnmount(返回)生命周期方法。這是添加頁(yè)面所需的所有偵聽(tīng)器和訂閱的地方。
對(duì)特定 props 變化的運(yùn)行效果
假設(shè)必須根據(jù)用戶(hù)感興趣的產(chǎn)品來(lái)獲取數(shù)據(jù)(產(chǎn)品詳細(xì)信息),如,所選產(chǎn)品有一個(gè) productId,需要在每次 productId 更改時(shí)運(yùn)行回調(diào)——而不僅僅是在初始渲染或每次 DOM 更新時(shí)。
useEffect(() => {
getProductDetails(productId);
}, [productId]);
這基本上復(fù)制了 componentDidUpdate 生命周期方法。還可以將多個(gè)值傳遞給依賴(lài)數(shù)組。
一個(gè)經(jīng)典的反例可以幫助更好地理解這一點(diǎn):
useEffect(() => {
console.log(`當(dāng)counter1: ${counter1}或counter2: ${counter2}發(fā)生變化時(shí),我會(huì)被調(diào)用。`);
}, [counter1, counter2]);
在上面的示例中,counter1 或 counter2 中的更新將觸發(fā)。
在依賴(lài)數(shù)組中傳遞對(duì)象
現(xiàn)在,如果回調(diào)依賴(lài)是一個(gè)對(duì)象怎么辦。如果這樣做,effects 會(huì)成功運(yùn)行嗎?
const [productId, setProductId] = useState(0);
const [obj, setObj] = useState({ a: 1 });
useEffect(() => {
// 對(duì)`obj`的變化做些什么
}, [obj]);
答案是否定的,因?yàn)閷?duì)象是引用類(lèi)型。對(duì)象屬性的任何更改都不會(huì)被依賴(lài)項(xiàng)數(shù)組監(jiān)聽(tīng)到,因?yàn)橹粰z查引用而不檢查內(nèi)部的值。
可以遵循幾種方法在對(duì)象中執(zhí)行深度比較。
JSON.stringify對(duì)象:
const [objStringified, setObj] = useState(JSON.stringify({ a: 1 }));
useEffect(() => {
//
}, [objStringified]);
現(xiàn)在,useEffect 可以檢測(cè)到對(duì)象的屬性何時(shí)發(fā)生變化并按預(yù)期運(yùn)行。
- useRef 和 Lodash 進(jìn)行比較:
還可以編寫(xiě)自定義函數(shù)以使用 useRef 進(jìn)行比較。它用于在組件的當(dāng)前屬性中的整個(gè)生命周期中保存可變值。
function deepCompareEquals(prevVal, currentVal) {
return _.isEqual(prevVal, currentVal);
}
function useDeepCompareWithRef(value) {
const ref = useRef();
if (!deepCompareEquals(value, ref.current)) {
ref.current = value;
}
return ref.current;
}
function MyComponent({ obj }) {
useEffect(() => {
//
}, [useDeepCompareWithRef(obj)]);
}
- 外部packages:如果對(duì)象太復(fù)雜而無(wú)法自己進(jìn)行比較,推薦一個(gè)第三方庫(kù) use-deep-compare-effect:
import useDeepCompareEffect from "use-deep-compare-effect";
function MyComponent({ obj }) {
useDeepCompareEffect(() => {}, [obj]);
}
useDeepCompareEffect 將進(jìn)行深度比較并僅在對(duì)象 obj 更改時(shí)運(yùn)行回調(diào)。
將 useEffect 用于單一目的
上面了解了依賴(lài)數(shù)組,可能需要分離 useEffect 以在組件的不同生命周期事件上運(yùn)行,或者只是為了更清晰的代碼,函數(shù)應(yīng)該服務(wù)于單一目的(就像一個(gè)句子應(yīng)該只傳達(dá)一個(gè)想法一樣)。
將 useEffects 拆分為簡(jiǎn)短單一用途函數(shù)可以降低BUG的出現(xiàn)。例如,假設(shè)有與 varB 無(wú)關(guān)的 varA,并且想要基于 useEffect(帶有 setTimeout)構(gòu)建一個(gè)遞歸計(jì)數(shù)器,先來(lái)看一段不推薦的代碼:
import React, { useEffect, useState } from "react";
export default function Home() {
const [varA, setVarA] = useState(0);
const [varB, setVarB] = useState(0);
useEffect(() => {
const timeoutA = setTimeout(() => setVarA(varA + 1), 1000);
const timeoutB = setTimeout(() => setVarB(varB + 2), 2000);
return () => {
clearTimeout(timeoutA);
clearTimeout(timeoutB);
};
}, [varA, varB]);
return (
<>
<span>
Var A: {varA}, Var B: {varB}
</span>
</>
);
}
上述代碼,變量 varA 和 varB 中的任何一個(gè)更改都會(huì)觸發(fā)兩個(gè)變量的更新。這就是為什么這個(gè)鉤子不能正常工作的原因。由于這是一個(gè)簡(jiǎn)短的示例,可能會(huì)覺(jué)得它很明顯,但是,在具有更多代碼和變量的較長(zhǎng)函數(shù)中,會(huì)因此錯(cuò)過(guò)這一點(diǎn)。所以做正確的事并拆分 useEffect 的邏輯。
import React, { useEffect, useState } from "react";
export default function Home() {
const [varA, setVarA] = useState(0);
const [varB, setVarB] = useState(0);
useEffect(() => {
const timeout = setTimeout(() => setVarA(varA + 1), 1000);
return () => clearTimeout(timeout);
}, [varA]);
useEffect(() => {
const timeout = setTimeout(() => setVarB(varB + 2), 2000);
return () => clearTimeout(timeout);
}, [varB]);
return (
<>
<span>
Var A: {varA}, Var B: {varB}
</span>
</>
);
}
上述代碼僅為了說(shuō)明問(wèn)題,實(shí)際編碼有些地方可以用其他的方式。
盡可能使用自定義掛鉤
再次以上面的例子為例,如果變量 varA 和 varB 完全獨(dú)立怎么辦?在這種情況下,可以簡(jiǎn)單地創(chuàng)建一個(gè)自定義鉤子來(lái)隔離每個(gè)變量。這樣,就可以確切地知道每個(gè)函數(shù)對(duì)哪個(gè)變量做了什么。
下面就來(lái)構(gòu)建一些自定義鉤子。
import React, { useEffect, useState } from "react";
const useVarA = () => {
const [varA, setVarA] = useState(0);
useEffect(() => {
const timeout = setTimeout(() => setVarA(varA + 1), 1000);
return () => clearTimeout(timeout);
}, [varA]);
return [varA, setVarA];
};
const useVarB = () => {
const [varB, setVarB] = useState(0);
useEffect(() => {
const timeout = setTimeout(() => setVarB(varB + 2), 2000);
return () => clearTimeout(timeout);
}, [varB]);
return [varB, setVarB];
};
export default function Home() {
const [varA, setVarA] = useVarA();
const [varB, setVarB] = useVarB();
return (
<>
<span>
Var A: {varA}, Var B: {varB}
</span>
</>
);
}
這樣每個(gè)變量都有自己的鉤子,更易于維護(hù)和易于閱讀!
有條件地以正確的方式運(yùn)行 useEffect
關(guān)于 setTimeout ,再來(lái)看個(gè)例子:
import React, { useEffect, useState } from "react";
export default function Home() {
const [varA, setVarA] = useState(0);
useEffect(() => {
const timeout = setTimeout(() => setVarA(varA + 1), 1000);
return () => clearTimeout(timeout);
}, [varA]);
return (
<>
<span>Var A: {varA}</span>
</>
);
}
出于某種原因,想將計(jì)數(shù)器的最大值限制為 5。有正確的方法和錯(cuò)誤的方法。
先來(lái)看看錯(cuò)誤的做法:
import React, { useEffect, useState } from "react";
export default function Home() {
const [varA, setVarA] = useState(0);
useEffect(() => {
let timeout;
if (varA < 5) {
timeout = setTimeout(() => setVarA(varA + 1), 1000);
}
return () => clearTimeout(timeout);
}, [varA]);
return (
<>
<span>Var A: {varA}</span>
</>
);
}
雖然這有效,但 clearTimeout 將在 varA 發(fā)生更改時(shí)運(yùn)行,而 setTimeout 是有條件地運(yùn)行。
有條件地運(yùn)行 useEffect 的推薦方法是在函數(shù)開(kāi)頭執(zhí)行條件返回,如下所示:
import React, { useEffect, useState } from "react";
export default function Home() {
const [varA, setVarA] = useState(0);
useEffect(() => {
if (varA >= 5) return;
const timeout = setTimeout(() => setVarA(varA + 1), 1000);
return () => clearTimeout(timeout);
}, [varA]);
return (
<>
<span>Var A: {varA}</span>
</>
);
}
在依賴(lài)數(shù)組中輸入 useEffect 中的每個(gè)道具
如果正在使用 ESLint,那么可能已經(jīng)看到來(lái)自 ESLint exhaustive-deps 規(guī)則的警告。這是至關(guān)重要的,當(dāng)應(yīng)用程序變得越來(lái)越大時(shí),每個(gè) useEffect 中都會(huì)添加更多的依賴(lài)項(xiàng)(props)。為了跟蹤所有這些并避免陳舊的閉包,應(yīng)該將每個(gè)依賴(lài)項(xiàng)添加到依賴(lài)項(xiàng)數(shù)組中。
同樣,關(guān)于 setTimeout 的問(wèn)題,假設(shè)只想運(yùn)行一次 setTimeout 并添加到 varA:
import React, { useEffect, useState } from "react";
export default function Home() {
const [varA, setVarA] = useState(0);
useEffect(() => {
const timeout = setTimeout(() => setVarA(varA + 1), 1000);
return () => clearTimeout(timeout);
}, []); // 避免這種情況:varA 不在依賴(lài)數(shù)組中!
return (
<>
<span>Var A: {varA}</span>
</>
);
}
雖然上述代碼會(huì)正確執(zhí)行,但是如果代碼變得更大或者更復(fù)雜,可能就會(huì)帶來(lái)問(wèn)題。在這種情況下,需要將所有變量都映射出來(lái),因?yàn)檫@樣可以更容易地測(cè)試和檢測(cè)可能出現(xiàn)的問(wèn)題(例如過(guò)時(shí)的 props 和閉包)。
正確的做法應(yīng)該是:
import React, { useEffect, useState } from "react";
export default function Home() {
const [varA, setVarA] = useState(0);
useEffect(() => {
if (varA > 0) return;
const timeout = setTimeout(() => setVarA(varA + 1), 1000);
return () => clearTimeout(timeout);
}, [varA]);
return (
<>
<span>Var A: {varA}</span>
</>
);
}
總結(jié)
上面學(xué)習(xí)了什么是 useEffect ?如何更好的使用 useEffect?如果了解基本概念,那么使用 useEffect 就不會(huì)有任何問(wèn)題。學(xué)習(xí)的一些內(nèi)容講通過(guò)一個(gè)個(gè)人項(xiàng)目的形式逐漸完善,豐富功能模塊。
項(xiàng)目GITHUB:https://github.com/QuintionTang/react-giant