初學React useEffect Hook

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