Help us understand the problem. What is going on with this article?

Railsチュートリアル幻の15章 React編

More than 1 year has passed since last update.

社内で利用した、Reactの研修資料を公開します。
Railsチュートリアルの続編として作成したので、Rails上でReactを使うためには...ということが書かれています。Railsチュートリアルテイストになっていますが、一部社内でのコーディング規約や利用するライブラリについての言及があります。

また、研修ということで、ソースコードはGitHubの差分をスクショするという形で掲載しています。コピペできるコードが欲しいという人は、差分表示を作るのに使ったリポジトリを利用して下さい。
https://github.com/kouheiszk/sample_apps/tree/master/5_1_2/ch15

第15章 Reactの導入

この章では、サンプルアプリケーションで作ってきたUIパーツをReactというJavaScriptのライブラリを利用してコンポーネント化していきます。具体的には、ユーザのフォロー(及びフォロー解除)できる仕組みを、Reactで置き換えます。まずReactの学習を始める前に、webpackというJavaScriptをビルドする新しい仕組みを導入します(15.1)。webpackの導入が終わったらReactをインストールし、Reactを利用できる環境を作成します(15.2)。その後、Reactとは何かを学ぶために、まず数字を押すとボタンがカウントアップするような簡単なコンポーネントを作り、Reactのコードの記述方法とその動作原理を学びます(15.3)。その後、フォローボタンをReactコンポーネントに置き換え、コンポーネント化とその方法を学びます。ここではAjax通信もReactコンポーネント内で行うようにし、フォロー(及びフォロー解除)を行える完璧なコンポーネントを作成していきます(15.4)。

この章では、本書(Railsチュートリアル)の中で最も幅広い知識が要求されます。コントローラの記述方法やルーティングの指定方法が不安な人は、以前の章をよく復習してから始めましょう。
Railsも5.1からReactを公式にサポートしましたし(4系でも利用することはできます)、JavaScriptでコンポーネントを記述するスタイルは今後のWebフロントエンド開発のスタンダードになっていくと思われます。個人の趣味で作成するような小規模開発でも、大規模な開発でも必ず役に立ちます。本章の最後では、Reactと同時に使われることがあるReduxというフレームワークについて簡単に説明します(15.5)。

15.0 開発環境と構築済みのアプリケーション

15.0.1 開発環境

Railsチュートリアルの1章に記載のある開発環境を利用します。
https://railstutorial.jp/chapters/beginning?version=5.1#sec-development_environment
Cloud9のRails TutorialのテンプレートではRubyだけではなく、これから説明するNode.jsも利用することができます。

15.0.2 アプリケーションの取得

偉大なる先輩が、Railsチュートリアルを14章まですすめてくれています。
本チュートリアルからRailsチュートリアルをReactを学ぶために開始する人は、この14章までチュートリアルが完了しているアプリケーションを利用しましょう。

$ pwd
/home/ubuntu/workspace
$ git clone https://github.com/yasslab/sample_apps # リポジトリを取得する
$ cd sample_apps/5_1_2                             # Rails5.1.2のチュートリアルに移動
$ cp -r ch1{4,5}                                   # 14章をコピーして15章に
$ cd ch15                                          # コピーした15章のディレクトリに
$ pwd
/home/ubuntu/workspace/sample_apps/5_1_2/ch15

15.0.3 アプリケーションの動作確認

アプリケーションを起動しましょう。
必要な依存パッケージをインストールします。

$ gem install bundler # 必要なら行う
$ bundle install

必要であれば、データベースを作成し初期データも投入しましょう。

$ rails db:migrate
$ rails db:seed

以上で起動の準備が整いました。念のため、テストが現状で全て成功することも確認しておきましょう。

$ rails test

テストが全て成功することが確認できたら、サーバを立ち上げて、ブラウザからアクセスしてみます。

$ rails server -b $IP -p $PORT

起動後の画面は下図の通りになるはずです。これでReactを学習する準備が整いました。

起動後の画面

15.1 webpackの導入

この章ではwebpackを導入していきます。
webpackとは、複数のJavaScriptのファイルを1つにまとめてくれるモジュールバンドラです。webpackを利用しないRailsプロジェクトの場合、この機能をAsset Pipelineが担っています。ではAsset Pipelineでも良いのではと思われると思いますが、webpackを利用すると他にも色々な機能が利用できます。例えばRailsだと、JavaScriptは素のJavaScriptで記述される以外に、CoffeeScriptやあるいはerbなどで記述されます。プロジェクトによってはTypeScriptやElmを使ったりもします。これらのJavaScript以外で書かれたコードを、ブラウザが解釈できるJavaScriptに変換してあげる必要があり、その役割もwebpackが担ってくれます。また、Reactなどをnodeモジュールとして管理したいという場合にも、webpackを利用することで簡単に管理できるようになります。

