React 16:Lifecycle Methods 新手包
React 16.3.0 發佈了關於 lifecycles methods 的更新,新增了 method:getDerivedStateFromProps
和 getDerivedStateFromError
,因為原有的 lifecycle method: componentWillMount
, componentWillReceiveProps
和 componentWillUpdate
經常讓人誤解誤用,造成許多實作上的程式漏洞。所以在這次 React 16 更新之後,為了幫助理解這些 lifecycle methods 使用的時機,下面將會分成四個部分來介紹:mounting, updating, unmounting 和 errors。
說明文章使用範例:一個類似 Pinterest 的瀑布流式簡單應用。預設每兩秒會在最下面載入一些需要加入排列的 blocks。
這邊會使用 bricks.js
這個 library 來處理頁面的佈局,bricks.js
是一個不錯工具,但其實他和 react 並沒有很麻吉,他和 vanilla JavaScript
或者 jQuery
還更合拍。那為什麼要故意用 bricks.js
呢?因為在 lifecycle methods 裡面常常需要整合各種 non-React 套件,在實作上許多好的套件並不只是為 React 而生,而 lifecycle methods 就在這其中扮演一個橋樑的角色。
One more thing:總之呢~lifecycle methods 他們 hen 容易你的程式越來越複雜,不到最後關頭絕不輕言呼叫。
Mounting
constructor
當 React 元件是一個 class component
,那麼當他誕生時呼叫的第一個方法就是 constructor
,也就是說一個 functional component
是不會有這個方法的唷,可能長得有點像這樣:
class MyComponent extends Component {
constructor(props) {
super(props);
this.state = {
counter: 0,
};
}
}
constructor
的使用伴隨著 component 相關的 props,所以你必須一併呼叫 super
並且傳入 props
。 在這邊可以設定整個元件 state
的初始值,也可以使用 props 的傳入來設定,例如:
class MyComponent extends Component {
constructor(props) {
super(props);
this.state = {
counter: props.initialCounterValue,
};
}
}
constructor
不是一個必須翻牌的方法,不找他也沒有關係,如果只是為了要設定 state 的初始值,只要 Babel
設定有支援 class fields
,下方這樣的寫法也是可以預設 state 的初始值:
class MyComponent extends Component {
state = {
counter: 0,
};
}
這種寫法少了幾行相對好像比較受喜愛,要從 props 取得設定值也沒有問題:
class MyComponent extends Component {
state = {
counter: this.props.initialCounterValue,
};
}
那麼 constructor
不就沒有存在的必要了嗎?其實他還有另外一個作用就是設定 ref
,例如範例中:
class Grid extends Component {
constructor(props) {
super(props);
this.state = {
blocks: [],
};
this.grid = React.createRef();
}
React 元件需要 constructor
來呼叫 createRef
,才能給例子中的 grid 建立 reference,之後再傳給 bricks.js
使用。
constructor
還可以用來 binding function,來這裡 React Binding Patterns: 5 Approaches for Handling this
看更多。
最佳使用時機:設定 state、建立 refs 和 function binding。
getDerivedStateFromProps (new)
在元件 mouting 階段 render 之前,最後被呼叫的方法就是 getDerivedStateFromProps
,可以在這個方法裡面用初始的 props 設定 state,如範例的 Grid
元件:
static getDerivedStateFromProps(props, state) {
return { blocks: createBlocks(props.numberOfBlocks) };
}
這裡使用 numberOfBlocks
這個 prop 來生出大小隨機的區塊,然後回傳一個 state object。回傳後用 console.log(this.state)
印出來會長得像這樣:
console.log(this.state);
// -> {blocks: Array(20)}
從 props 設定 state 這段程式碼完全可以放在 constructor
,拿來放在 getDerivedStateFromProps
好處是更加的直覺,他就純粹是用來設定 state,但在 constructor
裡面是還有可能做一些其他的事。
最佳使用時機 ( mount 階段 ):回傳設定值來自初始 props 的 state object。
render
回傳 JSX 長出 React 元件的地方,在 React 這個框架會花蠻多時要在這裡作業。
最佳使用時機:回傳生出元件的 JSX。
componentDidMount
當元件第一次長出來的時候這個方法就會被呼叫,如果你需要載入資料用 AJAX、fetch 之類的動作就是在此時進行,千萬不要在 constructor
、render
或其他地方去發出這些的請求,有一個叫 Tyler McGinnis 是這麼說的:
因為無法確保 AJAX / fetch 的結果會在什麼時候完成,如果在元件長好之前就載完了,那就表示你對一個還沒長出來的元件進行
setState
,這樣是沒有作用的,而且 React 也不希望這麼做,會生出一堆討厭的警告。
在 componentDidMount
裡面還能做許多有趣的事,
- 在剛生成的
<canvas>
上畫點東西 - 把一堆資料塞入一個瀑布流的佈局畫面
- 設定
event listeners
基本上就是在這邊處理那些有 DOM 之後才能做的事,或進行取得元件所需要的資料。 例如:
componentDidMount() {
this.bricks = initializeGrid(this.grid.current);
layoutInitialGrid(this.bricks)
this.interval = setInterval(() => {
this.addBlocks();
}, 2000);
}
例子中使用 brick.js
裡面的方法 initalizeGrid
來產生 grid。然後每二秒新增更多的 blocks,模擬實際上在用 loadRecommendations
時更多的載入資料。
最佳使用時機:送出 AJAX、fetch 等等來載入元件所需的資料。
Updating
getDerivedStateFromProps
唉〜這剛剛不是出現過!是的沒錯,但這個時候的用處比剛剛多了些。 如果需要用 props
更新 state
的話,可以在這裡處理然後回傳新的 state object。不過呢最好不要在這裡用 props
來更新 state
,最好認真地確認需不需要用這個方式把 state 存下來?
在下面的一些情況下可以在這個方法裡面處理,但通常是不建議這麼做:
- 當 source 更新的時候重新設置 video 或 audio 元件。
- 在 server 更新的時候,更新 UI 元件
- 在內容改變的時候,關閉 accordinon 元件。
在我們的範例中 Grid
這個元件有一個 numberOfBlocks
的 props ,但因為這個也屬於 Update 的一種,所以這裡的意思就是:如果元件 state 的blocks 大於零的話就回傳一個空的 object,不然就用 numberOfBlocks
這個 props 進行 state 的更新:
static getDerivedStateFromProps(props, state) {
if (state.blocks.length > 0) {
return {};
}
return { blocks: createBlocks(props.numberOfBlocks) };
}
也就是說如果原本 state 的 block 數超過新的 props,就不更新 state,直接回傳一個空的 object。
註:關於 static
方法,例如 getDerivedStateFromProps
是無法透過 this
來控制元件的,所以無法處理 grid ref。
最佳使用時機:透過 props 更新 state,
shouldComponentUpdate (new)
在 React 的信念裡面當元件收到新的 props 或是 新的 state 就要更新一下。只是元件本人有點小小的焦慮,覺得更新之前要先請示。因此誕生了 shouldComponentUpdate
這個方法,傳入 nextProps
和 nextState
當參數。
shouldComponentUpdate
只會回傳 boolean 值,預設是回傳 true,就好像這個元件問主人說:我應該 re-render 嗎?主人:喔,小可愛,是的,你應該這麼做。透過這個方法,可以確認元件不會沒事就隨便 render,能夠提昇一些效能。
關於 shouldComponentUpdate
的參考用法之一 How to Benchmark React Components: The Quick and Dirty Guide
在上面的文章裡提到的例子:一個 table 有許多的欄位,問題出於如果 table re-render,所有的欄位就算沒有需要更新也必須跟著一起 re-render,這樣的確是會拖慢效能。shouldComponentUpdate
就能做到只有在特定 props 改變的時候再進行更新。
貼心小提示:如果元件在這個方法裡面做了一些設定,那麼元件就不會跟一般的元件一樣進行更新,有時候 bug 就出現在我們忘了這些特別的手動設定,請小心服用。
範例裡面在前一個 getDerivedStateFromProps
方法寫了:在某些時候忽略新的 this.props.numberOfBlocks
。但因為 React 預設是只要收到新的 props 就要 re-render,所以元件還是會再重新 render 一次,這樣確實是浪費效能沒必要。使用 shouldComponentUpdate
處理一下,讓元件只有在新的 blocks 多於 state 的 blocks 的時候再進行更新唷。
shouldComponentUpdate(nextProps, nextState) {
// Only update if bricks change
return nextState.blocks.length > this.state.blocks.length;
}
最佳使用時機:需要精準地掌控元件應該在什麼時候 re-render 的各位。
render (和 Mounting
的時候一樣)
getSnapshotBeforeUpdate (new)
getSnapshotBeforeUpdate
是出現在 render 前的最後一個方法,在這裡可以看元件在此時此刻的 state 和 props 最後一眼。呃~呼叫這個方法的話不是會在 render 前增加一點小延遲嗎?總是有些情況是會需要知道現況,然後和即將進行的 render 作個整合,例如:
getSnapshotBeforeUpdate(prevProps, prevState) {
if (prevState.blocks.length < this.state.blocks.length) {
const grid = this.grid.current;
const isAtBottomOfGrid =
window.innerHeight + window.pageYOffset === grid.scrollHeight;
return { isAtBottomOfGrid };
}
return null;
}
在範例中是用在當使用者滾到網頁的最下方,如果有新的 blocks 載入的話要保持畫面停留在底部,然後回傳一個 object 如:isAtBottomOfGrid
,不然的話就來個 null
。不管如何在 getSnapshotBeforeUpdate
這個方法就是要回傳個什麼值,不然就要設為回傳 null
。 Oh my god why! 讓我們繼續看下去。
最佳使用時機:處理將要 render 的元件針會其中的某些屬性,接著會把值傳給 componentDidUpdate
。
componentDidUpdate
來到這裡終於把元件更新完畢,在 componentDidMount
這個方法可以獲得三個 props:previous state
、previous props
和 一些來自剛剛 getSnapshotBeforeUpdate
的東西,例如:
componentDidUpdate(prevProps, prevState, snapshot) {
this.bricks.pack();
if (snapshot.isAtBottomOfGrid) {
window.scrollTo({
top: this.grid.current.scrollHeight,
behavior: 'smooth',
});
}
}
以上的意思是:用 Bricks.js 的方法 pack
重新佈局 grid,然後從剛剛的 getSnapshotBeforeUpdate
這邊得知使用者處於 grid 最下方的狀態,所以在有新的 blocks 誕生的時候一樣能停在 grid 的最下方。
最佳使用時機:在 DOM 更新之後做一些對應。
Unmouting
componentWillUnmount
嗨~ 大家今天到這裡過得好嗎?小元件們走到現在也是個要說再見的時候了,在告別之前可以在 componentWillUnmount
這個方法裡面做最後的掙扎。那麼可以用來幹啥呢?可以把和這個元件有關的一些用不著的 request、event listeners 統統移除,要走就要走得乾乾淨淨的。範例中在 componentDidMount
裡面設了一個 setInterval
在這邊要把他清掉:
componentWillUnmount() {
clearInterval(this.interval);
}
最佳使用時機:在元件最後燒毀前清掉不需要遺留下的廢物。
Errors
getDerivedStateFromError
老天鵝,有子元件東西壞了啦,這時候應該要出現 error 畫面,一個最簡單的方法就是放一個類似 this.state.hasError
的設定,在發生 error 的時候可以用 getDerivedStateFromError
設成 true
:
static getDerivedStateFromError(error) {
return { hasError: true };
}
注意一點:getDerivedStateFromError
的回傳值只能是一包更新的 state object,其他啥也別做,要有什麼要處理的話留在下一個 componentDidCatch
方法再說。
最佳使用時機:更新 state 來呼叫 error 畫面。
componentDidCatch
跟剛剛的 getDerivedStateFromError
有點像,componentDidCatch
也是在子元件有 error 的時候被觸發,區別在於在這個方法裡面可以做更多其他的處理,例如把 error log 下來:
componentDidCatch(error, info) {
sendErrorLog(error, info);
}
上面的給的 error
參數是 error 訊息,例如:(Undefined Variable blah blah blah… ),info
就是 stack trace 那些 (In Component, in div, etc)。
componentDidCatch
只有 error 發生在 lifecycle methods 裡面的時候才有效,如果是從 click handler 來的 error 是不會被 catch 的唷。
通常這個方法會用在 error boundary 元件裡面,例如:
class ErrorBoundary extends Component {
state = { errorMessage: null };
static getDerivedStateFromError(error) {
return { errorMessage: error.message };
}
componentDidCatch(error, info) {
console.log(error, info);
}
render() {
if (this.state.errorMessage) {
return <h1>Oops! {this.state.errorMessage}</h1>;
}
return this.props.children;
}
}
最佳使用時機:Catch 和 log errors。
結論:
React 16 lifecycle methods 將要廢除 componentWilllMount
、componentWillReceiveProps
和 componentWillUpdate
這三個方法,然後新增 getDerivedStateFromProps
和 getDerivedStateFromError
這兩個方法。如果有用到被廢除的那三個方法,在 React 16 是還可以用使用沒問題,到了 React 17 也能用,只是會發出 UNSAFE_
的警告,不能預期的是也有可能造成一些 bug,希望大家能夠更無痛地使用這些 lifecycle methods。
REF: