サーバーサイドプログラマーのためのReact.js 入門 3. いよいよ React.js を書く

いよいよ React.js 入門

開発環境作成に2回も使いましたが、今回から React.js を始めます。

開発環境というかエディターですが、主なエディターは JSX, ES6 をサポートする拡張パッケージ、マクロ等があると思いますので入れておきましょう。もちん WebStorm のようなIDEも良いかもしれません。

これから作るTodoアプリはRuby on Railsのサーバーとhttpプロトコルで通信する必要があります。そこで今回は通信部分には、Promisベースの今風なHTTP clinet ライブラリー axios を使ってみます、 使い方は極めてシンプルです。

インストールは

$ npm install --save-dev axios

まずは、超簡単なReact.js アプリを動かす

前回書いた index.js を以下のように変更してください。もちろんターミナルでは webpack -d -w を実行して下さい。 また、別のターミナルでは rails sRuby on Rails のサーバーも起動しておいて下さい。

import React, { Component } from 'react'
import ReactDOM from 'react-dom'
import axios from 'axios'

class IndexPage extends Component {
  constructor(props) {
    super(props)
    this.state = {todos: []}
  }
  componentDidMount() {
    axios.get('/todos.json').then((response) => {
      this.setState({todos: response.data})
    }).catch((response) => {
      console.log(response)
    })
  }
  render() {
    return (
      <div>
        <h2>List of Todos</h2>
        {this.state.todos.map((todo) => <div>{todo.task}</div>)}
      </div>
    )
  }
}

ReactDOM.render(
  <IndexPage />,
  document.getElementById('example')
)

React.js は 画面をコンポーネント(部品)に分割し、そのコンポーネントは独自のHTMLタグになります。 上では <IndexPage> というコンポーネントを定義しそれを一番下の ReactDOM.render() で index.html にある <div id='example'></div> の中に埋め込むことでコンポーネントが表示されます。

f:id:yuum3:20160225155206p:plain

ブラウザーのConsoleに warnig が表示されています。内容については後で説明しますが、このように React は何かおかしいところがあると warnig を表示してくれます。これは開発・デバックの助けになるのでちゃんと対応して下さい。

解説

state

Reactのコンポーネントは Component クラスを継承して作ります。クラスのコンストラクター(constructor) の中で プロパティー(インスタンス変数)を定義しています。

state は重要なプロパティーで、このコンポーネントが保持する状態やデータなどを保持するオブジェクト(Hashとして使ってます)。ここで todos は サーバーから取得したTodo情報の配列です。適切な(後で説明します)初期値を書いておいて下さい。

render

次に render() メソッドを見てみましょう。これは画面に表示されるHTMLを戻すメソッドです。 JS(JavaScript)の中にHTML タグが書かれていますが、これがJSXで環境設定で入れたBabelがこの部分を通常のJSに変換してくれます。このHTMLタグの部分は JSの式として扱われます。

JSXの中の { 。。。 } にはJSの式が書かれます、この値がタグや属性値として使われます。<div>{todo.task}</div> の todo.task はタスクの文字列になります。

this.state.todos.map の部分は state 内の todos の値(配列)からtask文字列を表示するHTMLの列を生成しています。 ES6を使ってコンパクトに書いてますが、ES5 で書くと

{this.state.todos.map(function(todo) {
  return <div>{todo.task}</div>
})}

サーバーサイドのviewと似た感じですね、サーバーサイドのviewが1ページ全体(またはページ内の一部分)をリクエスト毎に一度に作成するように Reactも必要なタイミングで一度にページ全体を作成するようにプログラミングします。

jQueryなどを使ったAjaxではプログラマーが変更すべき部分のみを書き換えるようにプログラミングする必要がありました。なぜならHTML全体を書き換えるのでは遅く使いやすいUIを実現できないからです。その代償としてコードが煩雑になり、メンテナンスしづらいコードになりがちでした。

Reactは HTML全体を書き換えるようなプログラミンをしてもライブラリーが現在のDOM(HTML構造)と新しいDOM(HTML構造)を比較し、必要な部分のみを書き換えてくれます。

この Virtual DOM という仕掛けが、画面全体を書き換えるというシンプルなプログラミンモデルと性能を両立してくれます。

Reactが生成したHTML(下の画像)を見ると、data-reactid という属性が各タグにふられています、これはVirtual DOMと実際のDOMの管理に使われているのだと思われます。

f:id:yuum3:20160225155215p:plain

componentDidMount (コンポーネントのライフサイクル)

Reactではコンポーネントの表示を明示的な制御はせず Reactライブラリーに任せます。シンプルなコンポーネントは render メソッドだけでも出来ますが、通常はデータを取得したりいろいろな処理が必要になります。そこでReactでは表示処理のあるタイミングで呼び出されるメソッドが決まっています、それを自分でオーバーライトし処理を組み込みます。 詳細はComponent Specs and Lifecycle を見て下さい。凝ったUIを作ったり、React以外のJSライブラリーと組みせる場合などはこのライフサイクルを熟知する必要があります。

