画像のようなサンプルアプリを実装してみます。
公式のチュートリアル でも同じように学習できますが、この記事は
- より単純で分量が少ない
- 日本語で書かれている
- 同じアプリを Angular でも実装している
- Redux 導入の前段階にできる(Redux の記事は後日作成予定)
これらの点が異なっています。1
サンプル実装の手順
まずは Hello world
Create React App を使い、まずは環境をセットアップします。
Create React App を npm (Yarn や npx など任意のツールで構いません)でインストールし、新規プロジェクトを作成、開発サーバーを起動します:
yarn global add create-react-app
create-react-app qiita
cd qiita
yarn start
App.js の中身を以下のように、必要最低限に書き換えます:
import React, { Component } from 'react';
class App extends Component {
render() {
return (
<h1>Hello world</h1>
);
}
}
export default App;
ブラウザーがリロードされ、画面に Hello world が表示されたら成功です。
CodePen で環境設定をスキップする
公式チュートリアルの CodePen を流用し、ブラウザー上で試すこともできます。
その際は、以下のような違いに気をつけます:
- import/export 文なしにする
- Component クラスは
React.Component
として参照する -
ReactDOM.render()
のおまじないを書く - ファイル分けはできないので、一か所に全クラスを書く
コードはこのようになります:
class App extends React.Component {
render() {
return (
<h1>Hello world</h1>
);
}
}
ReactDOM.render(
<App />,
document.getElementById('root')
);
フォームの見た目だけ作成する
<h1>Hello world</h1>
の部分をインプット要素に置き換えます:
class App extends Component {
render() {
return (
<div>
<input type="text" />
<button>SEND</button>
</div>
);
}
}
テキストボックスとボタンが表示されるのを確認したら、これをコンポーネント化し、別のファイルに切り出します2:
import React, { Component } from 'react';
import { FormApp } from './FormApp';
class App extends Component {
render() {
return (
<FormApp />
);
}
}
export default App;
import React, { Component } from 'react';
export class FormApp extends Component {
render() {
return (
<div>
<input type="text" />
<button>SEND</button>
</div>
);
}
}
コンポーネントは、Component クラスを拡張したクラスです。
<FormApp />
のように呼び出すと、render()
メソッドが実行され、その戻り値が描画されます。
ブラウザーがリロードされ、同じ見た目となれば成功です。
フォームにイベント検知機能を追加する
まずは、ボタンを押されたことを検知し、コンソールに文字を出力してみます:
export class FormApp extends Component {
send() {
console.log('send called!');
}
render() {
return (
<div>
<input type="text" />
<button onClick={this.send.bind(this)}>SEND</button>
</div>
);
}
}
send()
メソッドを button 要素の onClick に割り当てています。
{}
の中身は JavaScript の式として扱われるので、Angular のように onClick={this.send()}
としてしまうと、onClick には send()
の戻り値 undefined が割り当てられます。
それでは意味がないので、そうしないよう気をつけます。
また onClick={this.send}
としてしまうと、今は問題ありませんが、今後 send()
内で this
を使ったときにエラーになる(this
が undefined となる)ため、bind()
メソッドで this
を固定しています。
同様に、input 要素にも onChange ハンドラーを割り当てます:
export class FormApp extends Component {
handleInput() {
console.log('handleInput called!');
}
send() {
console.log('send called!');
}
render() {
return (
<div>
<input type="text" onChange={this.handleInput.bind(this)} />
<button onClick={this.send.bind(this)}>SEND</button>
</div>
);
}
}
状態 (state) を持たせ、状態を更新する
constructor を追加し、その中で特殊なプロパティ state を設定します:
export class FormApp extends Component {
constructor(props) {
super(props);
this.state = {
value: '',
message: ''
};
}
constructor の引数 props
と super(props)
の呼び出しは、今はおまじないだと思ってください。
重要なのは this.state = {...}
のほうで、ここで value
と message
という状態を初期化しています。
value
はテキストボックスの中身、message
はボタンを押すと表示されるメッセージを表すために使います:
render() {
return (
<div>
<input type="text" value={this.state.value} onChange={this.handleInput.bind(this)} />
<button onClick={this.send.bind(this)}>SEND</button>
<div>{this.state.message}</div>
</div>
);
}
input 要素には value={this.state.value}
を追加し、message
は <div>{this.state.message}</div>
という形でボタンの次に追加しました。
初期値を変えると、その値が画面に表示されることが確認できます。
この段階では、テキストボックスに入力することができませんが、それは正常な動きです。
次のように handleInput()
を修正することで、テキストボックスへの入力が有効になります:
handleInput({ target: { value } }) {
this.setState({
value
});
}
ES6 の文法は見慣れないかもしれませんが、以下のように書いたのと同じです:
handleInput(event) {
let value = event.target.value;
this.setState({
value: value
});
}
React には厳格なルールがあり、その一つがこれです。
setState()
メソッド以外に state を変える方法がないという点です。
たとえユーザーの入力であっても、input 要素の value が value={this.state.value}
となっている限り、テキストボックスの中身は this.state.value
のままで不変です。
Angular は value の変化を自動で this.state.value
に反映し、state の中身を変えてくれますが (two-way binding)、React は onChange イベントを呼び出すだけです。
わざわざ setState()
を呼び出すのは無駄に見えますが、明示的に書かせることで、コードを読んでデータの流れがすぐわかるという利点を得られるのです。
この FormApp 程度ではその恩恵はありませんが、そういうものだと思って進んでください。
テキストボックスに入力が可能で、ボタンを押すとコンソールにログが出る状態のアプリができていれば、成功です。
ボタンの機能を追加する
最後の機能を追加して、アプリを完成させます。
send()
を以下のように修正します:
send() {
const { value } = this.state;
this.setState({
value: '',
message: value
});
}
value
、すなわちテキストボックスを空にし、div の中身に値を移しています。
ボタンを押すとテキストボックスが空になり、そのすぐ下に中身が移るアプリができていれば、成功です。
Refactoring: チュートリアルどおりのコードスタイルに近づける
constructor()
と render()
の中身を修正します:
export class FormApp extends Component {
constructor(props) {
super(props);
this.state = {
value: '',
message: ''
};
this.handleInput = this.handleInput.bind(this);
this.send = this.send.bind(this);
}
// ...
render() {
return (
<div>
<input type="text" value={this.state.value} onChange={this.handleInput} />
<button onClick={this.send}>SEND</button>
<div>{this.state.message}</div>
</div>
);
}
}
render()
が呼ばれるたびに bind()
メソッドを呼ぶのではなく、constructor 内で呼ぶようにして計算コストを下げました。
この規模のアプリでわざわざそうしたのは、性能を気にしたからではなく、constructor の中身に定型句を揃えたかったからです。
super(props)
、state の初期化そしてメソッドの bind という 3 つのおまじないです。
最終的なコード
import React, { Component } from 'react';
import { FormApp } from './FormApp';
class App extends Component {
render() {
return (
<FormApp />
);
}
}
export default App;
import React, { Component } from 'react';
export class FormApp extends Component {
constructor(props) {
super(props);
this.state = {
value: '',
message: ''
};
this.handleInput = this.handleInput.bind(this);
this.send = this.send.bind(this);
}
handleInput({ target: { value } }) {
this.setState({
value
});
}
send() {
const { value } = this.state;
this.setState({
value: '',
message: value
});
}
render() {
return (
<div>
<input type="text" value={this.state.value} onChange={this.handleInput} />
<button onClick={this.send}>SEND</button>
<div>{this.state.message}</div>
</div>
);
}
}
以上でおしまいです。
おつかれさまでした。
Angular で実装した場合
参考として、Angular による実装を載せておきます:
import { Component } from '@angular/core';
@Component({
selector: 'app-form-app',
template: `
<div>
<input type="text" [(ngModel)]="value">
<button (click)="send()">SEND</button>
<div>{{message}}</div>
</div>
`
})
export class FormAppComponent {
value = '';
message = '';
send() {
const value = this.value;
this.value = '';
this.message = value;
}
}