React 元件生命週期 (Component Lifecycle)
一般常會用 JavaScript Class 語法來宣告 React Component:
class Greeting extends React.Component {
render() {
return <h1>Hello, {this.props.name}</h1>;
}
}
在這一篇文章中,我們會來更深入介紹 React Component Class 有哪些屬性 (Properties) 和方法 (Methods)。
React Component Class 除了 render()
,還有一系列的 Lifecycle Methods (方法),讓你可以在不同的元件事件 (也就是所謂的元件生命週期) 發生時做點你想做的事。
Lifecycle Methods 如果是 will
開頭的,表示在某個事件發生「之前」,會執行這個方法;如果是 did
開頭的,表示在某個事件發生「之後」,會執行這個方法。
Lifecycle Methods 可以分為三大類:
- Mounting - 裝載,當元件被加入到 DOM 中時會觸發
- Updating - 更新,當元件的 props 或 state 更新,重新渲染 (re-rendered) DOM 時會觸發
- Unmounting - 卸載,當元件要從 DOM 中被移除時會觸發
- Error Handling - 例外處理,當元件發生 JavaScript errors 時會觸發
Mounting Lifecycle Methods
React 提供有這些 Mounting 階段的方法:
render()
render() 是 React Component 唯一一定要實作的方法。
render() 在每次 props 或是 state 被改變時,都會被執行一次。
在 render() 中你會根據當前 this.props
及 this.state
的資料狀態,來決定該元件當前的 UI 結構和顯示內容。
render() 可以返回下面這幾種資料型態其一:
React elements
通常 render() 返回的就是用 JSX 建立的 React 元素。
String / numbers
你也可以返回字串或是數字,這會被當作是 HTML DOM 的 text nodes 來顯示。
null
返回
null
告訴 React 不顯示任何東西。Booleans
返回布林值
false
也是告訴 React 不顯示任何東西。Booleans 通常是為了這種寫法 pattern:render() { // ..... return test && <Child />; }
其中 test 是一個布林值,當作一個開關值 (flag) 決定是否要顯示
<Child />
。
實作慣例上,我們會保持 render() 是一個 pure function,不會在裡面更改 State 值,也不會在裡面寫任何會造成 side-effects 的 code (例如和瀏覽器互動)。
constructor(props)
元件 Class 的 constructor (建構子) 會在元件還沒被掛載到 DOM 之前先被執行來做初始化。
React 元件是 React.Component
的 subclass,你必須在 constructor 的最前面 call super(props)
否則會發生問題,像是會沒有 this.props
。
constructor 是用來初始化 (initialize) state 的地方,直接將初始值指定 (assign) 給 this.state
這屬性,你不能在 constructor 中使用 setState()
喔。
用法例子:
constructor(props) {
super(props);
this.state = {
color: props.initialColor
};
}
static getDerivedStateFromProps(props, state)
getDerivedStateFromProps() 是一個 static method,會在「每一次」跑 render() 之前被呼叫執行。
getDerivedStateFromProps(props, state) 執行時會傳入當前的 props 和 state,執行後需要返回一個物件 (object) 來表示欲更新的 state 或返回 null
表示不更新。
例如:
class ExampleComponent extends React.Component {
state = {
isScrollingDown: false,
lastRow: null,
};
static getDerivedStateFromProps(props, state) {
if (props.currentRow !== state.lastRow) {
// 更新 state
return {
isScrollingDown: props.currentRow > state.lastRow,
lastRow: props.currentRow,
};
}
// 沒任何更新
return null;
}
}
componentWillMount()
componentWillMount() 只會被執行一次,會在元件被掛載到實際的 HTML DOM 之前被呼叫執行。
componentWillMount() 會在第一次的 render() 執行之前就先被執行,所以你不能在 componentWillMount() 中做跟 DOM 有關的操作。
通常 componentWillMount() 比較少會用到,實用性不大,大部分可以在 componentWillMount() 做的事都可以也更適合在 constructor() 中完成。
componentDidMount()
componentDidMount() 會在元件被掛載到 DOM 後被執行 - 也就是說元件已經實際存在在畫面中,任何需要 DOM 或會 Asynchronous 更新 state 狀態的操作都適合放在 componentDidMount() 做。
適合在 componentDidMount() 做的,像是綁定元件的 DOM 事件,或 AJAX 拉遠端資料來進一步初始化元件。
Updating Lifecycle Methods
React 提供有這些 Updating 階段的方法:
- componentWillReceiveProps()
- static getDerivedStateFromProps()
- shouldComponentUpdate()
- componentWillUpdate()
- render()
- getSnapshotBeforeUpdate()
- componentDidUpdate()
componentWillReceiveProps(nextProps)
componentWillReceiveProps() 會在每次元件接收到 props 更新時被執行,通常我們會在 componentWillReceiveProps() 中聽 props 的改變來更新元件對應的 State 值。
componentWillReceiveProps() 會傳進 nextProps 這參數,表示即將更新的 props 值,讓你可以運用 this.props 和 nextProps 來比對 props 前後值的變化,來更新相對應的 state。
有時候不一定是 props 有更新才會 call componentWillReceiveProps(),當父元件刷新子元件時也會執行 componentWillReceiveProps()。因為每一次元件更新時,componentWillReceiveProps() 有可能會被執行好幾次,所以要避免有任何 side effect 的 code 寫在裡面。
使用例子:
componentWillReceiveProps(nextProps) {
if (this.props.initialX !== nextProps.initialX) {
this.setState = ({
// .....
});
}
}
shouldComponentUpdate(nextProps, nextState)
shouldComponentUpdate() 是用來你想最佳化效能 (performance) 時使用,每當 Props 或 State 有更新時,React 會在 call render() 重繪畫面之前,先 call shouldComponentUpdate() 決定是否真的需要 render()。
React 雖然會很聰明地自動偵測到 Props 和 State 狀態有改變來自動更新元件 (re-render),但有些狀況下你可以幫助 React 更精確判斷元件是否真的需要更新,這就是 shouldComponentUpdate() 的功用了。
你可以利用 shouldComponentUpdate() 傳進的參數 nextProps, nextState (新的 props 和 state 值) 和當前的 this.props, this.state 來判斷元件資料狀態變化,shouldComponentUpdate() 執行後得返回一個布林值 (Boolean) 來告訴 React 是否需要更新元件,如果返回 false
,componentWillUpdate(), render(), componentDidUpdate() 這些 Lifecycole Methods 就不會被執行。
true
。componentWillUpdate(nextProps, nextState)
componentWillUpdate() 會在元件準備更新、執行 render() 之前被執行。
每一次元件更新時,componentWillUpdate() 有可能會被執行好幾次,所以要避免有任何 side effect 的 code 寫在裡面。而在 componentWillUpdate() 中,也禁止有任何會更新到元件的動作,像是不能 call this.setState(),如果你會更新 State 請用 getDerivedStateFromProps。
在大部分的情形下,其實 componentWillUpdate() 實用性並不高。
getSnapshotBeforeUpdate(prevProps, prevState)
getSnapshotBeforeUpdate 會在畫面實際渲染 (rendered) 前一刻被呼叫執行,簡單的說觸發的時機點是在 React 進行修改前,通常是更新 DOM 前。
getSnapshotBeforeUpdate 被執行後 return 的值會被傳進 componentDidUpdate 的第三個參數。
通常 getSnapshotBeforeUpdate 可以用在紀錄畫面準備修改前的當下狀態,像是 scroll position:
class ScrollingList extends React.Component {
constructor(props) {
super(props);
this.listRef = React.createRef();
}
getSnapshotBeforeUpdate(prevProps, prevState) {
// 如果 list 內容有變動,有新東西加到 list 前面時
if (prevProps.list.length < this.props.list.length) {
const list = this.listRef.current;
// 紀錄更新前一刻,捲軸的位置 scroll position
return list.scrollHeight - list.scrollTop;
}
return null;
}
componentDidUpdate(prevProps, prevState, snapshot) {
// 如果有 snapshot
if (snapshot !== null) {
const list = this.listRef.current;
// 調整 scroll 的位置,避免畫面才不會被新的東西推開
list.scrollTop = list.scrollHeight - snapshot;
}
}
render() {
return (
<div ref={this.listRef}>{/* ...contents... */}</div>
);
}
}
componentDidUpdate(prevProps, prevState, snapshot)
componentDidUpdate() 會在元件更新完成、執行完 render() 重繪後被執行。而每一次元件更新時,React 確保 componentDidUpdate() 只會被執行一次。
在這邊可以執行像在 componentDidMount() 中的操作,比對 prevProps/prevState 及 this.props/this.state 狀態差異,做像是存取 DOM、重畫 Canvas、重整頁面 layout、AJAX 網路呼叫等動作。
如果你的 component 有實作 getSnapshotBeforeUpdate 方法,getSnapshotBeforeUpdate 的返回值 (return value) 會被傳進去 componentDidUpdate 當作第三個參數 snapshot,沒實作的話這參數值就會是 undefined
。
Unmounting Lifecycle Methods
React 提供有這些 Unmounting 階段的方法:
componentWillUnmount()
當元件將要從 DOM 中被移除之前,React 會執行 componentWillUnmount()。
你可以在 componentWillUnmount() 中做資源清理的動作,清除跟這元件有關的任何遺留物,像是清除你在 componentDidMount() 中建立的資源,例如清除 timer、取消 AJAX request、移除 event listener 等。
Error Handling Lifecycle Methods
React 提供有這些例外處理的方法:
componentDidCatch(error, info)
React 在 V16 版後新增了錯誤邊界 (Error Boundary) 的概念,新加入了 componentDidCatch() 可以用來捕捉 (catch) 從子元件 (child component tree) 中拋出的錯誤 (JavaScript errors),避免因為一個小元件發生意外錯誤就造成整個頁面掛掉,讓錯誤不會影響到邊界外層的父元件,你可以在 componentDidCatch() 決定這個例外錯誤該怎麼處理,像是 fallback UI。
想要成為一個 Error Boundary 的方式很簡單,只要在 Component 中加入 componentDidCatch() 方法就成為 Error Boundary 元件,每當子元件發生錯誤時,會被 Error Boundary 捕捉,不會讓錯誤延伸影響到錯誤邊界外層。
Error Boundary 的限制:
- 只能捕捉子元件的錯誤,不包含 Error Boundary 元件本身
- 只能捕捉從 constructor(), render() 和各 Lifecycle Methods 中發生的錯誤
- 非同步 (Asynchronous) 程式中發生的錯誤無法被捕捉
- Event Handler 發生的錯誤無法被捕捉
Error Boundary 實作範例:
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
componentDidCatch(error, info) {
// 顯示 fallback UI
this.setState({ hasError: true });
// 可以將 error 錯誤訊息記錄下來
logErrorToMyService(error, info);
}
render() {
if (this.state.hasError) {
// 顯示 fallback UI
return <h1>Something went wrong.</h1>;
}
return this.props.children;
}
}
然後這樣使用 Error Boundary:
<ErrorBoundary>
<MyWidget />
</ErrorBoundary>
元件生命週期事件順序
整理一下整個 Component 的生命週期發生的事件順序。
當元件第一次 render 時的順序:
- constructor
- componentWillMount, getDerivedStateFromProps
- render
- (React 實際更新 DOM / refs)
- componentDidMount
此後,當元件被更新 (update) 時的順序:
- componentWillReceiveProps, getDerivedStateFromProps
- shouldComponentUpdate - 如果 return false 就不會再往下走
- componentWillUpdate
- render
- getSnapshotBeforeUpdate
- (React 實際更新 DOM / refs)
- componentDidUpdate
其中不能執行 this.setState() 的事件:
- render
- componentWillMount
- getDerivedStateFromProps
- shouldComponentUpdate
- componentWillUpdate
元件的方法和屬性
之前我們介紹過的元件方法 (Method) 有 setState(),而屬性 (Properties) 有 props 和 state。再來介紹幾個元件的方法和屬性:
component.forceUpdate(callback)
通常元件會根據 Props 和 State 的改變來更新元件,但有時候元件會依賴其他的資料,這時你就需要用 this.forceUpdate()
來告訴 React 你要更新元件。
執行 forceUpdate() 後 React 會跳過 shouldComponentUpdate() 直接執行 render() (但子元件的 Lifecycle Methods 都會被正常執行)。
而參數 callback
是一個 function,當元件更新完後會被執行。
一般實作上,需要盡量避免使用到 forceUpdate(),在 render() 中只用 this.props 和 this.state。
defaultProps
defaultProps 是一個 class property,用來設定當 props 是 undefined
時的預設值。
使用例子:
class CustomButton extends React.Component {
// ...
}
CustomButton.defaultProps = {
color: 'blue'
};
當沒有設定 props.color 時,props.color 值會是 blue:
render() {
return <CustomButton /> ;
}
只有當 props 是 undefined
才會用 defaultProps 的值,如果值是 null
時則還是 null
:
render() {
// props.color 的值會不動還是 null
return <CustomButton color={null} />;
}
Class Fields + Arrow Function
React 內建的 Lifecycle 方法裡的 this
都會正確指向 Component 本身,但如果是你自己新增的方法,你需要在 constructor() 自己綁定 (bind) this。
例如:
class MyComponent extends React.Component {
constructor(props) {
super(props);
// 要自己 bind this
this.handleClick = this.handleClick.bind(this);
}
render() {
return (
<button onClick={this.handleClick}>
This is a button
</button>
)
}
handleClick() {
this.updateList();
}
}
但更方便的方式是用 Class Fields (類別屬性) 語法和 ES6 的 Arrow function 讓 this 被自動綁定:
class MyComponent extends React.Component {
// ...
handleClick = () => {
this.updateList();
}
}