生のReactを知ろう – JSX、Flux、ES6、Webpackを使わず…

(編注:2016/07/29、いただいたフィードバックをもとに記事を修正いたしました。)

免責事項: 私はJSX、Flux、ES6、そしてwebpackを非常に気に入っています。これらのツールについては他のシリーズで話します。

React.jsが騒ぎを起こしているのはご存知の通りです。確かに、XMLHttpRequest以来の良いツールです。しかし、調査に数時間を費やした挙句、あまりに多くの用語に圧倒されただけで終わっていないでしょうか。JSX、flux、ES6、webpack、react-routerが使える今、他に必要なのはReactの使い方を説明してくれる人だけです。

喜んでください、それがまさに当シリーズでやろうとしていることです。信じられませんか?大丈夫、2分後、初めてのReactアプリを作った後には納得いただけるでしょう。何もダウンロードせずに、です。次の練習をやってみてください。

練習 1 シングルファイルのReact.jsアプリを書こう

JavaScript、CSS、HTML を使ったことはありますか。いいですね。それならこの練習ができます。

この練習の成果物は、あなたが作る最初のReactアプリを含むHTMLファイル1点です。3つのステップがあります。

ステップ 1

このHTMLを新規ファイルにコピーし、適当な場所に保存しましょう。

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <title>I'm in a React app!</title>
  </head>
  <body>
    <div id="react-app"></div>

    <script src="https://cdn.jsdelivr.net/react/0.14.0-rc1/react.js"></script>
    <script src="https://cdn.jsdelivr.net/react/0.14.0-rc1/react-dom.js"></script>
  </body>
</html>

このHTMLファイルには2つの重要な役割があります。レンダリングしたコンテンツ(idreact-appと共に)が動くdivを作成し、Reactが動く2つのライブラリファイルをロードします。

コピーできたら、次に進みましょう。

ステップ 2

HTMLファイルの末尾に、次のスクリプトを新規<script>タグに入力します。

コピー&ペーストではなく、入力するのはなぜでしょうか。タイプすることで、各コマンドのプロセスしながら、それらが頭の中にも叩き込まれるからです。一方、コピー&ペーストをした場合、賢くなったような快感が一瞬得られるだけです。ただ快感が欲しいのなら、この記事を読むのをやめて、flappy birdか何かで遊んだほうが手っ取り早いでしょう。

var rootElement =
  React.createElement('div', {}, 
    React.createElement('h1', {}, "Contacts"),
    React.createElement('ul', {},
      React.createElement('li', {},
        React.createElement('h2', {}, "James Nelson"),
        React.createElement('a', {href: 'mailto:james@jamesknelson.com'}, 'james@jamesknelson.com')
      ),
      React.createElement('li', {},
        React.createElement('h2', {}, "Joe Citizen"),
        React.createElement('a', {href: 'mailto:joe@example.com'}, 'joe@example.com')
      )
    )
  )

ReactDOM.render(rootElement, document.getElementById('react-app'))

ちょっとした仕事ですね。しかし、繰り返し入力したことで、createElementメソッドの概念がつかめるとよいのですが。まだ見えてこない、という人にはステップ3が役立つでしょう。

ステップ 3

WebブラウザでHTMLファイルを開き、アウトプットが次のようになっていることを確認してください。

どうでしょうか。良かった、分かりましたね。あなたは初めてのReact.jsアプリをnpmさえインストールせずに作ってしまいました。お祝いを兼ねて、その機能を見ていきましょう。

Reactの導入

基本的に、ReactはHTMLをJavaScriptでレンダリングするツールです。ReactDOM.renderは、ReactElementオブジェクトを使って、何をレンダリングするかを説明し、指定されたDOM nodeに結果を追加します。しかし、ReactElementはどうやって作るのでしょうか。ここでReact.createElementの出番です。

React.createElementは3つのパラメータを引数にとり、ReactElementを返します。Reactのドキュメンテーションにはこのように記述されています。

createElement(string/ReactClass type, [object props], [children ...]) -> ReactElement

type引数が、使用するHTML要素のタイプを指定します。また、カスタム要素も指定します(後で説明します)。props引数はどの属性がHTML要素に定義されているかを指定する方法です。練習に出てきたmailtoリンクから推測できたかもしれません。最後に、children引数は、返された要素のコンテンツに使われる文字列、ReactElementオブジェクト(あるいはその配列)です。childrenを除外するか指定するか選ぶことで、ReactElement1つか、ツリー全体を返します。

