はじめに
Fullstack React: 30 Days of React の意訳です。
ちょいちょい読んでるのでせっかくなので訳そうと思った次第。飽きるまで続きます。かなり意訳です。
- Day1 - What is React?
- Day2 - What is JSX?
- Day3 - Our First Components
- Day4 - Complex Components
- Day5 - Data-Driven Components
今日はちょっと酔っ払っています。
本編
今日はステートフルなコンポーネントについてやっていきます。どんなときになんでReactでStateを使っていけばいいのかを見ていきましょう。
最初の5DaysにReactの基礎分かったと思います。JSX、コンポーネントの親子関係、プロパティなどです。Day6ではもう一つの重要な要素であるStateについて見ていきます。
The state of things
Reactではプロパティ( this.prop
)の変更は許可していません。Day5まででみた <Header />
コンポーネントのタイトルを変更できたとします。こうすると元々のタイトルが何であったのかわからなくなってしまいます。親コンポーネントから受け渡されたプロパティを、子コンポーネントで変更できてしまうのは、よくないアイディアです。
ただ、コンポーネントで値を保持して、更新たい場合というのはもちろんあります。例えばストップウォッチの例を考えてみましょう。
出来る限りプロパティを使うことが好ましいですが、状態を保持しないといけないときには、Stateが使えます。
stateは、コンポーネントとその子コンポーネントからのみアクセスされるものです(親からアクセスさせないのが良い?)。stateには this.state
でアクセスでき、 this.setState()
で値を変更すると、viewが再レンダリングされます。
シンプルな時計コンポーネントを見てみましょう。
※本家だと動いています
このコンポーネントは現在の時刻を保持する必要があります。stateを使わなくても、画面全体を再レンダリングすることで時刻は更新可能ですが、他のコンポーネントは再レンダリングの必要がないかもしれません。
そこでstateを使うと、コンポーネント内で時刻を保持し、値の更新と再レンダリングを行うことができます。
このコンポーネントを作っていきましょう。stateの話に入る前に単純で render()
だけを行うコンポーネントを書いていきましょう。時刻の0埋めや、am/pm の分岐など考えなくてはいけないことはありますが、だいたい以下のようになるはずです。
class Clock extends React.Component {
render() {
const currentTime = new Date(),
hours = currentTime.getHours(),
minutes = currentTime.getMinutes(),
seconds = currentTime.getSeconds(),
ampm = hours >= 12 ? 'pm' : 'am';
return (
<div className="clock">
{
hours == 0 ? 12 :
(hours > 12) ?
hours - 12 : hours
}:{
minutes > 9 ? minutes : `0${minutes}`
}:{
seconds > 9 ? seconds : `0${seconds}`
} {ampm}
</div>
)
}
}
このままでは、時計の表示を更新するためにいちいちページを更新しないといけません。それでは使い物になりません。役に立つ時計にするためには、毎秒ごとに時刻の表示が更新されるようにしたいですよね。
それができるようにするには、現在時刻を毎秒取得することが必要ですが、まずは初期時刻の設定をしましょう。ES6の記法でコンストラクタ内でstateの初期値を設定することができます。
class Clock extends React.Component {
constructor(props) {
super(props); // ALWAYS required
const currentTime = new Date();
this.state = {
hours: currentTime.getHours(),
minutes: currentTime.getMinutes(),
seconds: currentTime.getSeconds(),
ampm: currentTime.getHours() >= 12 ? 'pm' : 'am'
};
}
// ...
}
一行目の
super(props)
は常に必要です。これを忘れてるとあなたのとても好きではないことがおこります(つまりエラーになります)。
React.createClass()
を使って書く場合は、コンストラクタを使う代わりに、getInitialState()
関数を定義し、その関数がオブジェクトを返すようにする必要がありまます。以下のような感じです。
const Clock = React.createClass({
getInitialState: function() {
const currentTime = new Date();
return {
hours: currentTime.getHours(),
minutes: currentTime.getMinutes(),
seconds: currentTime.getSeconds(),
ampm: currentTime.getHours() >= 12 ? 'pm' : 'am'
}
}
})
これでstateに初期値を設定したので、render関数からstateにアクセスすることができます。render関数を書き換えてみましょう。
render() {
const {hours, minutes, seconds, ampm} = this.state;
return (
<div className="clock">
{
hours == 0 ? 12 :
(hours > 12) ?
hours - 12 : hours
}:{
minutes > 9 ? minutes : `0${minutes}`
}:{
seconds > 9 ? seconds : `0${seconds}`
} {ampm}
</div>
)
}
次に setTimeout()
を使って1000ms毎に this.state
を更新できるようにしましょう。この機能はあとで呼び出せるように関数として定義しておきましょう。
class Clock extends React.Component {
constructor(props) {
super(props); // ALWAYS required
const currentTime = new Date();
this.state = {
// ...
};
this.setTimer(); // start up the timeout
}
setTimer() {
setTimeout(this.updateClock.bind(this), 1000);
}
updateClock() {
// This will be called in one second
}
// ...
}
次のセクションでライフサイクルフックについて説明しますが、いったんはコンストラクターに書いておきましょう
updateClock()
関数の中では、stateを新しい時刻に更新する必要があります。こんな感じです。
class Clock extends React.Component {
// ...
updateClock() {
const currentTime = new Date();
this.setState({
hours: currentTime.getHours(),
minutes: currentTime.getMinutes(),
seconds: currentTime.getSeconds(),
ampm: currentTime.getHours() >= 12 ? 'pm' : 'am'
})
}
// ...
}
コンポーネントがマウントされて1秒(1000ms)で時刻が更新されますが、これでは1度更新されて終了になってしまいます。関数の最後にsetTimer()
関数を呼び出して、もう1秒たったら再度時刻が更新されるようにします。
class Clock extends React.Component {
// ...
updateClock() {
const currentTime = new Date();
this.setState({
// ...
})
this.setTimer();
}
// ...
}
ここでコンポーネント再レンダリングは、timeout関数よりも遅い可能性がらあります。これはレンダリングのボトルネックを引き起こし、モバイルデバイスではムダなバッテーリー消費につながります。再レンダリングが確実に終わった後に、 setTimer()
が呼ばれるように setState()
の引数に関数を渡す方法が使えます。
※補足: setState()
の処理が終わりきらないうちに、次の setState()
が呼ばれて、処理のキューにどんどんつまっていくイメージ。このコンポーネントではまず起こらないと思うけど、複雑な再レンダリングをする場合のために覚えておくと良い
Update our activity list
Day5で見ていたアクティビティリストのヘッダーコンポーネントを改良してみましょう。虫メガネマークをクリックしたらinput要素が表示されるようにします。
stateを使って、inputの見える/見えないの状態を保持することができます。
class Header extends React.Component {
constructor(props) {
super(props);
this.state = {
searchVisible: false
}
}
// toggle visibility when run on the state
showSearch() {
this.setState({
searchVisible: !this.state.searchVisible
})
}
render() {
// Classes to add to the <input /> element
let searchInputClasses = ["searchInput"];
// Update the class array if the state is visible
if (this.state.searchVisible) {
searchInputClasses.push("active");
}
return (
<div className="header">
<div className="fa fa-more"></div>
<span className="title">
{this.props.title}
</span>
<input
type="text"
className={searchInputClasses.join(' ')}
placeholder="Search ..." />
{/* Adding an onClick handler to call the showSearch button */}
<div
onClick={this.showSearch.bind(this)}
className="fa fa-search searchIcon"></div>
</div>
)
}
}
覚えてほいてほしいこと
-
this.setState()
にオブジェクトを渡すと、this.state
オブジェクトとシャローマージが行われて、コンポーネントが再レンダリングされます。(補足: シャローマージとは、同じキーがあり、そのキーがさらにオブジェクトを持っていたとしても、そのオブジェクトはマージされずに上書きされるということです) - レンダリングに関係のある値だけをstateに保持するようにしましょう。CPUの無駄遣いになる可能性があるため。
最初に書きましたが、ステートフルなコンポーネントはテストが難しいため、可能であるならば props
を使うように心がけましょう。
Day6ではステートフルなコンポーネントを扱う方法を学びました。Day7ではコンポーネントのライフサイクルと、いつどのようにページとやり取りをするのかを見ていきます。