React Hooks 學(xué)習(xí)筆記 | useEffect Hook(二)

大家好,上一篇文章我們學(xué)習(xí)了 State Hook 的基礎(chǔ)用法,還沒看的同學(xué)們,小編建議你先看下《 React Hooks 學(xué)習(xí)筆記 | State Hook(一)》這篇文章,今天我們一起來了解 useEffect Hook 的基礎(chǔ)用法。
一、開篇
一般大多數(shù)的組件都需要特殊的操作,比如獲取數(shù)據(jù)、監(jiān)聽數(shù)據(jù)變化或更改DOM的相關(guān)操作,這些操作被稱作 “side effects(副作用)”。
在類組件中,我們通常會(huì)在 componentDidMount 和 componentDidUpdate 這兩個(gè)常用的生命鉤子函數(shù)進(jìn)行操作,這些生命周期的相關(guān)方法便于我們?cè)诤线m的時(shí)機(jī)更加精確的控制組件的行為。
這有一個(gè)簡(jiǎn)單的代碼示例,頁面加載完成后,更改頁面的標(biāo)題
componentDidMount() {
document.title = this.state.name + " from " + this.state.location;
}
當(dāng)你嘗試更改標(biāo)題對(duì)應(yīng)的狀態(tài)值時(shí),頁面的標(biāo)題不會(huì)發(fā)生任何變化,你還需要添加另一個(gè)生命周期的方法 componentDidUpdate() ,監(jiān)聽狀態(tài)值的變化重新re-render,示例代碼如下:
componentDidUpdate() {
document.title = this.state.name + " from " + this.state.location;
}
從上述代碼我們可以看出,要實(shí)現(xiàn)動(dòng)態(tài)更改頁面標(biāo)題的方法,我們需要調(diào)用兩個(gè)生命鉤子函數(shù),同樣的方法寫兩遍。但是我們使用 useEffect Hook 函數(shù),就能解決代碼重復(fù)的問題,示例代碼如下:
import React, { useState, useEffect } from "react";
//...
useEffect(() => {
document.title = name + " from " + location;
});
可以看出,使用 useEffect Hook ,我們就實(shí)現(xiàn)了兩個(gè)生命周期函數(shù)等效的目的,節(jié)省了代碼量。

二、添加清除功能
還有一個(gè)類組件的例子,在某些情況下,你需要在組件卸載(unmounted)或銷毀(destroyed)之前,做一些有必要的清除的操作,比如timers、interval,或者取消網(wǎng)絡(luò)請(qǐng)求,或者清理任何在componentDidMount()中創(chuàng)建的DOM元素(elements),你可能會(huì)想到類組件中的 componentWillUnmount()這個(gè)鉤子函數(shù),示例代碼如下:
import React from "react";
export default class ClassDemo extends React.Component {
constructor(props) {
super(props);
this.state = {
resolution: {
width: window.innerWidth,
height: window.innerHeight
}
};
this.handleResize = this.handleResize.bind(this);
}
componentDidMount() {
window.addEventListener("resize", this.handleResize);
}
componentDidUpdate() {
window.addEventListener("resize", this.handleResize);
}
componentWillUnmount() {
window.removeEventListener('resize', this.handleResize);
}
handleResize() {
this.setState({
resolution: {
width: window.innerWidth,
height: window.innerHeight
}
});
}
render() {
return (
<section>
<h3>
{this.state.resolution.width} x {this.state.resolution.height}
</h3>
</section>
)
}
}
上面的代碼將顯示瀏覽器窗口的當(dāng)前分辨率。當(dāng)你調(diào)整窗口大小,您應(yīng)該會(huì)看到自動(dòng)更新窗口的寬和高的值,同時(shí)我們又添加了組件銷毀時(shí),在 componentWillUnmount() 函數(shù)中定義清除監(jiān)聽窗口大小的邏輯。
如果我們使用 Hook 的方式改寫上述代碼,看起來更加簡(jiǎn)潔,示例代碼如下:
import React, { useState, useEffect } from "react";
export default function HookDemo(props) {
...
const [resolution, setResolution] = useState({
width: window.innerWidth,
height: window.innerHeight
});
useEffect(() => {
const handleResize = () => {
setResolution({
width: window.innerWidth,
height: window.innerHeight
});
};
window.addEventListener("resize", handleResize);
// return clean-up function
return () => {
document.title = 'React Hooks Demo';
window.removeEventListener("resize", handleResize);
};
});
...
return (
<section>
...
<h3>
{resolution.width} x {resolution.height}
</h3>
</section>
);
}
運(yùn)行后的效果如下所示:

