React useEffect使用教程
這篇文章會假設(shè)你對useEffectAPI有一定程度的了解。
一、每一次渲染都有它自己的 Props and State
在我們討論 effects 之前,我們需要先討論一下渲染,當(dāng)我們更新 state 的時候,React會重新渲染組件。每一次渲染都能拿到獨(dú)立的 state,這個狀態(tài)值是函數(shù)中的一個常量。
這里關(guān)鍵的點(diǎn)在于任意一次渲染中的常量都不會隨著時間改變。渲染輸出會變是因?yàn)槲覀兊慕M件被一次次調(diào)用,而每一次調(diào)用引起的渲染中,它包含的值獨(dú)立于其他渲染。
如果 props 和 state 在不同的渲染中是相互獨(dú)立的,那么使用到它們的任何值也是獨(dú)立的(包括事件處理函數(shù))。它們都“屬于”一次特定的渲染。即便是事件處理中的異步函數(shù)調(diào)用“看到”的也是這次渲染中的值。
二、每次渲染都有它自己的Effects
讓我們先看向官網(wǎng)的 useEffect 的例子:
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `You clicked ${count} times`;
});
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}effect是如何讀取到最新的count狀態(tài)值的呢?
也許,是某種 watching 機(jī)制類似 vue 中的數(shù)據(jù)響應(yīng)式使得能夠在 effect 函數(shù)內(nèi)更新?也或許是一個可變的值,React 會在我們組件內(nèi)部修改它以使我們的 effect 函數(shù)總能拿到最新的值?
都不是。
我們已經(jīng)知道是某個特定渲染中的常量。事件處理函數(shù)“看到”的是屬于它那次特定渲染中的狀態(tài)值。對于 effects 也同樣如此:
并不是count的值在“不變”的 effect 中發(fā)生了改變,而是 effect 函數(shù)本身在每一次渲染中都不相同。
React 會記住你提供的 effect 函數(shù),并且會在每次更改作用于DOM并讓瀏覽器繪制屏幕后去調(diào)用它。
所以雖然我們說的是一個 effect(這里指更新document的title),但其實(shí)每次渲染都是一個不同的函數(shù)— 并且每個 effect 函數(shù)看到的 props 和 state 都來自于它屬于的那次特定渲染。
三、關(guān)于依賴項(xiàng)不要對React撒謊
現(xiàn)在只需要記住:如果你設(shè)置了依賴項(xiàng),effect 中用到的所有組件內(nèi)的值都要包含在依賴中。這包括props,state,函數(shù)組件內(nèi)的任何東西。
在下面這個組件中,我們的直覺是:“開啟一次定時器,清除也是一次”。直覺上我們會設(shè)置依賴為 '[]'。“我只想運(yùn)行一次 effect ”,但是這樣對嗎?
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setCount(count + 1);
}, 1000);
return () => clearInterval(id);
}, []);
return <h1>{count}</h1>;
}我們以為他會一直遞增下去,但實(shí)際上他只會遞增一次,你想要觸發(fā)一次因?yàn)樗嵌〞r器 ,但為什么會有問題?
在第一次渲染中我們執(zhí)行了setCount(0 + 1)。但是我們設(shè)置了[]依賴,effect不會再重新運(yùn)行,它后面每一秒都會調(diào)用setCount(0 + 1)。我們對 React 撒謊說我們的 effect 不依賴組件內(nèi)的任何值,可實(shí)際上我們的 effect 有依賴。
四、兩種誠實(shí)告知依賴的方法
第一種策略是在依賴中包含所有 effect 中用到的組件內(nèi)的值。讓我們在依賴中包含:count
useEffect(() => {
const id = setInterval(() => {
setCount(count + 1);
}, 1000);
return () => clearInterval(id);
}, [count]);這在我們大部分初級開發(fā)者的眼中都沒有什么問題,并且程序確實(shí)不會出任何 bug,現(xiàn)在,每次修改都會重新運(yùn)行 effect,這能解決問題但是我們的定時器會在每一次改變后清除和重新設(shè)定。這肯定不是我們想要的結(jié)果。
第二種策略是修改 effect 內(nèi)部的代碼以確保它包含的值只會在需要的時候發(fā)生變更。
在這個場景中,我們其實(shí)并不需要在effect中使用 count。當(dāng)我們想要根據(jù)前一個狀態(tài)更新狀態(tài)的時候,我們可以使用的函數(shù)形式:
useEffect(() => {
const id = setInterval(() => {
setCount(c => c + 1);
}, 1000);
return () => clearInterval(id);
}, []);我們需要告知React的僅僅是去遞增狀態(tài) - 不管它現(xiàn)在具體是什么值。注意我們做到了移除依賴,并且沒有撒謊。我們的 effect 不再讀取渲染中的count值。
五、來自useReducer的助攻
如果我們有兩個互相依賴的狀態(tài),或者我們想基于一個 prop 來計(jì)算下一次的 state,setCount(c => c + 1)它并不能做到。幸運(yùn)的是,有一個更強(qiáng)大的姐妹模式,它的名字叫useReducer。
我們先來修改上面的例子讓它包含兩個狀態(tài):count 和 step 。我們的定時器會每次在 count 上增加一個 step 值:
function Counter() {
const [count, setCount] = useState(0);
const [step, setStep] = useState(1);
useEffect(() => {
const id = setInterval(() => {
setCount(c => c + step);
}, 1000);
return () => clearInterval(id);
}, [step]);
return (
<>
<h1>{count}</h1>
<input value={step} onChange={e => setStep(Number(e.target.value))} />
</>
);
}注意我們沒有撒謊。既然我們在 effect 里使用了step,我們就把它加到依賴?yán)?。所以這也是為什么代碼能運(yùn)行正確。
這個例子目前的行為是修改會重啟定時器 - 因?yàn)樗且蕾図?xiàng)之一。在大多數(shù)場景下,這正是你所需要的。清除上一次的effect然后重新運(yùn)行新的effect并沒有任何錯。不過,假如我們不想在改變后重啟定時器,我們該如何從effect中移除對的依賴呢?
下面這句話我希望你作為一名 react 開發(fā)人員要記下來:
當(dāng)你想更新一個狀態(tài),并且這個狀態(tài)更新依賴于另一個狀態(tài)的值時,你可能需要用useReducer去替換它們。
reducer 可以讓你把組件內(nèi)發(fā)生了什么(actions)和狀態(tài)如何響應(yīng)并更新分開表述。
我們用一個dispatch依賴去替換 effect 的依賴 step:
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
const { count, step } = state;
useEffect(() => {
const id = setInterval(() => {
dispatch({ type: 'tick' });
}, 1000);
return () => clearInterval(id);
}, [dispatch]);
return (
<>
<h1>{count}</h1>
<input value={step} onChange={e => {
dispatch({
type: 'step',
step: Number(e.target.value)
});
}} />
</>
);
}
const initialState = {
count: 0,
step: 1,
};
function reducer(state, action) {
const { count, step } = state;
if (action.type === 'tick') {
return { count: count + step, step };
} else if (action.type === 'step') {
return { count, step: action.step };
} else {
throw new Error();
}
}你可能會問:“這怎么就更好了?”答案是React會保證dispatch在組件的聲明周期內(nèi)保持不變。所以上面例子中不再需要重新訂閱定時器。
相比于直接在 effect 里面讀取狀態(tài),它 dispatch 了一個action來描述發(fā)生了什么。這使得我們的 effect 和狀態(tài)解耦。我們的 effect 不再關(guān)心怎么更新狀態(tài),它只負(fù)責(zé)告訴我們發(fā)生了什么。更新的邏輯全都交由 reducer 去統(tǒng)一處理。
六、把函數(shù)移到Effects里
一個典型的誤解是認(rèn)為函數(shù)不應(yīng)該成為依賴。舉個例子,下面的代碼看上去可以運(yùn)行正常:
function SearchResults() {
const [data, setData] = useState({ hits: [] });
async function fetchData() {
const result = await axios(
'https://hn.algolia.com/api/v1/search?query=react',
);
setData(result.data);
}
useEffect(() => {
fetchData();
}, []);
// ...需要明確的是,上面的代碼可以正常工作。但這樣做在組件日漸復(fù)雜的迭代過程中我們很難確保它在各種情況下還能正常運(yùn)行。
如果我們在某些函數(shù)內(nèi)使用了某些 state 或者 prop:
function SearchResults() {
const [query, setQuery] = useState('react');
// Imagine this function is also long
function getFetchUrl() {
return 'https://hn.algolia.com/api/v1/search?query=' + query;
}
// Imagine this function is also long
async function fetchData() {
const result = await axios(getFetchUrl());
setData(result.data);
}
useEffect(() => {
fetchData();
}, []);
// ...
}如果我們忘記去更新使用這些函數(shù)(很可能通過其他函數(shù)調(diào)用)的effects的依賴,我們的effects就不會同步props和state帶來的變更。這當(dāng)然不是我們想要的。
如果某些函數(shù)僅在effect中調(diào)用,你可以把它們的定義移到effect中:
function SearchResults() {
// ...
useEffect(() => {
// We moved these functions inside!
function getFetchUrl() {
return 'https://hn.algolia.com/api/v1/search?query=react';
}
async function fetchData() {
const result = await axios(getFetchUrl());
setData(result.data);
}
fetchData();
}, []);
}這么做有什么好處呢?我們不再需要去考慮這些“間接依賴”。我們的依賴數(shù)組也不再撒謊:在我們的 effect 中確實(shí)沒有再使用組件范圍內(nèi)的任何東西。
如果我們后面修改getFetchUrl去使用狀態(tài) query,我們更可能會意識到我們正在effect里面編輯它因此,我們需要把 query添加到effect的依賴?yán)铮?/p>
function SearchResults() {
const [query, setQuery] = useState('react');
useEffect(() => {
function getFetchUrl() {
return 'https://hn.algolia.com/api/v1/search?query=' + query;
}
async function fetchData() {
const result = await axios(getFetchUrl());
setData(result.data);
}
fetchData();
}, [query]);
}七、我不想把可復(fù)用的函數(shù)放到Effect里
有時候你可能不想把函數(shù)移入 effect 里。比如,組件內(nèi)有幾個 effect 使用了相同的函數(shù),你不想在每個 effect 里復(fù)制黏貼一遍這個邏輯。也或許這個函數(shù)是一個 prop。
在這種情況下你應(yīng)該忽略對函數(shù)的依賴嗎?這么做是不對的。再次強(qiáng)調(diào),effects不應(yīng)該對它的依賴撒謊。通常我們還有更好的解決辦法。一個常見的誤解是,“函數(shù)從來不會改變”。但是這篇文章你讀到現(xiàn)在,你知道這顯然不是事實(shí)。實(shí)際上,在組件內(nèi)定義的函數(shù)每一次渲染都在變。
function SearchResults() {
function getFetchUrl(query) {
return 'https://hn.algolia.com/api/v1/search?query=' + query;
}
useEffect(() => {
const url = getFetchUrl('react');
// ... Fetch data and do something ...
}, []);
useEffect(() => {
const url = getFetchUrl('redux');
// ... Fetch data and do something ...
}, []);
}在這個例子中,你可能不想把getFetchUrl移到 effects 中,因?yàn)槟阆霃?fù)用邏輯。
另一方面,如果你對依賴很“誠實(shí)”,你可能會掉到陷阱里。我們的兩個 effects 都依賴 getFetchUrl,而它每次渲染都不同,所以我們的依賴數(shù)組會變得無用:
function SearchResults() {
function getFetchUrl(query) {
return 'https://hn.algolia.com/api/v1/search?query=' + query;
}
useEffect(() => {
const url = getFetchUrl('react');
// ... Fetch data and do something ...
}, [getFetchUrl]);
useEffect(() => {
const url = getFetchUrl('redux');
// ... Fetch data and do something ...
}, [getFetchUrl]);
// ...
}我們有兩個更簡單的解決辦法。
第一個, 如果一個函數(shù)沒有使用組件內(nèi)的任何值,你應(yīng)該把它提到組件外面去定義,然后就可以自由地在 effects 中使用:
function getFetchUrl(query) {
return 'https://hn.algolia.com/api/v1/search?query=' + query;
}
function SearchResults() {
useEffect(() => {
const url = getFetchUrl('react');
// ... Fetch data and do something ...
}, []);
useEffect(() => {
const url = getFetchUrl('redux');
// ... Fetch data and do something ...
}, []);
// ...
}你不再需要把它設(shè)為依賴,因?yàn)樗鼈儾辉阡秩痉秶鷥?nèi),因此不會被數(shù)據(jù)流影響。
或者, 你也可以把它包裝成useCallback Hook:
function SearchResults() {
const getFetchUrl = useCallback((query) => {
return 'https://hn.algolia.com/api/v1/search?query=' + query;
}, []);
useEffect(() => {
const url = getFetchUrl('react');
// ... Fetch data and do something ...
}, [getFetchUrl]);
useEffect(() => {
const url = getFetchUrl('redux');
// ... Fetch data and do something ...
}, [getFetchUrl]);
// ...
}我們用 useCallback 對 getFetchUrl 做了一層緩存,現(xiàn)在只有當(dāng)依賴項(xiàng)變化的時候,才會重新執(zhí)行 useCallback 來返回新的函數(shù),依賴項(xiàng)沒有變化的時候就算組件 rerender 了,這個函數(shù)也不會重新執(zhí)行,這樣我們把 getFetchUrl 作為 useEffect 的依賴就沒問題了。
不同于傳遞參數(shù)的方式,現(xiàn)在我們從狀態(tài)中讀取 query:
function SearchResults() {
const [query, setQuery] = useState('react');
const getFetchUrl = useCallback(() => {
return 'https://hn.algolia.com/api/v1/search?query=' + query;
}, [query]);
useEffect(() => {
const url = getFetchUrl();
// ... Fetch data and do something ...
}, [getFetchUrl]);
// ...
}如果query保持不變,useCallback也會保持不變,我們的 effect 也不會重新運(yùn)行。但是如果修改了 query,useCallback 也會隨之改變,因此會重新請求數(shù)據(jù)。這就像你在Excel里修改了一個單元格的值,另一個使用它的單元格會自動重新計(jì)算一樣。
到此這篇關(guān)于React useEffect使用教程的文章就介紹到這了,更多相關(guān)React useEffect內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
react組件實(shí)例屬性props實(shí)例詳解
這篇文章主要介紹了react組件實(shí)例屬性props,本文結(jié)合實(shí)例代碼給大家簡單介紹了props使用方法,代碼簡單易懂,需要的朋友可以參考下2023-01-01
React中實(shí)現(xiàn)使用條件渲染來顯示不同的內(nèi)容
在React中,條件渲染是根據(jù)不同的條件選擇性地渲染組件,本文介紹了三種常見的條件渲染方式:使用if語句、使用三元運(yùn)算符和使用短路運(yùn)算符,通過這些方法,我們可以根據(jù)應(yīng)用的狀態(tài)和用戶的操作動態(tài)地顯示不同的內(nèi)容,從而提升用戶體驗(yàn)和組件的可維護(hù)性2025-02-02
詳解React如何使用??useReducer???高階鉤子來管理狀態(tài)
useReducer是React中的一個鉤子,用于替代?useState來管理復(fù)雜的狀態(tài)邏輯,本文將詳細(xì)介紹如何在React中使用?useReducer高階鉤子來管理狀態(tài),感興趣的可以了解下2025-02-02
深入理解React中es6創(chuàng)建組件this的方法
this的本質(zhì)可以這樣說,this跟作用域無關(guān)的,只跟執(zhí)行上下文有關(guān)。接下來通過本文給大家介紹React中es6創(chuàng)建組件this的方法,非常不錯,感興趣的朋友一起看看吧2016-08-08
react-native 實(shí)現(xiàn)購物車滑動刪除效果的示例代碼
這篇文章主要介紹了react-native 實(shí)現(xiàn)購物車滑動刪除效果的示例代碼,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-01-01