15.1.1 Webpacker gemの追加

まず、GemfileにWebpacker gemを追加します。これは、RailsのアセットパイプラインとNode.jsのwebpackをいい感じに連携してくれるgemです。

gem 'webpacker', '~> 3.0'

次に、いつも通りbundle installを実行します。

$ bundle install

15.1.2 webpackerのインストール

gemが追加されたら、railsコマンドでwebpackerのインストールタスクを実行し、webpackの設定ファイルなどを生成します。

$ rails webpacker:install

おっと、yarnがインストールされていないとエラーがでました。

$ rails webpacker:install
Yarn not installed. Please download and install Yarn from https://yarnpkg.com/lang/en/docs/install/

yarnを説明する前に、npmを説明しようと思います。
npmはNode.jsのパッケージマネージャです。先程webpackerを導入する際に利用したgemはRubyのパッケージマネージャですが、それのNode.js版という認識で問題ありません。
yarnnpmと似たパッケージマネージャーですが、インストールするパッケージをキャッシュすることによりnpmよりも高速に動作してくれるパッケージマネージャです。
詳しくは、yarnのホームページに詳しく書かれています。

https://yarnpkg.com/ja/

さて、yarnをインストールしましよう。Cloud9の環境ではすでにNode.jsが利用できるようになっており、それに伴いnpmも利用できます。
プロダクション環境でnpmを利用したyarnの導入は推奨されていませんが、今回はチュートリアルということで許容したいと思います。

$ npm install --global yarn
$ yarn --version

yarnのバージョンが表示されたら成功です。改めて、webpackerのインストールタスクを実行します。

$ rails webpacker:install

無事にタスクが終了し、設定ファイルなどの生成が成功したかと思います。

15.1.3 webpackerのインストールタスクで生成されたファイルの説明

gitで差分を確認してみると、生成されたファイルを一覧できます。

$ git status
On branch master
Your branch is up-to-date with 'origin/master'.

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

        modified:   .gitignore
        modified:   Gemfile
        modified:   Gemfile.lock
        modified:   package.json

Untracked files:
  (use "git add <file>..." to include in what will be committed)

        .babelrc
        .postcssrc.yml
        app/javascript/
        bin/webpack
        bin/webpack-dev-server
        config/webpack/
        config/webpacker.yml
        yarn.lock

no changes added to commit (use "git add" and/or "git commit -a")

簡単に幾つかのファイルの説明をしたいと思います。

package.json

package.jsonは、npmのパッケージやそのバージョンが書かれたファイルです。gemでいうGemfileのようなものです。Gemfilebundle installを実行するとGemfile.lockというファイルが作成され、これによりバージョンが固定されます。Gemfile.lockを他の環境に持っていくことで、他の環境でも全く同じ環境を作成することが出来ます。同等の機能がnpmにもあり、yarn.lockというファルがその役割を果たします。

.babelrc

.babelrcBabelというトランスパイラの設定ファイルです。JavaScriptはその言語の成長により、とても早いサイクルで言語仕様が追加されています。InternetExplorer、Safari、ChromeやFirefoxといったブラウザがこのJavaScriptを解釈するのですが、JavaScriptの新しい言語仕様への追いつき方がブラウザ毎に異なっていたり、そもそもブラウザの利用者がブラウザを最新のものにすぐにアップデートしてくれるとも限りません。このため、ブラウザが解釈できるJavaScirptに新しい言語仕様で書かれたコードを変換するのがBabelというトランスパイラの役割です。
.babelrcの中身を見てみると、どの程度のブラウザをサポートするのかという記述を発見できます。


