State 與生命週期
在前面介紹過的技巧中,我們只能靠著 ReactDOM.render()
更新畫面:
function tick() { const element = ( <div> <h1>Hello, world!</h1> <h2>It is {new Date().toLocaleTimeString()}.</h2> </div> ); ReactDOM.render( element, document.getElementById('root') ); } setInterval(tick, 1000);
接下來我們將修改這個範例,建立一個 Clock
組件,讓程式碼可以重複使用。
首先建立一個 Clock
組件,將相關的 UI 獨立出來:
// 建立 Clock 組件 function Clock(props) { return ( <div> <h1>Hello, world!</h1> <h2>It is {props.date.toLocaleTimeString()}.</h2> </div> ); } function tick() { ReactDOM.render( <Clock date={new Date()} />, document.getElementById('root') ); } setInterval(tick, 1000);
獨立出 Clock
之後,我們希望它可以獨立運作,不要讓我們還要定時去更新它,也就是說它最好可以這樣使用:
// 獨立運作的 Clock 組件 ReactDOM.render( <Clock />, document.getElementById('root') );
若要達到這樣的功能,我們須在 Clock
中加入一個 state(意指狀態),這個 state 就類似 props 一樣,可包含一些資料,但不同的地方是 state 存在於組件內部(私有的),只受組件本身的控制。
將函數轉為類別
接下來我們要使用的功能比較複雜一些,所以要以 ES6 class 的方式來定義組件。我們先把前面的 Clock
以 ES6 class 的方式改寫:
// 以 ES6 class 定義的 Clock 組件 class Clock extends React.Component { render() { return ( <div> <h1>Hello, world!</h1> <h2>It is {this.props.date.toLocaleTimeString()}.</h2> </div> ); } }
在類別中加入 State
將原本的 this.props.date
以 this.state.date
取代:
class Clock extends React.Component { render() { return ( <div> <h1>Hello, world!</h1> <h2>It is {this.state.date.toLocaleTimeString()}.</h2> </div> ); } }
加入類別的建構子(constructor),在建構子中自己設定目前時間,而 props
的值則從父類別繼承:
class Clock extends React.Component { // 加入建構子 constructor(props) { super(props); this.state = {date: new Date()}; } render() { return ( <div> <h1>Hello, world!</h1> <h2>It is {this.state.date.toLocaleTimeString()}.</h2> </div> ); } }
接著就可以使用這個自動的 Clock
了:
// 使用自動的 Clock ReactDOM.render( <Clock />, document.getElementById('root') );
生命週期
在較大型的應用程式中,我們必須注意資源的配給與回收問題,在資源不再被使用時,就需要將其回收。
目前我們的 Clock
只會自己設置一開始的時間,後續並不會更新。接下來我們要在 Clock
顯示時,加上一個 timer,讓它可以自動持續更新時間,然後在 Clock
被移除時,自動清除 timer,這些動作可以透過類別的掛載(mount)與卸載(unmount)功能來達成。
class Clock extends React.Component { constructor(props) { super(props); this.state = {date: new Date()}; } // 掛載函數 componentDidMount() { this.timerID = setInterval( () => this.tick(), 1000 ); } // 卸載函數 componentWillUnmount() { clearInterval(this.timerID); } tick() { this.setState({ date: new Date() }); } render() { return ( <div> <h1>Hello, world!</h1> <h2>It is {this.state.date.toLocaleTimeString()}.</h2> </div> ); } } ReactDOM.render( <Clock />, document.getElementById('root') );
這裡我們靠著掛載函數(componentDidMount
)讓 Clock
在顯示後,自動啟動 timer,定時呼叫 tick()
更新 state 的時間,然後藉由卸載函數(componentWillUnmount
)在 Clock
要移除前,註銷 timer。
State 注意事項
關於 state 的使用,有三個重點。
不可直接更改 State
若要更改 state 裡面的資料,不可以直接更改:
// 錯誤用法 this.state.comment = 'Hello';
要改用 setState
來設定新的值:
// 正確用法 this.setState({comment: 'Hello'});
在程式中,唯一可以直接更改 state 的地方就是在建構子之中,除此之外都必須呼叫 setState
。
State 的更新是非同步的
由於 this.props
與 this.state
的更新可能是非同步的,所以不可以直接根據其值來計算:
// 錯誤用法 this.setState({ counter: this.state.counter + this.props.increment, });
若需要根據舊的 state 來計算,要改成這樣:
// 正確用法 this.setState((prevState, props) => ({ counter: prevState.counter + props.increment }));
這裡我們使用箭頭函數來簡化程式碼,若用傳統的語法則會像這樣:
// 正確用法 this.setState(function(prevState, props) { return { counter: prevState.counter + props.increment }; });
更新的 State 值會合併
state 中可以儲存多種資料,我們可以只更新其中一部份,它會自動跟原有的值合併。假設我們的 state 有兩個值:
constructor(props) { super(props); this.state = { posts: [], comments: [] }; }
我們可以個別更新這兩個值:
componentDidMount() { fetchPosts().then(response => { this.setState({ posts: response.posts }); }); fetchComments().then(response => { this.setState({ comments: response.comments }); }); }
資料向下流動原則
組件的 state 資料只有組件本身可以存取,其餘不管是父組件或是子組件都無法直接得知其內容,因此 state 有區域性以及封裝的特性。
組件可以將自己的 state 傳遞給子組件:
<h2>It is {this.state.date.toLocaleTimeString()}.</h2>
自行定義的組件亦可:
<FormattedDate date={this.state.date} />
而 FormattedDate
在收到 date
之後,並不會知道該資料是來自於何處。
function FormattedDate(props) { return <h2>It is {props.date.toLocaleTimeString()}.</h2>; }
組件的 state 資料可以傳遞給其下方的子組件,但是不可以朝其他方向傳遞,React 中的資料都遵守這種向下流動原則。
文章撰寫中…
參考資料:React、從零開始學 ReactJS、Peter Chang
NTUDog
寫得真好 期待下篇