三、關(guān)于 [ ] 依賴數(shù)組參數(shù)的說明
在開篇的時(shí)候,我們使用 useEffect Hook 實(shí)現(xiàn)了 componentDidMount ,componentDidUpdate 兩個(gè)生命鉤子函數(shù)的一致的效果,這就意味著 DOM 加載完成后,狀態(tài)發(fā)生變化造成的 re-render 都會(huì)執(zhí)行 useEffect Hook 中的邏輯,在一些場(chǎng)景下,我們沒必要在狀態(tài)發(fā)生變化時(shí),調(diào)用此函數(shù)的邏輯,比如我們?cè)谶@里定義數(shù)據(jù)接口更改數(shù)據(jù)狀態(tài),數(shù)據(jù)狀態(tài)發(fā)生變化,會(huì)重新調(diào)用 useEffect Hook 中的請(qǐng)求邏輯,這樣豈不是進(jìn)入了無限循環(huán),數(shù)據(jù)量大的話,說不定就把接口請(qǐng)求死了。
但是還好, useEffect Hook 提供了依賴使用參數(shù),第一個(gè)參數(shù)是定義方法,第二個(gè)參數(shù)是依賴數(shù)組,用于自定義依賴的參數(shù),是否觸發(fā)再次執(zhí)行,接下來我們來看幾個(gè)示例效果:
3.1、after every render
useEffect(() => {
// run after every rendering
console.log('render')
})

如上圖所示,我們每次更改狀態(tài)值導(dǎo)致組件重新渲染時(shí),我們?cè)?useEffect 中定義的輸出將會(huì)反復(fù)的被執(zhí)行。
3.2、Once(執(zhí)行一次)
接下來我們可以在第二個(gè)參數(shù)上定義一個(gè)空數(shù)組,解決上述問題,告訴 Hook 組件只執(zhí)行一次(及時(shí)狀態(tài)發(fā)生改變導(dǎo)致的 re-render ),示例代碼如下:
useEffect(() => {
// Just run the first time
console.log('render')
}, [])

如上圖運(yùn)行效果所示,你會(huì)發(fā)現(xiàn) Hook 函數(shù)中定義的輸出,無論我們?cè)趺锤臓顟B(tài)值,其只輸出一次。
3.3、依賴 state/props 的改變?cè)賵?zhí)行
如果你想依賴特定的狀態(tài)值、屬性,如果其發(fā)生變化時(shí)導(dǎo)致的 re-render ,再次執(zhí)行 Hook 函數(shù)中定義的邏輯,你可以將其寫在數(shù)組內(nèi),示例代碼如下:
useEffect(() => {
// When title or name changed will render
console.log('render')
}, [title, name])
四、用一張圖總結(jié)下
說了這么多,我們做一下總結(jié),說白了就是整合了 componentDidMount,componentDidUpdate,與 componentWillUnmount 這三個(gè)生命鉤子函數(shù),變成了一個(gè)API,其用法可以用如下一張圖進(jìn)行精簡(jiǎn)概括

五、繼續(xù)完善購(gòu)物清單
在上一篇系列文章里《 React Hooks 學(xué)習(xí)筆記 | State Hook(一)》,我們通過做一個(gè)簡(jiǎn)單的購(gòu)物清單實(shí)踐了 State Hook,本篇文章我們通過繼續(xù)完善這個(gè)實(shí)例,加深我們對(duì) useEffect Hook 的理解,學(xué)習(xí)之前大家可以先提前下載上一篇文章的源碼。
本節(jié)案例,為了更加接近實(shí)際應(yīng)用場(chǎng)景,這里我使用了 Firebase 快速構(gòu)建后端的數(shù)據(jù)庫(kù)和其自身的接口服務(wù)。(谷歌的產(chǎn)品,目前需要科學(xué)上網(wǎng)才能使用,F(xiàn)irebase 是 Google Cloud Platform 為應(yīng)用開發(fā)者們推出的應(yīng)用后臺(tái)服務(wù)。借助Firebase,應(yīng)用開發(fā)者們可以快速搭建應(yīng)用后臺(tái),集中注意力在開發(fā) client 上,并且可以享受到 Google Cloud 的穩(wěn)定性和 scalability )。

