做者:Dave Ceddiahtml
譯者:前端小智前端
來源:daveceddia.react
阿里雲最近在作活動,低至2折,有興趣能夠看看:promotion.aliyun.com/ntms/yunpar…git
爲了保證的可讀性,本文采用意譯而非直譯。github
想象一下:你有一個很是好用的函數組件,而後有一天,我們須要向它添加一個生命週期方法。json
呃...數組
剛開始我們可能會想怎麼能解決這個問題,而後最後變成,一般的作法是將它轉換成一個類。但有時候我們就是要用函數方式,怎麼破? useEffect
hook 出現就是爲了解決這種狀況。瀏覽器
使用useEffect
,能夠直接在函數組件內處理生命週期事件。 若是你熟悉 React class 的生命週期函數,你能夠把 useEffect
Hook 看作 componentDidMount
,componentDidUpdate
和 componentWillUnmount
這三個函數的組合。來看看例子:安全
import React, { useEffect, useState } from 'react';
import ReactDOM from 'react-dom';
function LifecycleDemo() {
useEffect(() => {
// 默認狀況下,每次渲染後都會調用該函數
console.log('render!');
// 若是要實現 componentWillUnmount,
// 在末尾處返回一個函數
// React 在該函數組件卸載前調用該方法
// 其命名爲 cleanup 是爲了代表此函數的目的,
// 但其實也能夠返回一個箭頭函數或者給起一個別的名字。
return function cleanup () {
console.log('unmounting...');
}
})
return "I'm a lifecycle demo";
}
function App() {
// 創建一個狀態,爲了方便
// 觸發從新渲染的方法。
const [random, setRandom] = useState(Math.random());
// 創建一個狀態來切換 LifecycleDemo 的顯示和隱藏
const [mounted, setMounted] = useState(true);
// 這個函數改變 random,並觸發從新渲染
// 在控制檯會看到 render 被打印
const reRender = () => setRandom(Math.random());
// 該函數將卸載並從新掛載 LifecycleDemo
// 在控制檯能夠看到 unmounting 被打印
const toggle = () => setMounted(!mounted);
return (
<>
<button onClick={reRender}>Re-render</button>
<button onClick={toggle}>Show/Hide LifecycleDemo</button>
{mounted && <LifecycleDemo/>}
</>
);
}
ReactDOM.render(<App/>, document.querySelector('#root'));
複製代碼
在CodeSandbox中嘗試一下。dom
單擊「Show/Hide
」按鈕,看看控制檯,它在消失以前打印「unmounting...
」,並在它再次出現時打印 「render!
」。
如今,點擊Re-render
按鈕。每次點擊,它都會打render!
,還會打印umounting
,這彷佛是奇怪的。
爲啥每次渲染都會打印 'unmounting
'。
我們能夠有選擇性地從useEffect
返回的cleanup
函數只在組件卸載時調用。React 會在組件卸載的時候執行清除操做。正如以前學到的,effect
在每次渲染的時候都會執行。這就是爲何 React 會在執行當前 effect 以前對上一個 effect
進行清除。這實際上比componentWillUnmount
生命週期更強大,由於若是須要的話,它容許我們在每次渲染以前和以後執行反作用。
useEffect在每次渲染後運行(默認狀況下),而且能夠選擇在再次運行以前自行清理。
與其將useEffect
看做一個函數來完成3個獨立生命週期的工做,不如將它簡單地看做是在渲染以後執行反作用的一種方式,包括在每次渲染以前和卸載以前我們但願執行的須要清理的東西。
若是但願 effect
較少運行,能夠提供第二個參數 - 值數組。 將它們視爲該effect
的依賴關係。 若是其中一個依賴項自上次更改後,effect
將再次運行。
const [value, setValue] = useState('initial');
useEffect(() => {
// 僅在 value 更改時更新
console.log(value);
}, [value])
複製代碼
上面這個示例中,我們傳入 [value]
做爲第二個參數。這個參數是什麼做用呢?若是value
的值是 5
,並且我們的組件重渲染的時候 value
仍是等於 5,React 將對前一次渲染的 [5]
和後一次渲染的 [5]
進行比較。由於數組中的全部元素都是相等的(5 === 5)
,React 會跳過這個 effect
,這就實現了性能的優化。
若是想執行只運行一次的 effect
(僅在組件掛載和卸載時執行),能夠傳遞一個空數組([]
)做爲第二個參數。這就告訴 React 你的 effect
不依賴於 props
或 state
中的任何值,因此它永遠都不須要重複執行。這並不屬於特殊狀況 —— 它依然遵循依賴數組的工做方式。
useEffect(() => {
console.log('mounted');
return () => console.log('unmounting...');
}, [])
複製代碼
這樣只會在組件初次渲染的時候打印 mounted
,在組件卸載後打印: unmounting
。
不過,這隱藏了一個問題:傳遞空數組容易出現bug
。若是我們添加了依賴項,那麼很容易忘記向其中添加項,若是錯過了一個依賴項,那麼該值將在下一次運行useEffect
時失效,而且可能會致使一些奇怪的問題。
在這個例子中,一塊兒來看下如何使用useEffect
和useRef
hook 將input
控件聚焦在第一次渲染上。
import React, { useEffect, useState, useRef } from "react";
import ReactDOM from "react-dom";
function App() {
// 存儲對 input 的DOM節點的引用
const inputRef = useRef();
// 將輸入值存儲在狀態中
const [value, setValue] = useState("");
useEffect(
() => {
// 這在第一次渲染以後運行
console.log("render");
// inputRef.current.focus();
},
// effect 依賴 inputRef
[inputRef]
);
return (
<input
ref={inputRef}
value={value}
onChange={e => setValue(e.target.value)}
/>
);
}
ReactDOM.render(<App />, document.querySelector("#root"));
複製代碼
在頂部,咱們使用useRef
建立一個空的ref
。 將它傳遞給input
的ref prop
,在渲染DOM 時設置它。 並且,重要的是,useRef
返回的值在渲染之間是穩定的 - 它不會改變。
所以,即便我們將[inputRef]
做爲useEffect
的第二個參數傳遞,它實際上只在初始掛載時運行一次。這基本上是 componentDidMount
效果了。
再來看看另外一個常見的用例:獲取數據並顯示它。在類組件中,無們經過能夠將此代碼放在componentDidMount
方法中。在 hook 中可使用 useEffect hook 來實現,固然還須要用useState
來存儲數據。
下面是一個組件,它從Reddit獲取帖子並顯示它們
import React, { useEffect, useState } from "react";
import ReactDOM from "react-dom";
function Reddit() {
const [posts, setPosts] = useState([]);
useEffect(async () => {
const res = await fetch(
"https://www.reddit.com/r/reactjs.json"
);
const json = await res.json();
setPosts(json.data.children.map(c => c.data));
}); // 這裏沒有傳入第二個參數,你猜猜會發生什麼?
// Render as usual
return (
<ul>
{posts.map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
ReactDOM.render(
<Reddit />,
document.querySelector("#root")
);
複製代碼
注意到我們沒有將第二個參數傳遞給useEffect
,這是很差的,不要這樣作。
不傳遞第二個參數會致使每次渲染都會運行useEffect
。而後,當它運行時,它獲取數據並更新狀態。而後,一旦狀態更新,組件將從新呈現,這將再次觸發useEffect
,這就是問題所在。
爲了解決這個問題,咱們須要傳遞一個數組做爲第二個參數,數組內容又是啥呢。
useEffect
所依賴的惟一變量是setPosts
。所以,我們應該在這裏傳遞數組[setPosts]
。由於setPosts
是useState
返回的setter
,因此不會在每次渲染時從新建立它,所以effect
只會運行一次。
虛接着擴展一下示例,以涵蓋另外一個常見問題:如何在某些內容發生更改時從新獲取數據,例如用戶ID,名稱等。
首先,我們更改Reddit
組件以接受subreddit
做爲一個prop,並基於該subreddit
獲取數據,只有當 prop
更改時才從新運行effect
.
// 從props中解構`subreddit`:
function Reddit({ subreddit }) {
const [posts, setPosts] = useState([]);
useEffect(async () => {
const res = await fetch(
`https://www.reddit.com/r/${subreddit}.json`
);
const json = await res.json();
setPosts(json.data.children.map(c => c.data));
// 當`subreddit`改變時從新運行useEffect:
}, [subreddit, setPosts]);
return (
<ul>
{posts.map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
ReactDOM.render(
<Reddit subreddit='reactjs' />,
document.querySelector("#root")
);
複製代碼
這仍然是硬編碼的,可是如今我們能夠經過包裝Reddit
組件來定製它,該組件容許我們更改subreddit
。
function App() {
const [inputValue, setValue] = useState("reactjs");
const [subreddit, setSubreddit] = useState(inputValue);
// Update the subreddit when the user presses enter
const handleSubmit = e => {
e.preventDefault();
setSubreddit(inputValue);
};
return (
<>
<form onSubmit={handleSubmit}>
<input
value={inputValue}
onChange={e => setValue(e.target.value)}
/>
</form>
<Reddit subreddit={subreddit} />
</>
);
}
ReactDOM.render(<App />, document.querySelector("#root"));
複製代碼
在 CodeSandbox 試試這個示例。
這個應用程序在這裏保留了兩個狀態:當前的輸入值和當前的subreddit
。提交表單將提交subreddit
,這會致使Reddit
從新獲取數據。
順便說一下:輸入的時候要當心,由於沒有錯誤處理,因此當你輸入的subreddit
不存在,應用程序將會爆炸,實現錯誤處理就做爲大家的練習。
各位能夠只使用一個狀態來存儲輸入,而後將相同的值發送到Reddit
,可是Reddit
組件會在每次按鍵時獲取數據。
頂部的useState
看起來有點奇怪,尤爲是第二行:
const [inputValue, setValue] = useState("reactjs");
const [subreddit, setSubreddit] = useState(inputValue);
複製代碼
咱們把reactjs
的初值傳遞給第一個狀態,這是有意義的,這個值永遠不會改變。
那麼第二行呢,若是初始狀態改變了呢,如當你輸入box
時候。
記住,useState
是有狀態的。它只使用初始狀態一次,即第一次渲染,以後它就被忽略了。因此傳遞一個瞬態值是安全的,好比一個可能改變或其餘變量的 prop
。
使用useEffect
就像瑞士軍刀。它能夠用於不少事情,從設置訂閱到建立和清理計時器,再到更改ref
的值。
與 componentDidMount
、componentDidUpdate
不一樣的是,在瀏覽器完成佈局與繪製以後,傳給 useEffect
的函數會延遲調用。這使得它適用於許多常見的反作用場景,好比如設置訂閱和事件處理等狀況,所以不該在函數中執行阻塞瀏覽器更新屏幕的操做。
然而,並不是全部 effect
均可以被延遲執行。例如,在瀏覽器執行下一次繪製前,用戶可見的 DOM 變動就必須同步執行,這樣用戶纔不會感受到視覺上的不一致。(概念上相似於被動監聽事件和主動監聽事件的區別。)React
爲此提供了一個額外的 useLayoutEffect
Hook 來處理這類 effect
。它和 useEffect
的結構相同,區別只是調用時機不一樣。
雖然 useEffect
會在瀏覽器繪製後延遲執行,但會保證在任何新的渲染前執行。React 將在組件更新前刷新上一輪渲染的 effect
。
原文:daveceddia.com/useeffect-h…
代碼部署後可能存在的BUG無法實時知道,過後爲了解決這些BUG,花了大量的時間進行log 調試,這邊順便給你們推薦一個好用的BUG監控工具 Fundebug。
乾貨系列文章彙總以下,以爲不錯點個Star,歡迎 加羣 互相學習。
我是小智,公衆號「大遷世界」做者,對前端技術保持學習愛好者。我會常常分享本身所學所看的乾貨,在進階的路上,共勉!
關注公衆號,後臺回覆福利,便可看到福利,你懂的。