createElementは素のJavaScriptなので、ループ、if文、その他JavaScriptで行えるものなら何でも挿入できます。さらに、JSONに格納されたデータに簡単に代入することも可能です。例えば、連絡先のリストを持っているものの、e-mailアドレスのないコンタクトはレンダリングしたくない場合、次のような記述が考えられます。

 var contacts = [
  {key: 1, name: "James Nelson", email: "james@jamesknelson.com"},
  {key: 2, name: "Bob"}
]

var listElements = contacts
  .filter(function(contact) { return contact.email; })
  .map(function(contact) {
    return React.createElement('li', {key: contact.key},
      React.createElement('h2', {}, contact.name),
      React.createElement('a', {href: 'mailto:'+contact.email}, contact.email)
    )
  })

var rootElement =
  React.createElement('div', {}, 
    React.createElement('h1', {}, "Contacts"),

    // If your `children` is an array, you'll need to give each one a unique `key`
    // prop. I'll explain why a little later.
    React.createElement('ul', {}, listElements)
  )

ReactDOM.render(rootElement, document.getElementById('react-app'))

すっきりしています。つまりReactは非常に多弁なJavaScriptベースのテンプレート化システムであると言えます。しかし… それでは熱心に評価される理由になりません。何が特長なのでしょうか。

コンポーネント

React.createElementのタイプシグネチャ内の小さなReactClassに気づいたでしょうか。使用しないので省きましたが、何を意味するか分かりますか。


ドラムロールの音をください、答えです。「React.createElementは標準HTMLに限定されない」。オリジナルのコンポーネントを作ることもできるのです。

どうやって? React.createClassを使って。

var ContactItem = React.createClass({
  propTypes: {
    name: React.PropTypes.string.isRequired,
  },

  render: function() {
    return (
      React.createElement('li', {className: 'Contact'},
        React.createElement('h2', {className: 'Contact-name'}, this.props.name)
      )
    )
  },
});

この小さなスニペットはContactItemと呼ばれる新規コンポーネントを定義します。HTML要素と同様に、ContactItemは属性のリスト(propsと呼びます)を受け入れます。HTML要素と異なるのは、どんなpropsでも指定できることです。

上述のコンポーネントは次のように使えます。

var element = React.createElement(ContactItem, {name: "James K Nelson"})

そして予想どおり、elementにはコンポーネントのrender関数が返す値が含まれます。

children引数をcreateElementに渡す場合、その値はthis.props.childrenの下で取得できます。

次の練習で理解度をテストしてみましょう。

練習 2 コンタクトリストのリファクタリング

学習したことを使って、練習1で書いた答え、特に次の点をリファクタリングしましょう。

  • 3つのプロパティ、nameemaildescriptionを受け入れるContactItemクラスを作る。
  • e-mailアドレスを持たないコンタクトを選別する。
  • データをcreateElementに直接渡す代わりに、配列に格納する。

次のデータを使ってください。

var contacts = [
    {key: 1, name: "James K Nelson", email: "james@jamesknelson.com", description: "Front-end Unicorn"},
    {key: 2, name: "Jim", email: "jim@example.com"},
    {key: 3, name: "Joe"},
]

終わったら、次のfiddleと照らし合わせてみてください。

propTypes

練習 2の私の答えでは、propTypesオブジェクトに2種類の値、isRequiredが付いている値と付いていない値を使っていることに気づいたでしょうか。このサフィックスは、指定されたプロパティが必ずReact.createElementに渡されなければならないことを示しており、Reactの全てのprop typesに適用できます。

しかし、ちょっと待ってください。propTypesオブジェクトは実際のところ何をしているのでしょうか。

ほとんどの場合、 propTypes何もしていないというのが答えです。事実、完全になくしてもアプリは正確に機能します。ではなぜそこにあるのでしょう。

propTypesはデバッグを行うメカニズムなのです。これによってReactはcreateElementに渡されたprops引数が有効かどうかをリアルタイムでチェックします。正しくない場合はReactはコンソールに警告を出します。

React console warning

この手引きで propTypesを紹介するのは、それが必須事項ではなくとも、長い目で見ると多くの無駄を防ぐことになるからです。そして私のニュースレターに登録するとダウンロードできる、Cheat Sheet(PDF)を印刷しておけば、より労力を軽減できます。話を元に戻します。ここまででデータを表示させる方法が分かりました。次に更新の仕方を学びましょう。