5.1、創(chuàng)建Firebase
1、在 https://firebase.google.com/(科學(xué)上網(wǎng)才能訪問),使用谷歌賬戶登錄 ,進(jìn)入控制臺(tái)創(chuàng)建項(xiàng)目。


5.2、添加狀態(tài)加載、錯(cuò)誤提示UI組件
接下來我們添加進(jìn)度加載組件和錯(cuò)誤提示對(duì)話框組件,分別用于狀態(tài)加載中狀態(tài)提示和系統(tǒng)錯(cuò)誤狀態(tài)提示,代碼比較簡(jiǎn)單,這里就是貼下相關(guān)代碼。
LoadingIndicator 數(shù)據(jù)加載狀態(tài)提示組件
import React from 'react';
import './LoadingIndicator.css';
const LoadingIndicator = () => (
<div className="lds-ring">
<div />
<div />
<div />
<div />
</div>
);
export default LoadingIndicator;
// components/UI/LoadingIndicator.js
.lds-ring {
display: inline-block;
position: relative;
width: 54px;
height: 54px;
}
.lds-ring div {
box-sizing: border-box;
display: block;
position: absolute;
width: 44px;
height: 44px;
margin: 6px;
border: 6px solid #ff2058;
border-radius: 50%;
animation: lds-ring 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite;
border-color: #ff2058 transparent transparent transparent;
}
.lds-ring div:nth-child(1) {
animation-delay: -0.45s;
}
.lds-ring div:nth-child(2) {
animation-delay: -0.3s;
}
.lds-ring div:nth-child(3) {
animation-delay: -0.15s;
}
@keyframes lds-ring {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
/*
components/UI/LoadingIndicator.css
*/
ErrorModal 系統(tǒng)錯(cuò)誤組件
import React from "react";
import './ErrorModal.css'
const ErrorModal = React.memo(props => {
return (
<React.Fragment>
<div className="backdrop" onClick={props.onClose} />
<div className="error-modal">
<h2>An Error Occurred!</h2>
<p>{props.children}</p>
<div className="error-modal__actions">
<button type="button" onClick={props.onClose}>
Okay
</button>
</div>
</div>
</React.Fragment>
);
});
export default ErrorModal;
// components/UI/ErrorModal.js
.error-modal {
position: fixed;
top: 30vh;
left: calc(50% - 15rem);
width: 30rem;
background: white;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.26);
z-index: 100;
border-radius: 7px;
}
.error-modal h2 {
margin: 0;
padding: 1rem;
background: #ff2058;
color: white;
border-radius: 7px 7px 0 0;
}
.error-modal p {
padding: 1rem;
}
.error-modal__actions {
display: flex;
justify-content: flex-end;
padding: 0 0.5rem;
}
.backdrop {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100vh;
background: rgba(0, 0, 0, 0.75);
z-index: 50;
}
/*
components/UI/ErrorModal.css
*/
5.3、增加接口顯示購(gòu)物清單列表
接下來,我們?cè)谫?gòu)物清單頁 Ingredients 組件里,我們使用今天所學(xué)的知識(shí),在 useEffect() 里添加歷史購(gòu)物清單的列表接口,用于顯示過往的清單信息,這里我們使用 firebase 的提供的API, 請(qǐng)求 https://react-hook-update-350d4-default-rtdb.firebaseio.com/ingredients.json 這個(gè)地址,就會(huì)默認(rèn)給你創(chuàng)建 ingredients 的集合,并返回一個(gè) JSON 形式的數(shù)據(jù)集合,示例代碼如下:
useEffect(() => {
fetch('https://react-hook-update-350d4-default-rtdb.firebaseio.com/ingredients.json')
.then(response => response.json())
.then(responseData => {
const loadedIngredients = [];
for (const key in responseData) {
loadedIngredients.push({
id: key,
title: responseData[key].title,
amount: responseData[key].amount
});
}
setUserIngredients(loadedIngredients);
})
}, []);
// components/Ingredients/Ingredients.js
上述代碼我們可以看出,我們使用 fetch 函數(shù)請(qǐng)求接口,請(qǐng)求完成后我們更新 UserIngredients 數(shù)據(jù)狀態(tài),最后別忘記了,同時(shí)在 useEffect 函數(shù)中,依賴參數(shù)為空數(shù)組[ ],表示只加載一次,數(shù)據(jù)狀態(tài)更新時(shí)導(dǎo)致的 re-render,就不會(huì)發(fā)生無限循環(huán)的請(qǐng)求接口了,這個(gè)很重要、很重要、很重要!
5.4 、更新刪除清單的方法
這里我們要改寫刪除清單的方法,將刪除的數(shù)據(jù)更新到云端數(shù)據(jù)庫(kù) Firebase ,為了顯示更新狀態(tài)和系統(tǒng)的錯(cuò)誤信息,這里我們引入 ErrorModal ,添加數(shù)據(jù)加載狀態(tài)和錯(cuò)誤狀態(tài),示例如下:
import ErrorModal from '../UI/ErrorModal';
const Ingredients = () => {
...
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState();
...
}
// components/Ingredients/Ingredients.js
接下來我們來改寫刪除方法 removeIngredientHandler
const removeIngredientHandler = ingredientId => {
setIsLoading(true);
fetch(`https://react-hook-update-350d4-default-rtdb.firebaseio.com/ingredients/${ingredientId}.json`,
{
method: 'DELETE'
}
).then(response => {
setIsLoading(false);
setUserIngredients(prevIngredients =>
prevIngredients.filter(ingredient => ingredient.id !== ingredientId)
);
}).catch(error => {
setError('系統(tǒng)開了個(gè)小差,請(qǐng)稍后再試!');
setIsLoading(false);
})
};
// components/Ingredients/Ingredients.js
從上述代碼我們可以看出,首先我們先將加載狀態(tài)默認(rèn)為true,接下來請(qǐng)求刪除接口,這里請(qǐng)注意接口地址 ${ingredientId} 這個(gè)變量的使用(當(dāng)前數(shù)據(jù)的 ID 主鍵),刪除成功后,更新加載狀態(tài)為 false 。如果刪除過程中發(fā)生錯(cuò)誤,我們?cè)赾atch 代碼塊里捕捉錯(cuò)誤并調(diào)用錯(cuò)誤提示對(duì)話框(更新錯(cuò)誤狀態(tài)和加載狀態(tài))。
5.5、更新添加清單的方法
接著我們改寫添加清單的方式,通過接口請(qǐng)求的方式,將添加的數(shù)據(jù)添加至 Firebase 數(shù)據(jù)庫(kù),代碼比較簡(jiǎn)單,就不多解釋了,示例代碼如下:
const addIngredientHandler = ingredient => {
setIsLoading(true);
fetch('https://react-hook-update-350d4-default-rtdb.firebaseio.com/ingredients.json',
{
method: 'post',
body: JSON.stringify(ingredient),
headers: {'Content-Type': 'application/json'}
})
.then(response => {
setIsLoading(false);
return response.json();
})
.then(responseData => {
setUserIngredients(prevIngredients => [
...prevIngredients,
{id: responseData.name, ...ingredient}
]);
});
};
// components/Ingredients/Ingredients.js
5.5、添加搜索組件功能
我們繼續(xù)完善購(gòu)物清單的功能,為購(gòu)物清單添加新功能-搜索功能(通過請(qǐng)求接口),方便我們搜索清單的內(nèi)容,界面效果如下圖所示,在中間添加一個(gè)搜索框。