{
  "presets": [
    ["env", {
      "modules": false,
      "targets": {
        "browsers": "> 1%",
        ...

> 1%と書かれているのは、1%以上のシェアのあるブラウザが解釈できるJavaScriptにトランスパイルしますという意味です。つまり、この設定にした場合、あなたのRailsで作成したアプリケーションの利用者の内の1%は、あなたのアプリケーションを正常に利用できない可能性があるということを覚えておいてください。他にも、last 2 versionsnot ie <= 8などの書き方、それらを同時に指定するといったこともできるので、色々と設定しどのようにトランスパイル後のコードが変化するのかを試してみてください。

app/javascript/*

app/javascript/以下にpacksというディレクトリがあり、その中にapplication.jsというファイルがあると思います。これが、HTML(erb)から読み込まれるファイルになります。

bin/*

bin/webpackbin/webpack-dev-serverは、webpackerをインストールした際に作成されたコマンドで、それぞれコンパイルやコンパイル用サーバを立ち上げるコマンドになります。

config/*

config/webpack/*config/webpacker.ymlbin/webpackなどのコマンドを実行した際に利用される設定ファイルになります。基本的な設定はconfig/webpacker.ymlに書いてあり、環境毎に異なる設定はconfig/webpack/以下のファイルに書きます。

15.1.4 webpackの利用

app/javascript/packs/application.jsを見てみましょう。

app/javascript/packs/application.js
.
.
.
console.log('Hello World from Webpacker')

このファイルを読み込んでブラウザのコンソールにHello World from Webpackerという文字列を表示させてみましょう。
ただし、15.1.3で説明したように、このままのJavaScriptを利用することはできず、BabelでトランスパイルしたJavaScriptを読み込む必要があります。
このトランスパイルは、以下のコマンドで実行できます。

$ bin/webpack

これで生成されるファイルは、application-8f8bdd9f4ff51391ec46.jsの様なダイジェスト付きのファイル名になり、public/packs/以下に生成されます。この出力先のディレクトリはconfig/webpacker.ymlで指定されています。
同時に生成されるmanifest.jsonには、元のファイル名とダイジェスト付きのファイル名の対応が書かれています。

15.1.5 トランスパイルされたファイルの読み込み

トランスパイルが完了したら、HTML(erb)からファイルを読み込めるようになっています。
app/views/layouts/application.html.erbにコードを追加します。

diff1.png

サーバを再起動して、ブラウザで実際にページにアクセスして見ましょう。
デベロッパーツールなどを開いて、コンソールを立ち上げると、Hello World from Webpackerという文字列がコンソールに表示されています。

さて、webpackerのインストールタスクを実行した際に、bin/webpack-dev-serverというコマンドが追加されていました。
これは何なのでしょうか?実行してみましょう。

$ bin/webpack-dev-server
.
.
.
[13] ./node_modules/loglevel/lib/loglevel.js 7.86 kB {0} [built]
[14] (webpack)-dev-server/client/socket.js 1.05 kB {0} [built]
[15] ./node_modules/sockjs-client/dist/sockjs.js 181 kB {0} [built]
[16] (webpack)-dev-server/client/overlay.js 3.73 kB {0} [built]
[18] ./node_modules/html-entities/index.js 231 bytes {0} [built]
[21] (webpack)/hot nonrecursive ^\.\/log$ 170 bytes {0} [built]
[23] (webpack)/hot/emitter.js 75 bytes {0} [built]
[25] ./app/javascript/packs/application.js 515 bytes {0} [built]
  + 11 hidden modules
webpack: Compiled successfully.

コンパイルが成功したというメッセージが表示されましたが、コンソールが返ってきません。
Ctrl+Cで終了できます。

さて、もう一度bin/webpack-dev-serverを実行し、その状態でapp/javascript/packs/application.jsを編集して見てください。ファイルが変更が保存されたタイミングで、トランスパイルが実行されたと思います。
このwebpack-dev-serverを立ち上げておくことで、ファイルの変更の度にコンソールから$ bin/webpackを実行しなくても自動でトランスパイルが実行されるようになります。
webpack-dev-serverを立ち上げた状態で、再度Railsのサーバを再起動してください。
Railsの起動時にwebpack-dev-serverが立ち上がっている場合、Webpackerが自動でwebpack-dev-serverで立ち上がるサーバからアセットを取得するように切り替えてくれます。

ここで、コンソールを見るとエラーが出ていると思います。
Cloud9でwebpack-dev-serverを実行するための設定が必要でした。
config/webpacker.ymldev_serverの項目を書き換えましょう。

diff2.png

これで、webpack-dev-serverで自動的にコンパイルされたアセットを、ブラウザのリロードで読み込めるようになります。

[WDS] Disconnected!というエラーがコンソールに表示されると思います。5分間調べましたが解決できなかったので、無視してすすめます。コンソールに赤い文字が出てくることにより健康を害する方は、webpack-dev-serverの利用を控えて下さい。

15.2 Reactの導入

15.2.1 Reactのインストール

ここからReact.jsの学習に入っていきます。WebpackerのタスクにReactをインストールするタスクがあるので、これを実行します。

$ rails webpacker:install:react

これでReactが使えるようになりました。簡単ですね!
app/javascript/packs/hello_react.jsxが追加されています。この状態で、bin/webpackを実行しましょう。

$ bin/webpack

public/packs/hello_react-5612ffe4e734573493a4.jsが生成されました。

15.2.2 Reactコンポーネントの利用

作成されたReactコンポーネントのJavaScriptを読み込みます。
app/views/layouts/application.html.erbにコードを追加します。

03.png

ブラウザをリロードしてみましょう。画面の左下にHello React!が表示されているはずです。

Hello React!が表示された

さて、このHello React!を出力しているapp/javascript/packs/hello_react.jsxを見ていきたいと思います。

import React from 'react'
import ReactDOM from 'react-dom'
import PropTypes from 'prop-types'

これらは、ReactReactDOMといったモジュールをインポートしています。この様に記述することで、window.Reactのようにグローバルな名前空間を汚染することなく、モジュールを利用することができます。これらのモジュールがどの様に利用されるかを見ていきましょう。

const Hello = props => (
  <div>Hello {props.name}!</div>
)

これがコンポーネントになります。少し昔の書き方をすると以下のようになります。

var Hello = function(props) {
  return (
    <div>Hello {props.name}!</div>
  )
}

() => {}は新しいJavaScriptの記法で、function() {}の略です1。慣れていきましょう。
JavaScriptの中で普通にHTMLが書けているように見えますが、これはJSXという記法です。基本的にはHTMLのように書けますが、class="btn"className="btn"と書く必要があったりと微妙に異なります。

さらに言うと、実は上のコードはコンポーネントの省略した書き方で、冗長な書き方をすると次のようになります。

class Hello extends React.Component {
  render() {
    return <div>Hello {props.name}!</div>
  }
}

コード上にReactというモジュールが(インポートされていたのに)利用されていませんでしたが、それは実は省略されていたからでした。利用しないのでインポートしなくても問題無い場合もありますが、JSX記法を利用する場合は(ESLintなどを導入した時にエラーが出るので)、インポートしておきましょう。
React.Componentという記述が現れると、コンポーネントという感じが増しましたね。ただ、基本的にはこのようなシンプルなものは省略して書きましょう。

さて、省略した書き方でのfunctionが表すとおり、これはpropsというオブジェクトを受け取ってHTMLを返す関数になっています。Reactコンポーネントとは関数なのかと思う方もいるかもしれませんが、(機能的には)ほぼその認識で間違いありません。関数は引数に対する結果を返します。基本的には、引数が同じ場合は同じ結果が返されます。Reactコンポーネントは引数としてpropsというオブジェクトを受け取り、その内容が同じであれば同じHTMLを出力します。今回のHelloコンポーネントであれば、props.nameが同じである限り、返されるHTMLの中身は変わらないですよね。

このpropsですが、どういう値をコンポーネントの外から受け取るのかという指定ができます。

Hello.defaultProps = {
  name: 'David'
}

Hello.propTypes = {
  name: PropTypes.string
}

これは、Helloコンポーネントのpropsに値がどのような型で含まれるのかが定義されています。
上のように定義すると、props.namestring型でなくてはいけません。また、props.nameが指定されなかった場合、その値はDavidになります。
ではこのprops.nameはどこで指定するのでしょうか。その答えがコードの最後のブロックにあります。

document.addEventListener('DOMContentLoaded', () => {
  ReactDOM.render(
    <Hello name="React" />,
    document.body.appendChild(document.createElement('div')),
  )
})

3行目に<Hello name="React" />とかかれていますが、このようにコンポーネントの属性値としてpropsに値を渡します。
ReactDOM.renderは2つの引数をとり、1つ目の引数がコンポーネント、2つ目の引数がそのコンポーネントが表示されるDOM要素になります。Helloコンポーネントは、bodyタグの最後に追加された空のdivの中に表示されます。

Reactコンポーネントがrenderの結果を変化させるのは、propsが変わったタイミング以外ではstateと呼ばれる内部状態が変化したときとなります。これは、15.3.2のカウンターコンポーネントの作成のタイミングで説明したいと思います。

15.2.3 Reactコンポーネントを自由に表示する

さて、Helloコンポーネントはbodyタグ内の一番最後に表示されました。これを例えばロゴの後ろやボタンの前に表示するにはどうしたらいいでしょうか。もちろん位置指定の記述を書き換えればいいのですが、ちょうど良いセレクタがない場合はどうしたらいいでしょうか。セレクタがない場合は、空の<div/>を用意し、それにidを付与する必要があります。しかしJavaScriptとHTMLのIDやクラスが密に関係しあっていて大丈夫なのでしょうか。色々と問題がありそうに思えます。このような問題を、新たにreact-railsというgemを導入することで解決します。

Gemfileを以下のように変更します。

gem.png

その後、おなじみのbundle installを実行しましょう。

$ bundle install

react-railsがインストールできたら、各種ファイルの生成などを行いましょう。以下のコマンドを実行することで、依存するライブラリがインストールされたり、app/javascript/packs/application.jsに新しくreact-railsの設定が追記されたりします。

$ rails generate react:install

さて、react-railsですが利用するにあたり1つルールがあります。それがapp/javascripts/components以下にコンポーネントを設置しなくていはいけないというルールです。このルールに従っておくと、コンポーネントの読み込みをreact-railsが自動で行ってくれます。
Helloコンポーネントをapp/javascripts/components/hello_react.jsxに移動しましょう。また、ReactDOM.render()react-railsが実行してくれるのでコメントアウトして、export default Helloを追記しましょう。
このexport default Helloは、このJavaScriptファイルがインポートされたときに、デフォルトで何がエクスポートされるのかということを指定しています。hello_react.jsxをインポートした際は、Helloコンポーネントが返されることが期待されるため、export default HelloのようにHelloコンポーネントをエクスポートしています。

hello.png

app/javascript/packs/application.jsに追記されたreact-railsの設定で、コンポーネントを明示的にimportする必要はありません。react-railsが用意したヘルパメソッドに、app/javascripts/components以下に置かれたコンポーネントのファイル名を指定してあげることでインポートできます。
app/views/layouts/application.html.erbのコードを、react-railsのヘルパメソッドを利用するように変更しましょう。

app.png

これで好きな場所にReactコンポーネントを表示できるようになりました。

15.3 カウンターコンポーネントを作る

screencapture-rails-tutorial-kouheiszk-c9users-io-1511759097321.png

15.3.1 従来の実装

Reactを利用せずに普通に実装するなら、まずHTMLにカウント値を表示できるようにして、押すとカウントアップするボタンと、押すとカウントダウンするボタンの2つを表示できるようにします。とりあえず、app/views/layouts/application.html.erbにコードを追加してみましょう。

counter_app.png

数字の0とボタンが2つ表示されるようにできました。次に、ボタンを押すと数字が変化するようにしましょう。ボタンのクリックイベントで数字が変化するようにします。

counter_js.png

さて、この程度の簡単なプログラムであればいいのですが、例えば3の倍数のときにはfizzと、5の倍数のときにはbuzzと、3と5の倍数の場合はfizzbuzzと表示したいとなったらどうしたらいいでしょうか。(やってみてください。)
また、このようにHTMLのいろいろな要素にイベントを生やす際はDOMにid属性を付与して回る必要が出てきますが、重複しないようにするために何らかの工夫が必要そうです。

15.3.2 Reactコンポーネントで作成する

カウンターコンポーネントを作りましょう。

counter_component.png

counter_app.png

イベントを生やします。

hoge.png

ID指定などせずにイベントを直接生やせるので便利ですね!
初期値が0ですが、10から始めたりできるように、初期値を外から渡しましょう。

props.png

count.png

カウンターが10から始まっていると思います。
ボタンをクリックしたりして、正しく動いているか確認してみましょう。

ここまでできたら、いよいよ実践です。
app/views/layouts/application.html.erbに追加したコードの幾つかは一旦削除しましょう。

remove.png

15.4 フォローボタンのコンポーネントを作る

15.4.1 フォローボタンのコンポーネント雛形作成

はじめは、特に機能を入れ込まず、外枠だけ作っていきます。
フォローボタンのコンポーネントには、誰をフォローするのか(あるいはしているのか)という情報と、もしもフォローしている場合はそのリレーション情報を渡します。コンポーネントのファイル名をfollow_button.jsxにするとして、呼び出しは次のとおりになります。

asdf.png

フォローボタンは、フォローの状態によってボタンのテキストの内容が変わるようにします。渡されるプロパティは、relationshipが存在しない場合がnullになるということを考慮して、プロパティに渡されたrelationshipnullの場合にはフォローしていないということにします。

asdf.png

このボタンを押すとフォロー状態が変化します。フォローしているかどうかをrelationshipの値がnullかどうかで判断すると書きましたが、propsの値は変化させられません。コンポーネント内で変化するものはカウンターコンポーネントのカウント値同様stateで管理しましょう。

asdf.png

さて、次にクリック時のイベント定義していきましょう。こちらも、フォローしているかどうかで挙動が変わるようにします。

asdf.png

15.4.2 コンポーネントから通信する

フォローボタンが押されたらフォローを、アンフォローボタンが押されたらフォローの解除をサーバにリクエストします。
Railsチュートリアルではこれをフォームのサブミットで行って(さらにJavaScriptを利用してDOMの中身を書き換えて)いましたが、Reactはレスポンスを利用して自信のstateを書き換えます。stateが書き換わることによってrender()で表示される内容が変化します。
さっそくフォローするコードを書いていきましょう。

asdf.png

通信には、jQueryの$.ajaxを利用しました。各パラメータの意味は

$.ajax({
  type: 'POST', // HTTPのメソッド
  url: `/relationships`, // リクエスト先のURL
  dataType: 'json', // リクエストの種類
  contentType: 'application/json', // レスポンスの種類
  data: JSON.stringify({
    followed_id: this.props.user.id
  }) // 実際に送信するデータ
})

となっています。最後のbeforeSendですが、これはCSRFでないことをRailsに教えるための秘密の文字列をリクエストのヘッダに仕込むものになります。あまり覚える必要はありませんが無いと動かないので、これはRailsで必要なものだと思っていてください。

beforeSend: function(xhr) {
  xhr.setRequestHeader('X-CSRF-Token', $('meta[name="csrf-token"]').attr('content'))
}

さて、これでフォロー状態の切り替えのリクエストを送信できるようになりましたが、Rails側がJSONでのリクエストに対応していません。
JSONでのリクエストに対応させましょう。

asdf.png

これで、JSONでリクエストした場合フォロー情報を作成し、作成したフォロー情報をレスポンスとしてJSONで返してくれます。
レスポンスのJSONは自動的にJavaScriptのオブジェクトとなり、thenで取得できます。
取得したrelationshipで、stateを書き換えます。

.then((response) => {
  const relationship = response
  this.setState({
    relationship
  })
})

同様にフォロー解除もできるようにしましょう。

asdf.png

sadf.png

15.4.3 チャタリングを防止する

フォローとフォローの解除ができましたが、ボタンを連打すると2回フォローリクエストやフォロー解除リクエストが走ってしまいます。リクエスト中は、ボタンの操作を行えないように、stateにリクエスト中のフラグを用意しましょう。

asdf.png

15.4.4 ボタンの見た目を良くする

ボタンの見た目ですが、無機質で味気ないですね。

asfd.png

HTMLの属性にclassを追加して見栄えを良くしたいですが、状態によってクラスを変えようとすると次のようなコードになりがちです。

let className = 'btn'
if (isFollowing) {
  className += ' btn-danger'
}
else {
  className += ' btn-primary'
}

あまり可読性が良くない上に、状態によるclassNameの出し分けが増えた場合、正しいクラス名を付与するのが至難の技となります。classnamesというモジュールを導入して、状態によるクラス名の変更を簡単にしましょう。npmではなくyarnで追加です。

$ yarn add classnames

これでモジュールを利用できるようになりました。コードを以下のように変更します。

asfd.png

あとは、以前のフォローボタンを削除すれば完成です!

asdf.png

見た目も良いフォロー/フォロー解除ボタンが完成しました。

asdf.png

15.5 Reduxの紹介

15.5.1 Reduxとは

Reactはそれだけでも利用するメリットが大きいですが、Reduxと一緒に利用されることも多いと思います。
Reactは使えるが、その上でReduxを使うとなると一気にハードルが上がるという人も多いのでは無いでしょうか。ということで、このチュートリアルではReduxの使い方も併せて簡単に説明したいと思います。
Reduxは、Reactのコンポーネント内でのstateの管理を1つにまとめ、Fluxというデータフローの考え方でそのstateを管理するためのライブラリです。

詳しい説明は、公式が分かりやすいと思います。公式のBasicsを読むとReduxをおおまかに理解できると思います。
https://redux.js.org/docs/basics/

Reduxは以下に示す登場人物が多いことも理解が進みにくい原因の一つだと思います。ただし、良く見てみるとそれぞれの役割は単純で、またデータの流れる方向も以下のリストの順番通りです。これを踏まえて、Reactで作成したカウンターコンポーネントをReduxで書き直してみましょう。

登場人物 役割
Action アクション
ActionCreator アクションを発生させる
Reducer アクションを受け取って、Storeのステートを更新する
Store ステートを管理する
ContainerComponent Reactのコンポーネントに、StoreのステートやActionCreatorを渡す
Component Reactコンポーネント

15.5.2 準備

Reduxを始める前に、まずは空のコンポーネントを新しく作りましょう。ファイルの配置も少し変えapp/javascript/counter/index.jsを以下の内容で作成してください。

スクリーンショット 2017-11-28 1.24.52.png

ただ空の<div />を表示するだけのコンポーネントになっています。
このコンポーネントをreact-railsで読み込めるようにします。react-railsapp/javascripts/components以下に配置されたファイル名以外に、window.*に公開されているコンポーネントも読み込んでくれます。先程作成したコンポーネントをwindows.Counterで読めるようにしましょう。
app/javascript/packs/application.jsを編集します。

asdf.png

さて、これで<%= react_componennt('Counter') %>とRailsのViewで読み込めるようになりました。
app/views/layouts/application.html.erbで読み込みましょう。

asdf.png

最後に、プロジェクトでReduxを利用できるようにしましょう。reduxreact-reduxというNodeモジュールを追加します。

$ yarn add redux react-redux

以上で準備は完了です。早速Reduxでカウンターを作成していきましょう。

15.5.3 Reduxでカウンターを作成する

Actioin

ReduxはActionと呼ばれるデータをやり取りすることでstateを変化させます。
ActionはJavaScriptのオブジェクトで、以下のような構造です。

{
  type: "CLICK",
  button: "a-button"
}
{
  type: "SIGN_IN",
  user: {
    id: 12345,
    name: "David"
  }
}

Actionは必ずtypeを持っているほかは特に形式に決まりはありません。typeだけを持つActionでも大丈夫です。
カウンターアプリで考えられるアクションは、カウントアップとカウントダウンの2つです。このActionを作っていきましょう。
ここで、ReduxではActiontypeはReduxアプリケーションの中で一意である必要があります。重複などを避けるために、定数として管理しましょう。

asdf.png

この定数を利用して、Actionを定義していきます。

ActionCreator

ActionActionCreatorによって作成されます。
ActionCreatorは関数の形をとり、実行されるとActionを返します。

スクリーンショット 2017-11-28 11.03.12.png

Reducer

ActionCreatorが作成したActionReducerに伝わり、ReducerActionを受け取り、その内容を元にStoreを更新します。カウンターでは、カウントアップのActionを受けっ取ったらStoreにあるカウント値をインクリメントし、カウントダウンのActionを受け取ったらStoreにあるカウント値をデクリメントします。

スクリーンショット 2017-11-28 11.07.07.png

ReducerStoreの初期値も決定できます。Reduxの初期化の際に特別なActionが発行され、それがReducerに伝わることでstateのデフォルト値がStoreに入ります。

Reducerは往々にして肥大化することが多く、予め複数のファイルに分けておくことが推奨されます。
複数に分割されたReducerをまとめるcombineReducerというメソッドがreduxから提供されています。

スクリーンショット 2017-11-28 11.15.05.png

Store

Storeはステートを管理する以外に特に説明することはありません。
reduxはミドルウェアと呼ばれる幾つかの拡張が用意されていて、Storeの中身を見ることのできるミドルウェアも存在します。そういったミドルウェアを利用する場合はこのファイルに幾つか記載します。

スクリーンショット 2017-11-28 11.17.46.png

さて、Storeまで完成したので、コンポーネントとのつなぎこみを行って行きましょう。

ContainerComponent

ContainerComponentはReactコンポーネントに、StorestateActionCreator2を渡す役割を担います。
stateActionCreatorはコンポーネントにpropsとして渡ります。コンポーネントにどのstateActionCreatorをどういった名前で渡してやるのかというマッピングを行うのがContainerComponentの役割になります。

スクリーンショット 2017-11-28 11.27.06.png

mapStateToPropsに渡ってくるstatereducer毎に別れています。今回はcountというreducerを作りましたので、state.countcountがコンポーネントに渡すカウント値になります。

mapDispatchToPropsでは、コンポーネントに渡す関数を用意します。コンポーネントが渡された関数を実行すると、対応するActionCreatorが実行され、Actionが発行されます。

Component

コンポーネントが自身の状態をstateに持つ時代は終わりを迎えました。
今ではコンポーネントは、外部から渡されるpropsによってコンポーネントのrender()する結果が決定されるようになりました。

スクリーンショット 2017-11-28 11.33.44.png

15.5.4 Storeとコンポーネントを接続する

実はあと数ステップReduxを動かすために必要な動作が残っています。
ContainerComponentComponentを参照したので、あとはContainerComponentStoreを渡せばいいのでは。そう思われる人も多いと思います。実は、Storeを渡すのはContainerComponentではなく普通のComponentがいいとされているため、今度はContainerComponentを呼び出すComponentを作成します。
このコンポーネントの名前はどのように名付けても良いのですが、ここではAppとしています。

スクリーンショット 2017-11-28 11.38.40.png

import Counter from '../containers/counter'ContainerComponentをインポートしていることに注意してください。
これであとは、Storeをこのコンポーネントに渡せばカウンターが動作するようになります。一番はじめに作成したapp/javascript/counter/index.jsがこのコンポーネントを読み込むようにし、さらにはStoreが渡されるようにしましょう。

スクリーンショット 2017-11-28 11.41.41.png

react-reduxProviderというコンポーネントを用意してくれており、このコンポーネントの中にAppコンポーネントを記述します。Providerstoreを属性値として受け取ることができ、受け取ったstoreProviderの中に存在するコンポーネントが読み込んでいるContainerComponentに渡されます。

トランスパイルが成功していることを確認しブラウザをリロードすると、カウンターが表示されるようになりました。

screencapture-rails-tutorial-kouheiszk-c9users-io-1511837185412.png

15.6 最後に

15.6.1 本章のまとめ

  • webpackergemを利用することで、簡単にReactを利用した開発を行える
  • JavaScriptは進化し続けていて、最新の記法で記述したJavaScriptをブラウザが理解できる形式に変換するためにトランスパイラを利用する ― JavaScriptの最新の記法は謎めいているが、慣れると非常に協力である
  • Reactを利用すると、HTMLとJSをコンポーネントという単位でまとめるためて記述することができる
  • Reactは渡されるpropsの変化や内部のstateが更新される度にrenderを呼び出し、propsstateに応じたHTMLを返す
  • JavaScriptからAjax通信を行う場合は、CSRFトークンをリクエストに含める必要がある
  • ReactコンポーネントのHTMLのクラスを条件によって変えたい場合は、classnamesを利用する
  • Reduxは登場人物(ファイル)が多いだけで、各登場人物の役割は単純である
  • Reduxを利用するとstateがStoreで一元管理されるので、状態の見通しが良くなる
  • Reduxは必ず利用する必要はなく、コンポーネントが複雑なstateを持ち、それらが他のコンポーネントにも影響を与える場合に利用を検討すべき

演習

演習1

Babelでサポートするブラウザの指定を変更し、全てのブラウザの最新バージョン(IEはIE11とMS edge)をサポートするような指定にしてみましょう。また、指定の違いで出力がどう異なるかも可能であれば調べましょう。

演習2

コンポーネントを表示する際に渡すHashを用意する際に、user.attributesを利用するとHashで表現したくないプロパティもHashになってしまったと思います。
例えばユーザを表示するコンポーネントの場合、管理権限を持っているかどうか(管理権限を管理するカラムがユーザテーブルに存在する)ということが分かってしまいます。
パスワードの項目や管理者権限を持っているかどうかがuser.attributesすることで利用者に見える状態になってしまうのは良くないので、これを防ぐ方法を考えましょう。

また、例えばマイクロポストコンポーネントがあったときに、

render_component('micropost', micropost: micropost.attributes)

としたいですが、この中でmicropost.user.nameなどは取得できません。これを取得する方法も同時に考えましょう。

Hint: 例えばrepresenterというgemを利用します。

演習3

ユーザ情報を表示するコンポーネントを作成して、マイクロポストのユーザ名をマウスオーバーするとツールチップ形状のモーダルでそのコンポーネントが表示されるようにしてみてください。

Hint: https://github.com/react-bootstrap/react-overlays の利用を検討してみてください

演習4

マイクロポストの一覧を表示するコンポーネントを作成してください。
もちろん演習3の内容は引き継ぎます。

Hint: マイクロポストの削除は、コンポーネント内でリクエストするようにしましょう。ページ遷移が発生せずにコンポーネントが削除されるにはどうしたら良いか考えましょう。

演習5

マイクロポストをTwitterの様にモーダルで表示できるようにします。
マイクロポストコンポーネント(あるいはマイクロポストコンポーネント内に設置したボタン)をクリックすると、そのモーダルが表示されるようにしましょう。
モーダルが表示されたタイミングでURLを動的に書き換え、パラメータにマイクロポストを表すIDを含めます。
マイクロポストを表すIDが含まれたURLにアクセスすると、該当するツイートがモーダルで表示されるようにもしてみてください。

演習6

Reduxで作成したカウンターに初期値を外から与える方法を考えましょう。

演習7

演習4や演習5で作成したマイクロポストを表示するコンポーネントを、Reduxで書き換えてみましょう。


  1. 正確にはfunction() {}.bind(this)の略です 

  2. 正確にはActionCreatorのDispacherがコンポーネントに渡されます 

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away