現在、Reactを使いアプリを作っていますが、凝った事はしてないので、ほとんどコンポーネント

  • componentDidMount() 最初にコンポーネントが描画された後に呼び出される
  • componentWillReceiveProps(nextProps) コンポーネントが再描画される際に呼び出される(正確には新たなpropsを受け取る時、propsは後で説明します)

の2つで済んでます。 Component Specs and Lifecycle に書かれているように データの取得等はcomponentDidMountメソッドに実装します。

componentDidMountは最初の描画の後に呼び出されます、今回のコードではtodosの初期値は空配列なので、最初は <div><h2>List of Todos</h2></div> が表示されます(もちろん見えませんが)。 その後 componentDidMount が実行され RailsサーバーからTodo情報を取得してきます。 axios の詳細は axiosのGitHub に書かれていますが、見なくとも想像できると思います。

  1. get() でGETリクエストをサーバーに送り
  2. データが取得できると then() に書かれた(無名)関数が実行されます
  3. エラーが起きた場合は catch() に書かれた(無名)関数が実行されます

データが取得できたら this.setState() メソッドが呼び出されていますが Reactではstateの更新はプロパティーへの代入ではなく、このメソッドを呼び出す必要があります。

this.setState() は変更された state を使い再描画を行います。(上のrenderで説明したように Virtual DOM により実際に描かれるのはtodo.task表示のdivタグのみです)

注意点

私が、初めて Reactを使ってハマった点などを書いておきます。

  • 一度表示されたコンポーネントインスタンスがメモリ上に残っているので、他のコンポーネントから再表示を起こしても componentDidMount は実行されません。その時は componentWillReceiveProps が呼び出されます
  • 上にも書きましたが、最初はデータが空(初期値)の状態で描画が起きるので、初期値は重要です。適切な値を設定しておくか、renderの方で空データを考慮しないとnull絡みのエラーが発生します
  • renderで描かれるコンポーネントは基本的に1つのタグである必要があります、今回のコードではHTML的には最初のdivタグはいらないのですが、 このReactの制限から div で括っています
  • List of Todosの画面で発生していた Warning: Each child in an array or iterator should have a unique "key" prop. ... ですが、リストやテーブルのように要素数が可変のタグを正しくReactが認識出るようにkey 属性でユニークな値を設定する必要があります。今回のコードは正しくは以下のように書きます
  ・・・ 省略 ・・・

  render() {
    return (
      <div>
        <h2>List of Todos</h2>
        {this.state.todos.map((todo) => <div key={todo.id}>{todo.task}</div>)}
      </div>
    )
  }
}

 ・・・ 省略 ・・・

Todoリスト表示完成版

Todoリストの表示ページを、

  • ページ全体
  • Todoリスト表示
  • Todo一項目表示

というコンポーネントに分割してみましょう。コンポーネントにはHTMLの属性のように値を渡すことができます。

値を受け取るコンポーネント側では、this.props.属性名 で値をアクセスします。 この props も重要なプロパティーです。また、propsの値をコンポーネント内で変更しないのが React の考え方です。

TodoList.propTypes = { から始まる部分ですが、propの型を宣言する部分です。無くても動作しますが型宣言をしておくと 違う型が渡された場合 waring が発生するので、開発には役立つと思います。

詳細は Prop Validation を参照して下さい

import React, { Component, PropTypes } from 'react'
import ReactDOM from 'react-dom'
import axios from 'axios'

class IndexPage extends Component {
  constructor(props) {
    super(props)
    this.state = {todos: []}
  }
  componentDidMount() {
    axios.get('/todos.json').then((response) => {
      this.setState({todos: response.data})
    }).catch((response) => {
      console.log(response)
    })
  }
  render() {
    return(
      <div>
        <h2>List of Todos</h2>
        <TodoList todos={this.state.todos}/>
      </div>
    )
  }
}

class TodoList extends Component {
  render() {
    return(
      <table>
        <thead>
          <tr>
            <th>Due</th>
            <th>Task</th>
            <th colSpan="3"></th>
          </tr>
        </thead>
        <tbody>
          {this.props.todos.map((todo) => (<TodoListItem key={todo.id} todo={todo} />))}
        </tbody>
      </table>
    )
  }
}
TodoList.propTypes = {
  todos: PropTypes.array.isRequired
}

class TodoListItem extends Component {
  render() {
    return(
      <tr>
        <td> {this.props.todo.due} </td>
        <td> {this.props.todo.task} </td>
      </tr>
     )
  }
}
TodoListItem.propTypes = {
  todo: PropTypes.object.isRequired
}

ReactDOM.render(
  <IndexPage />,
  document.getElementById('example')
)

予告

次回は、Todo情報の変更などに付いて書きます。