import React,{useState,useEffect,useRef} from "react";
import Card from "../UI/Card";
import './Search.css';
const Search = React.memo(props=>{
const { onLoadIngredients } = props;
const [enteredFilter,setEnterFilter]=useState('');
const inputRef = useRef();
useEffect(() => {
const timer = setTimeout(() => {
if (enteredFilter === inputRef.current.value) {
const query =
enteredFilter.length === 0
? ''
: `?orderBy="title"&equalTo="${enteredFilter}"`;
fetch(
'https://react-hook-update-350d4-default-rtdb.firebaseio.com/ingredients.json' + query
)
.then(response => response.json())
.then(responseData => {
const loadedIngredients = [];
for (const key in responseData) {
loadedIngredients.push({
id: key,
title: responseData[key].title,
amount: responseData[key].amount
});
}
onLoadIngredients(loadedIngredients);
});
}
}, 500);
return () => {
clearTimeout(timer);
};
}, [enteredFilter, onLoadIngredients, inputRef]);
return(
<section className="search">
<Card>
<div className="search-input">
<label>搜索商品名稱</label>
<input
ref={inputRef}
type="text"
value={enteredFilter}
onChange={event=>setEnterFilter(event.target.value)}
/>
</div>
</Card>
</section>
)
});
export default Search;
// components/Ingredients/Search.js
上述代碼,我們定義為了避免頻繁觸發(fā)接口,定義了一個(gè)定時(shí)器,在用戶輸入500毫秒后在請(qǐng)求接口。你可以看到 useEffect() 里,我們使用了 return 方法,用于清理定時(shí)器,要不會(huì)有很多的定時(shí)器。同時(shí)依賴參數(shù)有三個(gè) [enteredFilter, onLoadIngredients,inputRef],只有用戶的輸入內(nèi)容和事件屬性發(fā)生變化時(shí),才會(huì)再次觸發(fā) useEffect() 中的邏輯。這里我們用到了useRef 方法獲取輸入框的值,關(guān)于其詳細(xì)的介紹,會(huì)在稍后的文章介紹。
接下來貼上 Search.css 的相關(guān)代碼,由于內(nèi)容比較簡(jiǎn)單,這里就不過多解釋了。
.search {
width: 30rem;
margin: 2rem auto;
max-width: 80%;
}
.search-input {
display: flex;
justify-content: space-between;
align-items: center;
flex-direction: column;
}
.search-input input {
font: inherit;
border: 1px solid #ccc;
border-radius: 5px;
padding: 0.15rem 0.25rem;
}
.search-input input:focus {
outline: none;
border-color: #ff2058;
}
@media (min-width: 768px) {
.search-input {
flex-direction: row;
}
}
/*
components/Ingredients/Search.css
*/
最后我們將 Search 組件添加至清單頁面,在這個(gè)頁面里定義了一個(gè) useCallback 的方法,類似 Vue 的 computed 緩存的特性,避免重復(fù)計(jì)算,這個(gè)方法主要用來接收 Search 子組件傳輸數(shù)據(jù),用于更新 UserIngredients 數(shù)據(jù)中的狀態(tài),在稍后的文章里我會(huì)詳細(xì)介紹,這里只是簡(jiǎn)單的貼下代碼,示例代碼如下:
const filteredIngredientsHandler = useCallback(filteredIngredients => {
setUserIngredients(filteredIngredients)
}, []);// components/Ingredients/Ingredients.js
接下來在 return 里添加 Search 組件和 ErrorModal 組件,在 Search 組件的 ingredients 屬性里添加上述定義的 filteredIngredientsHandler 方法,用于接收組件搜索接口請(qǐng)求返回的數(shù)據(jù)內(nèi)容,用于更新 UserIngredients 的數(shù)據(jù)狀態(tài),示例代碼如下:
<div className="App">
{error && <ErrorModal onClose={clearError}>{error}</ErrorModal>}
<IngredientForm onAddIngredient={addIngredientHandler} loading={isLoading}/>
<section>
<Search onLoadIngredients={filteredIngredientsHandler}/>
<IngredientList ingredients={userIngredients} onRemoveItem={removeIngredientHandler}/>
</section>
</div>// components/Ingredients/Ingredients.js
到這里,本節(jié)的實(shí)踐練習(xí)就完了,基本上是一個(gè)基于后端接口的,基礎(chǔ)的增刪改查案例,稍微完善下就可以運(yùn)用到你的實(shí)際案例中。你可以點(diǎn)擊閱讀原文進(jìn)行體驗(yàn)(主要本案例采用了Firebase ,科學(xué)上網(wǎng)才能在線體驗(yàn))。
六、結(jié)束語
好了,本篇關(guān)于 useEffect() 的介紹就結(jié)束了,希望你已經(jīng)理解了 useEffect 的基本用法,感謝你的閱讀,你可以點(diǎn)擊閱讀原文體驗(yàn)本文的案例部分,如果你想獲取源碼請(qǐng)回復(fù)"r2",小編建議親自動(dòng)手做一下,這樣才能加深對(duì) useEffect Hook 的認(rèn)知,下一篇本系列文章將會(huì)繼續(xù)介紹 useRef,敬請(qǐng)期待。
