社内で利用した、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版という認識で問題ありません。
yarn
はnpm
と似たパッケージマネージャーですが、インストールするパッケージをキャッシュすることによりnpm
よりも高速に動作してくれるパッケージマネージャです。
詳しくは、yarn
のホームページに詳しく書かれています。
さて、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
のようなものです。Gemfile
はbundle install
を実行するとGemfile.lock
というファイルが作成され、これによりバージョンが固定されます。Gemfile.lock
を他の環境に持っていくことで、他の環境でも全く同じ環境を作成することが出来ます。同等の機能がnpm
にもあり、yarn.lock
というファルがその役割を果たします。
.babelrc
.babelrc
はBabel
というトランスパイラの設定ファイルです。JavaScriptはその言語の成長により、とても早いサイクルで言語仕様が追加されています。InternetExplorer、Safari、ChromeやFirefoxといったブラウザがこのJavaScriptを解釈するのですが、JavaScriptの新しい言語仕様への追いつき方がブラウザ毎に異なっていたり、そもそもブラウザの利用者がブラウザを最新のものにすぐにアップデートしてくれるとも限りません。このため、ブラウザが解釈できるJavaScirptに新しい言語仕様で書かれたコードを変換するのがBabel
というトランスパイラの役割です。
.babelrc
の中身を見てみると、どの程度のブラウザをサポートするのかという記述を発見できます。
{
"presets": [
["env", {
"modules": false,
"targets": {
"browsers": "> 1%",
...
> 1%
と書かれているのは、1%以上のシェアのあるブラウザが解釈できるJavaScriptにトランスパイルしますという意味です。つまり、この設定にした場合、あなたのRailsで作成したアプリケーションの利用者の内の1%は、あなたのアプリケーションを正常に利用できない可能性があるということを覚えておいてください。他にも、last 2 versions
やnot ie <= 8
などの書き方、それらを同時に指定するといったこともできるので、色々と設定しどのようにトランスパイル後のコードが変化するのかを試してみてください。
app/javascript/*
app/javascript/
以下にpacks
というディレクトリがあり、その中にapplication.js
というファイルがあると思います。これが、HTML(erb)から読み込まれるファイルになります。
bin/*
bin/webpack
やbin/webpack-dev-server
は、webpacker
をインストールした際に作成されたコマンドで、それぞれコンパイルやコンパイル用サーバを立ち上げるコマンドになります。
config/*
config/webpack/*
やconfig/webpacker.yml
はbin/webpack
などのコマンドを実行した際に利用される設定ファイルになります。基本的な設定はconfig/webpacker.yml
に書いてあり、環境毎に異なる設定はconfig/webpack/
以下のファイルに書きます。
15.1.4 webpackの利用
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
にコードを追加します。
サーバを再起動して、ブラウザで実際にページにアクセスして見ましょう。
デベロッパーツールなどを開いて、コンソールを立ち上げると、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.yml
のdev_server
の項目を書き換えましょう。
これで、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
にコードを追加します。
ブラウザをリロードしてみましょう。画面の左下にHello React!
が表示されているはずです。
さて、このHello React!
を出力しているapp/javascript/packs/hello_react.jsx
を見ていきたいと思います。
import React from 'react'
import ReactDOM from 'react-dom'
import PropTypes from 'prop-types'
これらは、React
やReactDOM
といったモジュールをインポートしています。この様に記述することで、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.name
はstring
型でなくてはいけません。また、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
を以下のように変更します。
その後、おなじみの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コンポーネントをエクスポートしています。
app/javascript/packs/application.js
に追記されたreact-rails
の設定で、コンポーネントを明示的にimport
する必要はありません。react-rails
が用意したヘルパメソッドに、app/javascripts/components
以下に置かれたコンポーネントのファイル名を指定してあげることでインポートできます。
app/views/layouts/application.html.erb
のコードを、react-rails
のヘルパメソッドを利用するように変更しましょう。
これで好きな場所にReactコンポーネントを表示できるようになりました。
15.3 カウンターコンポーネントを作る
15.3.1 従来の実装
Reactを利用せずに普通に実装するなら、まずHTMLにカウント値を表示できるようにして、押すとカウントアップするボタンと、押すとカウントダウンするボタンの2つを表示できるようにします。とりあえず、app/views/layouts/application.html.erb
にコードを追加してみましょう。
数字の0とボタンが2つ表示されるようにできました。次に、ボタンを押すと数字が変化するようにしましょう。ボタンのクリックイベントで数字が変化するようにします。
さて、この程度の簡単なプログラムであればいいのですが、例えば3の倍数のときにはfizz
と、5の倍数のときにはbuzz
と、3と5の倍数の場合はfizzbuzz
と表示したいとなったらどうしたらいいでしょうか。(やってみてください。)
また、このようにHTMLのいろいろな要素にイベントを生やす際はDOMにid属性を付与して回る必要が出てきますが、重複しないようにするために何らかの工夫が必要そうです。
15.3.2 Reactコンポーネントで作成する
カウンターコンポーネントを作りましょう。
イベントを生やします。
ID指定などせずにイベントを直接生やせるので便利ですね!
初期値が0ですが、10から始めたりできるように、初期値を外から渡しましょう。
カウンターが10から始まっていると思います。
ボタンをクリックしたりして、正しく動いているか確認してみましょう。
ここまでできたら、いよいよ実践です。
app/views/layouts/application.html.erb
に追加したコードの幾つかは一旦削除しましょう。
15.4 フォローボタンのコンポーネントを作る
15.4.1 フォローボタンのコンポーネント雛形作成
はじめは、特に機能を入れ込まず、外枠だけ作っていきます。
フォローボタンのコンポーネントには、誰をフォローするのか(あるいはしているのか)という情報と、もしもフォローしている場合はそのリレーション情報を渡します。コンポーネントのファイル名をfollow_button.jsx
にするとして、呼び出しは次のとおりになります。
フォローボタンは、フォローの状態によってボタンのテキストの内容が変わるようにします。渡されるプロパティは、relationship
が存在しない場合がnull
になるということを考慮して、プロパティに渡されたrelationship
がnull
の場合にはフォローしていないということにします。
このボタンを押すとフォロー状態が変化します。フォローしているかどうかをrelationship
の値がnull
かどうかで判断すると書きましたが、props
の値は変化させられません。コンポーネント内で変化するものはカウンターコンポーネントのカウント値同様state
で管理しましょう。
さて、次にクリック時のイベント定義していきましょう。こちらも、フォローしているかどうかで挙動が変わるようにします。
15.4.2 コンポーネントから通信する
フォローボタンが押されたらフォローを、アンフォローボタンが押されたらフォローの解除をサーバにリクエストします。
Railsチュートリアルではこれをフォームのサブミットで行って(さらにJavaScriptを利用してDOMの中身を書き換えて)いましたが、Reactはレスポンスを利用して自信のstate
を書き換えます。state
が書き換わることによってrender()
で表示される内容が変化します。
さっそくフォローするコードを書いていきましょう。
通信には、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でのリクエストに対応させましょう。
これで、JSONでリクエストした場合フォロー情報を作成し、作成したフォロー情報をレスポンスとしてJSONで返してくれます。
レスポンスのJSONは自動的にJavaScriptのオブジェクトとなり、then
で取得できます。
取得したrelationship
で、state
を書き換えます。
.then((response) => {
const relationship = response
this.setState({
relationship
})
})
同様にフォロー解除もできるようにしましょう。
15.4.3 チャタリングを防止する
フォローとフォローの解除ができましたが、ボタンを連打すると2回フォローリクエストやフォロー解除リクエストが走ってしまいます。リクエスト中は、ボタンの操作を行えないように、stateにリクエスト中のフラグを用意しましょう。
15.4.4 ボタンの見た目を良くする
ボタンの見た目ですが、無機質で味気ないですね。
HTMLの属性にclassを追加して見栄えを良くしたいですが、状態によってクラスを変えようとすると次のようなコードになりがちです。
let className = 'btn'
if (isFollowing) {
className += ' btn-danger'
}
else {
className += ' btn-primary'
}
あまり可読性が良くない上に、状態によるclassNameの出し分けが増えた場合、正しいクラス名を付与するのが至難の技となります。classnames
というモジュールを導入して、状態によるクラス名の変更を簡単にしましょう。npm
ではなくyarn
で追加です。
$ yarn add classnames
これでモジュールを利用できるようになりました。コードを以下のように変更します。
あとは、以前のフォローボタンを削除すれば完成です!
見た目も良いフォロー/フォロー解除ボタンが完成しました。
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
を以下の内容で作成してください。
ただ空の<div />
を表示するだけのコンポーネントになっています。
このコンポーネントをreact-rails
で読み込めるようにします。react-rails
はapp/javascripts/components
以下に配置されたファイル名以外に、window.*
に公開されているコンポーネントも読み込んでくれます。先程作成したコンポーネントをwindows.Counter
で読めるようにしましょう。
app/javascript/packs/application.js
を編集します。
さて、これで<%= react_componennt('Counter') %>
とRailsのViewで読み込めるようになりました。
app/views/layouts/application.html.erb
で読み込みましょう。
最後に、プロジェクトでReduxを利用できるようにしましょう。redux
とreact-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ではAction
のtype
はReduxアプリケーションの中で一意である必要があります。重複などを避けるために、定数として管理しましょう。
この定数を利用して、Action
を定義していきます。
ActionCreator
Action
はActionCreator
によって作成されます。
ActionCreator
は関数の形をとり、実行されるとAction
を返します。
Reducer
ActionCreator
が作成したAction
はReducer
に伝わり、Reducer
はAction
を受け取り、その内容を元にStore
を更新します。カウンターでは、カウントアップのAction
を受けっ取ったらStore
にあるカウント値をインクリメントし、カウントダウンのAction
を受け取ったらStore
にあるカウント値をデクリメントします。
Reducer
はStore
の初期値も決定できます。Reduxの初期化の際に特別なAction
が発行され、それがReducer
に伝わることでstate
のデフォルト値がStore
に入ります。
Reducer
は往々にして肥大化することが多く、予め複数のファイルに分けておくことが推奨されます。
複数に分割されたReducer
をまとめるcombineReducer
というメソッドがredux
から提供されています。
Store
Store
はステートを管理する以外に特に説明することはありません。
redux
はミドルウェアと呼ばれる幾つかの拡張が用意されていて、Store
の中身を見ることのできるミドルウェアも存在します。そういったミドルウェアを利用する場合はこのファイルに幾つか記載します。
さて、Store
まで完成したので、コンポーネントとのつなぎこみを行って行きましょう。
ContainerComponent
ContainerComponent
はReactコンポーネントに、Store
のstate
やActionCreator
2を渡す役割を担います。
state
やActionCreator
はコンポーネントにprops
として渡ります。コンポーネントにどのstate
やActionCreator
をどういった名前で渡してやるのかというマッピングを行うのがContainerComponent
の役割になります。
mapStateToProps
に渡ってくるstate
はreducer
毎に別れています。今回はcount
というreducer
を作りましたので、state.count
のcount
がコンポーネントに渡すカウント値になります。
mapDispatchToProps
では、コンポーネントに渡す関数を用意します。コンポーネントが渡された関数を実行すると、対応するActionCreator
が実行され、Action
が発行されます。
Component
コンポーネントが自身の状態をstate
に持つ時代は終わりを迎えました。
今ではコンポーネントは、外部から渡されるprops
によってコンポーネントのrender()
する結果が決定されるようになりました。
15.5.4 Storeとコンポーネントを接続する
実はあと数ステップReduxを動かすために必要な動作が残っています。
ContainerComponent
がComponent
を参照したので、あとはContainerComponent
にStore
を渡せばいいのでは。そう思われる人も多いと思います。実は、Store
を渡すのはContainerComponent
ではなく普通のComponent
がいいとされているため、今度はContainerComponent
を呼び出すComponent
を作成します。
このコンポーネントの名前はどのように名付けても良いのですが、ここではApp
としています。
import Counter from '../containers/counter'
でContainerComponent
をインポートしていることに注意してください。
これであとは、Store
をこのコンポーネントに渡せばカウンターが動作するようになります。一番はじめに作成したapp/javascript/counter/index.js
がこのコンポーネントを読み込むようにし、さらにはStore
が渡されるようにしましょう。
react-redux
がProvider
というコンポーネントを用意してくれており、このコンポーネントの中にApp
コンポーネントを記述します。Provider
はstore
を属性値として受け取ることができ、受け取ったstore
はProvider
の中に存在するコンポーネントが読み込んでいるContainerComponent
に渡されます。
トランスパイルが成功していることを確認しブラウザをリロードすると、カウンターが表示されるようになりました。
15.6 最後に
15.6.1 本章のまとめ
-
webpacker
gemを利用することで、簡単にReactを利用した開発を行える - JavaScriptは進化し続けていて、最新の記法で記述したJavaScriptをブラウザが理解できる形式に変換するためにトランスパイラを利用する
― JavaScriptの最新の記法は謎めいているが、慣れると非常に協力である - Reactを利用すると、HTMLとJSをコンポーネントという単位でまとめるためて記述することができる
- Reactは渡される
props
の変化や内部のstate
が更新される度にrender
を呼び出し、props
やstate
に応じた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で書き換えてみましょう。