この記事の位置づけ
React公式ドキュメントの「メインコンセプト」について、実際にReactの開発を行う中で特に参考になった章を要点の解説付きで纏めたものである。実際にReact公式ドキュメントを読み解いてもらうのが一番だが、時間がない人に要点だけでも読んでもらえるようにしたいと思い、纏めている。
公式のメインコンセプトでは、以下の章立てで説明をしている。
- Hello World
- JSXの導入
- 要素のレンダー
- コンポーネントとprops
- stateとライフサイクル
- イベント処理
- 条件付きレンダー
- リストとkey
- フォーム
- stateのリフトアップ
- コンポジションvs継承
- Reactの流儀
当記事では、以下の3章分について要点まとめをしている。
4. コンポーネントとprops
5. stateとライフサイクル
6. イベント処理
出典元資料
React公式ドキュメントのメインコンセプト
メインコンセプト
4. コンポーネントとprops
知っておくべきポイント
コンポーネントの種類
Reactのコンポーネントには「関数コンポーネント」と「クラスコンポーネント」の二種類がある。
関数コンポーネント
関数コンポーネントには以下の特徴がある。
- JavaScriptの関数として定義する
- propsを引数として受け取ることができる
- stateは持たない
- React要素を返す
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}
クラスコンポーネント
クラスコンポーネントには以下の特徴がある。
- React.Componentクラスを継承したクラスとして定義する
- propsを引数として受け取ることができる
- stateを持つ
- render()関数でReact要素のイミュータブルな置き換えを定義する
関数コンポーネントとクラスコンポーネントの使い分け
以下の基準で関数コンポーネントとクラスコンポーネントを使い分けるのがよいと考えている。
-
コンポーネントに状態を持たせる必要があるか?
- 状態を持たせる必要がないなら、関数コンポーネントを使用する
- 状態持たせる必要があるなら、クラスコンポーネントを使用する
-
リファクタリングでstateのリフトアップを実施した場合
- 子のコンポーネントはstateを持たなくなるので、関数コンポーネントに変更する
- 親のコンポーネントはstateを持つ必要があるので、クラスコンポーネントを使用する
関数コンポーネントは状態を持たないのでステートレスコンポーネント、クラスコンポーネントは状態を持つのでステートフルコンポーネントと一般的に呼ばれる。
stateのリフトアップでstateはできるだけ親に集約することを推奨している通り、ステートフルコンポーネントはできるだけ少なくし、基本的にステートレスコンポーネントでUIを構成することが望ましい。
ユーザ定義のコンポーネントを使用する
以下の例のように、Welcome
というコンポーネントを定義してレンダリングすることが可能。
ユーザ定義のコンポーネントを定義することで、コンポーネント単位のUI分割や再利用が可能となる。
※以下の例でもWelcomeコンポーネントを3回再利用している
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}
function App() {
return (
<div>
<Welcome name="Sara" />
<Welcome name="Cahal" />
<Welcome name="Edite" />
</div>
);
}
ReactDOM.render(
<App />,
document.getElementById('root')
);
React公式は、できるだけUI部品を小さなコンポーネントに分割することを推奨している。
その理由としては以下の通り。
- コンポーネントが大きくなると、React要素内のネストが深くなるため変更が難しくなる
- 個々のUI部品を再利用するのが困難になる
React公式は、経験則から以下のケースでコンポーネントの分割と再利用を検討すべきとしている。
- 複数回使われるUI(
Button
、Panel
、Avatar
など) - UI自体が複雑である場合(
App
、Comment
など)
コンポーネントのpropsは変更してはいけない
Reactのルールとして、すべてのReactコンポーネントは自身のpropsを変更してはいけないという取り決めがある。これは、Reactのコンポーネントは純粋関数でなければいけないというルールがあるためである。
純粋関数とは?
渡された値を変更せず、必ず同じ結果を返す関数を純粋関数と呼ぶ。
以下のような関数は、必ずa+bの結果を返すので純粋関数と呼べる。
function sum(a, b) {
return a + b;
}
対して、以下のような関数は渡された値を変更しているため、純粋関数とは呼べない。
function withdraw(account, amount) {
account.total -= amount;
}
一言コメント
React公式チュートリアルをやっただけでは、以下の2点は曖昧なままなのでやはりメインコンセプトは読んでおく価値があると思う。
- Reactコンポーネントには関数コンポーネントとクラスコンポーネントが存在する
- Reactコンポーネントは純粋関数でなければいけない
5. stateとライフサイクル
知っておくべきポイント
クラスコンポーネントの作成
Reactコンポーネントに状態を持たせる場合はクラスコンポーネントを使うというのは、「4. コンポーネントとprops」の章で紹介したとおり。
クラスコンポーネントは以下のような構成となっている。
class Clock extends React.Component { //ポイント1
render() { //ポイント2
return ( //ポイント3
<div>
<h1>Hello, world!</h1>
<h2>It is {this.props.date.toLocaleTimeString()}.</h2>
</div>
);
}
}
ReactDOM.render(
<Clock date={new Date()} />,
document.getElementById('root')
);
ポイントは以下の3点。
- React.Componentを継承するクラスを作成する
- render()メソッドを定義し、returnでReact要素を返す
- コンポーネントのpropsにはthis.propsでアクセスする
クラスコンポーネントにローカルのstateを追加する
上記のクラスコンポーネントには状態をまだ持たせていない。
コンポーネントの状態(ローカルのstate)を持たせると、以下のような構成となる。
class Clock extends React.Component {
constructor(props) {
super(props); //ポイント1
this.state = {date: new Date()}; //ポイント2
}
render() {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {this.state.date.toLocaleTimeString()}.</h2> //ポイント3
</div>
);
}
}
ReactDOM.render(
<Clock />,
document.getElementById('root')
);
ポイントは以下の3点。
- コンストラクタで親クラスにpropsを渡す
- コンポーネントのローカルstate(
this.state
)に値を格納する - ローカルstateには
this.state
でアクセスする
クラスコンポーネントのライフサイクルメソッド
多数のコンポーネントを有するアプリケーションの場合、コンポーネントが不要となったときにコンポーネントが保持しているリソースを開放することが重要。
それを実現するために、Reactのクラスコンポーネントはライフサイクルメソッドという仕組みを持っている。
class Clock extends React.Component {
constructor(props) {
super(props);
this.state = {date: new Date()}; //ポイント1
}
componentDidMount() { //ポイント2
this.timerID = setInterval(
() => this.tick(),
1000
);
}
componentWillUnmount() { //ポイント3
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')
);
ポイントは以下の2点。
- Clockコンポーネントのローカルstateに現在日付をセットしている
- ClockコンポーネントがDOMとして描画された後に
componentDidMount()
メソッドが実行される- このことを、Reactではマウント(mounting)と呼ぶ
- この例では、
this.timerID
に1秒のタイマーをセットしている - タイマーが1秒過ぎると
tick()
が実行され、ローカルstateの日時が更新される- これによって、ClockコンポーネントのDOMが描画されてから1秒後に日時が更新されることになる
- Clockコンポーネントが生成したDOMが削除されるときに
componentWillUnmount()
メソッドが実行される- このことを、Reactではアンマウント(unmounting)と呼ぶ
- この例では、
this.timerID
の1秒タイマーをリセットしている- これによって、ClockコンポーネントのDOMが描画されるとタイマーがリセットされることになる
stateを正しく使用する
Reactコンポーネントのstateを扱うにあたって、いくつか知っておくべきことがある。
stateは直接変更しない
this.state
の値を直接変更してはいけない。直接変更すると、Reactは値の変更を検知できないためDOMが再レンダーされない。
this.state
の値を直接変更してよいのは、Reactコンポーネントのコンストラクタ内だけである。
(コンストラクタでthis.state
に初期値を設定する処理は再レンダーの必要がないため)
this.state
の値を変更したい場合、this.setState()
を使用する。this.setState()
による値の変更はReactが検知できるため、値の変更によってDOMの再レンダーが行われる。
// this.stateを直接変更するのはコンストラクタ内以外ではNG
this.state.comment = 'Hello';
// this.stateの値変更はthis.setState()で行う
this.setState({comment: 'Hello'});
stateの更新は非同期に行われることがある
Reactはthis.props
とthis.state
を非同期で更新するため、stateの値変更がthis.props
とthis.state
の値に依存していると意図しない動作になる恐れがある。
例えば、以下のケースではthis.state.counter
の値変更がthis.state.counter
の値とthis.props.increment
の値に依存している。
this.props
とthis.state
は非同期で更新されるので、this.state.counter
の加算が非同期で実行される前に、次の加算処理が行われる可能性がある。そうすると、処理結果が意図しないものになる恐れがある。
this.setState({
counter: this.state.counter + this.props.increment,
});
この問題を回避するために、this.setState()
にはthis.state
やthis.props
の値を使用しての値変更を行う際に同期的に値変更を行う方法が用意されている。
以下のように、this.setState
に関数(第1引数にstate、第2引数にprops)を渡すことで、stateの値変更は値が確定している状態のstateとpropsが使われる。
this.setState((state, props) => ({
counter: state.counter + props.increment
}));
単方向データフロー
いかなるstateも必ず特定のコンポーネントが所有しており、stateから生ずるすべてのデータやUIは、コンポーネントの親子関係における子のコンポーネント側にのみ影響するデータフローとなっている。
このようなデータフローは一般的には単方向データフローと呼ばれている。
コンポーネントのツリー構造において、React公式はデータフローのイメージを「propsをデータが流れ落ちる滝であるとすれば、各コンポーネントのstateは任意の場所から合流してくる追加の水源であり、それもまた下に流れ落ちていくもの」と表現している。
一言コメント
React公式チュートリアルをやっていれば、stateを直接変更してはならないことは知っているはず。
ただ、ライフサイクルメソッドという機能についてはチュートリアルだけでは知らないままなので、この機能は是非抑えておきたい。
6. イベント処理
知っておくべきポイント
Reactにおけるイベント処理
onclickなどのDOM要素のイベントと同様の動作を、Reactのイベント処理として設定することが可能。
DOM要素のonclickイベントでactivateLasers関数を実行する例
<button onclick="activateLasers()">
Activate Lasers
</button>
ReactのonClickイベント処理でactivateLasers関数を実行する例
<button onClick={activateLasers}>
Activate Lasers
</button>
JSXのコールバックにおけるthis
の扱い
Reactのイベント処理でクラスメソッドを呼び出す場合、以下の3つの方法がある。
方法1:クラスコンポーネントのコンストラクタでthisを束縛しておく
コンストラクター内でthis.handleClick
で使用するthisを束縛しておく。
ただ、この方法だとすべてのクラスメソッドについてthisを束縛しておく必要があるので、クラスメソッドの数が多い場合はあまり現実的ではない。
class Toggle extends React.Component {
constructor(props) {
super(props);
this.state = {isToggleOn: true};
this.handleClick = this.handleClick.bind(this); //コンストラクタで各クラスメソッドにthisをbindする
}
handleClick() {
this.setState(state => ({
isToggleOn: !state.isToggleOn
}));
}
render() {
return (
<button onClick={this.handleClick}>
{this.state.isToggleOn ? 'ON' : 'OFF'}
</button>
);
}
}
方法2:パブリッククラスフィールド構文を使用する(Create React Appの場合はデフォルトでこの書き方が可能)
クラスメソッドをアロー演算子の関数として定義しておくことで、thisを予め束縛しておくことが可能。
Create React Appでアプリを初期生成した場合、この構文はBabelで予め有効になっている。
class LoggingButton extends React.Component {
handleClick = () => { //handleClickメソッドをアロー演算子の関数として定義することでthisを束縛する
console.log('this is:', this);
}
render() {
return (
<button onClick={this.handleClick}>
Click me
</button>
);
}
}
方法3:Reactのイベント処理でアロー関数を使用する(パブリッククラスフィールド構文を使用していない場合はこの手法を取る)
JSX内のReactイベント処理をアロー関数で記述することで、thisを束縛した状態でクラスメソッドを呼び出すことができる。
class LoggingButton extends React.Component {
handleClick() {
console.log('this is:', this);
}
render() {
return (
<button onClick={() => this.handleClick()}> //Reactイベント呼び出し時にアロー演算子を使用することでthisを束縛する
Click me
</button>
);
}
}
Reactのイベントハンドラに引数を渡す
Reactのイベント処理に対して、引数を渡すことができる。
第1引数は任意の値、第2引数はReactイベントを渡す。
<button onClick={(e) => this.deleteRow(id, e)}>Delete Row</button>
一言コメント
Reactによるイベント処理は、クリックしたときやフォーカスが外れたときの動作を定義したいときによく使う。thisの束縛については、Create React Appを使用しているのなら「パブリッククラスフィールド構文」を使用しておけば間違いないと思うが、thisはJSの理解を深めるために避けて通れないので、他の書き方についても理解しておいたほうがよいと思う。
また、Reactのイベントハンドラに引数を渡すのはよくあるので、これも書き方を覚えておいたほうがよい。