reactjs
React

React を、サンプルを実装しながら理解する

ReactFormApp.gif

画像のようなサンプルアプリを実装してみます。

公式のチュートリアル でも同じように学習できますが、この記事は

  • より単純で分量が少ない
  • 日本語で書かれている
  • 同じアプリを 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 の中身を以下のように、必要最低限に書き換えます:

src/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> の部分をインプット要素に置き換えます:

src/App.js(抜粋)
class App extends Component {
  render() {
    return (
      <div>
        <input type="text" />
        <button>SEND</button>
      </div>
    );
  }
}

テキストボックスとボタンが表示されるのを確認したら、これをコンポーネント化し、別のファイルに切り出します2

src/App.js
import React, { Component } from 'react';
import { FormApp } from './FormApp';

class App extends Component {
  render() {
    return (
      <FormApp />
    );
  }
}

export default App;
src/FormApp.js
import React, { Component } from 'react';

export class FormApp extends Component {
  render() {
    return (
      <div>
        <input type="text" />
        <button>SEND</button>
      </div>
    );
  }
}

コンポーネントは、Component クラスを拡張したクラスです。
<FormApp /> のように呼び出すと、render() メソッドが実行され、その戻り値が描画されます。

ブラウザーがリロードされ、同じ見た目となれば成功です。

フォームにイベント検知機能を追加する

まずは、ボタンを押されたことを検知し、コンソールに文字を出力してみます:

src/FormApp.js(抜粋)
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 ハンドラーを割り当てます:

src/FormApp.js(抜粋)
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 を設定します:

src/FormApp.js(抜粋)
export class FormApp extends Component {
  constructor(props) {
    super(props);

    this.state = {
      value: '',
      message: ''
    };
  }

constructor の引数 propssuper(props) の呼び出しは、今はおまじないだと思ってください。
重要なのは this.state = {...} のほうで、ここで valuemessage という状態を初期化しています。

value はテキストボックスの中身、message はボタンを押すと表示されるメッセージを表すために使います:

src/FormApp.js(抜粋)
  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() を修正することで、テキストボックスへの入力が有効になります:

src/FormApp.js(抜粋)
  handleInput({ target: { value } }) {
    this.setState({
      value
    });
  }

ES6 の文法は見慣れないかもしれませんが、以下のように書いたのと同じです:

src/FormApp.js(抜粋)
  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() を以下のように修正します:

src/FormApp.js(抜粋)
  send() {
    const { value } = this.state;
    this.setState({
      value: '',
      message: value
    });
  }

value、すなわちテキストボックスを空にし、div の中身に値を移しています。
ボタンを押すとテキストボックスが空になり、そのすぐ下に中身が移るアプリができていれば、成功です。

Refactoring: チュートリアルどおりのコードスタイルに近づける

constructor()render() の中身を修正します:

src/FormApp.js(抜粋)
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 つのおまじないです。

最終的なコード

src/App.js
import React, { Component } from 'react';
import { FormApp } from './FormApp';

class App extends Component {
  render() {
    return (
      <FormApp />
    );
  }
}

export default App;
src/FormApp.js
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 による実装を載せておきます:

src/app/form-app/form-app.component.ts
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;
  }
}

参考文献


  1. 私も React 初学者なので、当然品質も異なります。みなさまのフィードバックをお待ちしています。 

  2. 同じファイルに書いても動作しますが、コンポーネントを再利用することを考え、別ファイルにしています。