Get the PDF cheatsheet!




新規データを表示させる

現状、私たちのアプリは静的データ一式を取り扱い、1度だけDOMへレンダリングします。しかし、データに変化がある場合には何が起こるのでしょうか。

Reactと呼ばれるツールにどのような期待をしようと、何も変わりません。他の多くのJavaScriptフレームワークの自動更新ビューとは異なり、Reactは手動でレンダリングの命令を出さなければなりません。その方法を説明します。

// Render our app to the DOM element with id `react-app`
var rootElement = React.createElement(ApplicationComponent, initialData)
ReactDOM.render(rootElement, document.getElementById('react-app'))

// Actually, there is no re-render. Just render.
var nextRootElement = React.createElement(ApplicationComponent, updatedData)
ReactDOM.render(nextRootElement, document.getElementById('react-app'))

最初のrender関数の呼び出しによって、全く新しいHTML要素のツリーが#react-app以下に作成されますが、2回目の呼び出しは変更が生じたところのみの変更しか行いません。これがパフォーマンスにおいてReactが好評価を得ている理由です。実際に次のことをしてみます。

var rootElement = React.createElement(ApplicationComponent, data)
ReactDOM.render(rootElement, document.getElementById('react-app'))
ReactDOM.render(rootElement, document.getElementById('react-app'))

2回目のrender関数の呼び出しは全く何もしません。特別なkeyプロパティをそれぞれの要素の配列に指定してレンダリングしない限りは何もしません。なぜこうなのか理解するにはrenderがどのように動くか少し知る必要があります。

ReactDOM.renderの仕組み

ReactDOM.renderの最初の呼び出しは簡単です。ReactElement内で渡され、対応のHTMLツリーを#react-appの下に作成します。しかし上でも述べたように2回目以降の呼び出しでは、それ以前にレンダリングされたHTML要素は、対応するReactElementオブジェクトに変更がない限り、更新されません。これはなぜなのでしょうか。

を変更するのかを決定する時、Reactは新しいReactElementツリーと前のツリーを比較します。その際に、いくつかのルールに従います。

  • 異なる型を持つReactElementは削除され、再度レンダリングされます。
  • 異なるプロパティあるいは子を持つReactElementはその場で再度レンダリングされます。
  • 配列順番が変更された同一のReactElementは、新しい配列どおりに変更されます。

ただし、ここで注意すべき点が1つあります。Reactが同一のtypepropを持つReactElementの配列を見つけると、見た目は同一のようでも、本当に同一であるかは知ることはできません。例えば要素がuser focusプロパティを持っているか否かの場合のように、要素が内部状態を持つことができるからです。しかし、それぞれの要素の区別がつかなくなってしまうため、Reactがこれら要素を再度レンダリングすると問題が発生します。その結果、配列の順番が変更になったのか分からなくなってしまうのです。

前述の例のkeyプロパティはここで使用することになります。keyプロパティはReactが要素を区別できるようにし、DOMとReactElementのツリーを整列しておきます。

理論はこのくらいにして、実用的なことに移りましょう。

フォーム

アプリにフォームを追加できるだけの知識が備わりました。ユーザインタラクションがなくてもいいのであれば、の話です。しかし、なぜこのような制約があるのでしょう。

Reactでは、input要素は特別のことではありません。valueプロパティを取り、そのvalueが何であるかを表示します。コンポーネントが外部から渡されたプロパティを直接変更できないため、ユーザ入力によって値の表示を変更してしまうことはありません。

少しこれを考えてみましょう。value属性をフォームに入力しても、何も変わりません。ただし、値を変えられないわけではありません。inputイベントをリッスンして値を必要に応じて更新すれば好きな値に変更することができます。これについては後ほど触れます。

value以外にもinput要素に持たせたいプロパティを持たせることが可能です。しかし、ちょっとした例外がいくつかあります。HTML <input>に渡したい属性もpropsで提供されています。次の2つの例外は知っておくべきでしょう。

  • Reactのtextarea要素はchildrenとしてではなく、propsとしてコンテンツを持ちます。
  • HTMLのfor属性はhtmlForにバインドされています。(JavaScriptでは、forは予約語です)。

私の作成したReact Cheat Sheetに上の例外は記載されています。私のニュースレターに登録した人にお配りしています。

練習 3 読み取り専用フォームの作成

では、次のpropTypesを持つ、新しいContactFormのコンポーネントクラスを作成します。

propTypes: {
  contact: React.PropTypes.object.isRequired
},

<form>要素をレンダリングすると、nameemailが入力できる<input>要素が表示され、descriptionが入力できる<textarea>が表示され、送信のための<button type="submit">が表示されるはずです。複雑化を避けるためにもラベルの代わりにプレースホルダを使うといいでしょう。そして、Reactのtextareaコンポーネントがvaluechildrenとしてではなくpropsとして受け取ることを覚えておきましょう。

ContactFormクラスを作成した後、contactプロパティとして渡される空白のコンタクトをコンタクトリストの下に追加してください。


できましたね。すばらしい。私の実装と比べてみてください。違いを見つけてほしいとは思いますが、私の実装に全ての行が一致する必要はないので、一致させることに固執しないでください。重要なのは動くものを書くことです。

アプリを美しくする

まだ記事を読み終わっていないのに、すでに売る気満々です。しかし、アプリの見た目が悪かったら誰も見向きもしません。そこで、ちょっとしたスタイルを追加しましょう。

どうすればいいのでしょう。このアプリは小さいので、HTMLセレクタを使用してスタイルを付けることできます。

ul {
  background: #ff0000;
}

しかし、別のリストを作ってしまうとアプリが動かなくなってしまいまので、後の苦労を避けるためにもクラスを指定しましょう。競合を避けるため、親コンポーネントのクラス名を名前空間に定義します。

ここでは、ContactItemコンポーネントは次のようになります。

render: function() {
  return (
    React.createElement('li', {className: 'ContactItem'},
      React.createElement('h2', {className: 'ContactItem-name'}, this.props.name),
      React.createElement('a', {className: 'ContactItem-email', href: 'mailto:'+this.props.email}, this.props.email),
      React.createElement('div', {className: 'ContactItem-description'}, this.props.description)
    )
  )
},

注:DOMと同じようにReactもclassNameプロパティを使用してCSSクラスを指定します(JavaScriptではclassは予約語です)。

コンポーネントのrender関数の一部ではないcreateElement呼び出しでクラス名を取得するにはどうすればいいのでしょうか。

ほとんどのReact系のプロジェクトでは、全てがクラスによって組織化されています。1つのReact.createElementを呼び出し、直接ReactDOM.renderに渡されるトップレベルの要素をレンダリングします。このアプリでは、トップレベルのContactViewを次のシグネチャを使用しましょう。

propTypes: {
  contacts: React.PropTypes.array.isRequired,
  newContact: React.PropTypes.object.isRequired,
},

練習4 おまけ

新規アプリにスタイルを付けたい場合、ContactViewを上で定義したとおりに作成して、classNameプロパティを必要に応じて追加して、さらにHTMLファイルにスタイルシートを追加します。

どのようにすればいいかヒントをあげましょう。fiddleを用意してみました。

誰も読み取り専用のアプリを好まない

見た目の良いアプリを作成するために知識は揃いました。しかし、インタラクティブにする方法はどのようにして学べばよいのでしょうか。

この記事は生のReactシリーズの第一弾です。毎週新しい記事を掲載予定です。Reactで本物のアプリを実際に読者が作成するのに十分な情報を出せるまで書き続けます。本物のアプリの定義は次のとおりです。

  • フォームイベントを処理できる。
  • ナビゲーションやルーティングを処理できる。
  • APIと通信できる
  • ユーザ認証の管理ができる。

最新情報Part 2: Ridiculously Simple Formsを掲載しました。

emailアドレスをくれた方には、新しい記事を発表のタイミングでお送りします。さらに特典としてReactES6JavaScriptのPromiseの3つの印刷用PDF Cheat Sheetを差し上げます。

I will send you useful articles, cheatsheets and code.

I won’t send you useless inbox filler. No spam, ever.






Unsubscribe at any time.

注釈:役に立つ記事やCheat Sheet、コードをお送りします。決して大量の無駄な情報を送るようなことはしません。絶対に迷惑メールは送りません。

感想をぜひ教えてください。@james_k_nelsonにツイートするか、 james@jamesknelson.comにメールしてください。読んでくださってありがとうございます。

さらに読む:

関連リンク: