React-Redux をわかりやすく解説しつつ実践的に一部実装してみる
全くわからない
こんにちは。
ここ1〜2週間くらい React と Redux 触ってるんですが、あまりにもまともなドキュメントがなく、どう動かして良いのかわからなかった状態でした。
しかし、色々仕組みを追っているうちに「こんな感じで書けばいいのでは」というのがわかってきたので僕の言葉&コードでまとめてみようと思います。
独自の解釈かも知れない部分があるので、変なところはコメントいただけると助かります。
まず React について
React についてはたくさん書かれてるのと、ふぅん〜くらいに流してもだいたいなんとかなるので端折ります。
上記の基本〜コンポーネントの使用くらいまで読んだらだいたいOKです。
重要なのは、
(MVCで言うところの)Viewのみを担当する。 JavaScriptのコード中に(PHPの様に)「HTMLタグ(っぽいもの)」を書ける。
ここらへん。
実践的に重要となるのは、render() 中の JSX の書き方くらいでしょうか。
結局僕は「Facebook製の優秀なテンプレートエンジン」くらいのレベルで解釈して後は調べるのをやめました。今度もうちょっと深追いします。
Redux について
React だけでも フロントエンド開発はできるっちゃできるんですが、フロントエンドでは結局状態管理が必要ですよね。
例えば、
- ページングがある一覧に対して、現在のページ数を保持したり、「次へ」を押して読み込んでる最中は読み込んでいるという状態を管理したり
- チェックボタンが3つあって、3つチェックされたら送信ボタンが ON になるための状態を管理したり
といった状態管理するためのフレームワークです。
もちろん、React と独自実装でも状態管理はできるけど、いい感じに状態管理周りをまとめてくれるフレームワークの一つが Redux です。
Redux のアーキテクチャ
Redux は 5つの要素からなります
- State
- 重要なやつ
- 状態を保持してるところ
- この State の状態を常に監視して、View 部分である React でレンダリングを行う
- Action
- type をキーとして、State を更新するための定義を書く
- 更新する時は dispatch 関数を実行することで状態更新する
- dispatch(definedFuncInActionCreator()) というような形
- しかし!!!更新したい状態の定義はここではしない、それは ActionCreatorでやる
- 関数のdispatchを行う場所、的なイメージ
- その他、ajax 通信するならここ
- ActionCreator
- Action では書かなかった、更新したい状態(新しいState)の定義を書く
- 直接 State をいじるのではなく、上書き更新したい部分だけ書く
- ファイル的には Action と同じファイル上で書いてるようだし、ほぼほぼセット
- テストしやすくしてるため分離してるみたい
- Store
- 一回書いたらほとんど変更しないところ
- State を使うよーとか、redux のミドルウェア(Logger とか Thunk とか)を定義するみたいな部分
- 状態管理部分の実装中はあんまり触らない
- Reducer
- state の状態を更新するためのロジックを書く部分
- 基本的に Switch 文中に Action で定義した State の type で分岐しロジックを書く
- return するものは必ず新しい Object というルールが有る(Object.assign({}, state, {変更したいプロパティ}) という関数をよく書く)
ばっと説明しましたが、たぶん上記を読んだだけではよくわからない
ので、いちから設計等を考慮して実装してみたいと思います
1. まず、作りたいものの検討
今回は以下の様なものにします
- 最初の段階では div 要素に「入力してください」と表示される
- input ボックスにデータを入力して submit ボタンを押したら、 input ボックスの値を取得し、特定の div 要素のテキストが更新される
- input ボックスに入力した文字数が表示される
- show ボタンを押すと div 要素の表示が表示される
- hide ボタンを押すと div 要素の表示が消える
ものを作ってみたいと思います。
簡単すぎず若干複雑さがあるものにしてみました。
2. State の設計
State は以下のような Object で考えます
{
text: "更新される内容",
number: 0,
flag: trueかfalse
}
状態管理したいのは div 要素のテキスト、文字数、表示するしないのフラグあたりですね。
この状態が変更されたときに「どう表示を変更するか」を別途実装します。
そして、状態が変更されたならば View 側に通知するために「store.subscribe() という関数を実行する」のですが、 React-Redux のライブラリの @connect 関数(後述)を使うと勝手に通知してくれるので、View への通知はあんまり気にしなくても大丈夫です。
3. Action の設計と実装
Action で State がどう変わるか設計します。
State が変更されるのはどういうときかを考えると、
- submit が押されたとき
- show が押されたとき
- hide が押されたとき
今回はこの 3パターンですね。
submit が押された時
{
type: "SUBMIT",
text: "inputから取得した文字列",
number: inputのvalueのlength
}
show が押された時
{
type: "SHOW",
flag: true
}
hide が押された時
{
type: "HIDE",
flag: false
}
上記のような更新を State にかけるといい感じに差分が変更されるでしょう。
更新したい部分のプロパティだけが入ったオブジェクトを定義します。
./frontend/javascripts/actions/Text.js
export function submitText(text) {
return {
type: "SUBMIT",
text: text,
number: text.length
};
}
export function showText(text) {
return {
type: "SHOW"
// flag は可変ではなく true 固定のため、 Reducer 側で書く
};
}
export function hideText(text) {
return {
type: "HIDE"
// flag は可変ではなく false 固定のため、 Reducer 側で書く
};
}
4. Component (というか React 部分の HTML)
簡単にベースの HTML を考えます。以下のようなHTMLで表示するとします。
<div>
<div class="(hide)">
文章文章
</div>
<div>
<span>100</span> 文字
</div>
<input type="text" ref="inputText">
<button>submit</button>
<button>show</button>
<button>hide</button>
</div>
ただし、 HTML のようなもの ( jsx ) であるだけで、
- jsx は HTML ではないため、class の設定が className になっていたりするので少し注意が必要なこと
- 既存の HTML のタグやテキスト以外にも、{this.hogehoge()} とかもできちゃう
- js のクラスも Render 内で解釈が可能 {HogeClass}
- import したライブラリとか読み込みたい場合に使ったりする
- 同一ファイル内でクラスを定義してあればそれでも良い
- 属性の書き方が以下のように少し違う(単独の文字列の時はダブルクオーテーションで良く、それ以外の時は { } で囲む)
- 文字列を設定する場合 class="moji"
- 数字を設定する場合 class={123}
- インスタンス等 class={Instance}
- 要素の結合がある場合 class={"btn " + (flag ? "hide" : "" )}
<p 要素名=値 要素名=値>hoge</p>というようにイコールの前後はスペースを空けない<input>等の単独の要素は/>で終わらせ、<input type="input" />といった形で閉じる
などなど、結構直感的にわかるかと思います。
5. Component のイベントの設計と実装
さらに Component でのイベントの登録(EventListener)も考えておきます。
DOM上に onClick={dispatch(hoge(flag))} みたいにも書けますが、見通しをよく・共通化するためにも function 化します
今回の関数は以下の通り
- submitButtonClicked()
- submit が押されたときの挙動を書く
- showButtonClicked()
- show が押されたときの挙動を書く
- hideButtonClicked()
- hide が押されたときの挙動を書く
僕の中でまだわかりきっていませんが、ActionCreator と Component の function での役割の差分は、
- Component のみで dispatch できるなら dispatch する
- Component では、dispatch 前にパラメータの取得、要素の設定など可変なものを準備する
- ActionCreator では動的な取得は必要以上に行わず、引数で済ます
といったところを考えています。ココらへん詳しい人教えてください。
今回の Component を具体化すると
./frontend/javascripts/components/Text.js
import React, { Component } from "react"
import { connect } from 'react-redux'
// 使うアクションを import する
import { submitText, showText, hideText } from "../actions/Text"
class Text extends Component {
render() {
// mapStateToProps で紐付けしていることに注意
const { flag, text, number } = this.props;
// ココらへんは Redux じゃなくて React (JSX) の書き方
return (
<div>
<div className={(flag ? "" : "hide")}>
{text}
</div>
<div>
<span>{number}</span> 文字
</div>
<input type="text" ref="inputText" />
<button onClick={e => this.submitButtonClicked(e)}>submit</button>
<button onClick={e => this.showButtonClicked(e)}>show</button>
<button onClick={e => this.hideButtonClicked(e)}>hide</button>
</div>
)
}
submitButtonClicked(e) {
// input の値を取得
// action 内ではできない事をここで処理する
const inputText = this.refs.inputText.getDOMNode().value;
dispatch(submitText(inputText));
}
showButtonClicked(e) {
dispatch(showText());
}
hideButtonClicked(e) {
dispatch(hideText());
}
}
function mapStateToProps(state) {
const { flag, text, number } = state;
return {
flag,
text,
number
}
}
// react-redux にある connect 関数を使うと state と Text の this.props をひも付けできる
// 前述したとおり、 store.subscribe() を呼ばなくても良くなる
export default connect(mapStateToProps)(Text)
上記のようになります。
6. Reducer の設計と実装
Reducer は先程出たように、State の更新処理を行う部分。
どういうパラメータの更新かは Action で定義されているが、具体的に State のどの部分の更新なのかをひも付けてあげる部分。
Index.js
まず、 ./frontend/javascripts/reducers/Index.js を作る
import {combineReducers} from "redux"
import text from "./Text"
const reducer = combineReducers({
text
});
export default reducer;
これは Reducers が肥大化するため、 reducers/Index.js では複数の Reducer を結合できる仕組みを作っている。
他にも処理したい Reducer が増える場合、
- import する
- combineReducersのオブジェクトに追記してあげる
import {combineReducers} from "redux"
import text from "./Text"
import form from "./Form"
const reducer = combineReducers({
text,
form
});
export default reducer;
といったように。(今回は上記は必要ないですが)
Text.js
さらに、 ./frontend/javascripts/reducers/Text.js を作ります。
ここが実際に今回の状態変更の処理をする部分になっています。
// 初期値の設定をしてあげる
const initialState = {
flag: true,
text: "入力してください",
number: 0
}
// action で受け取った値を state に適用して更新する
export default function text(state = initialState, action) {
const { flag, text, number } = action;
switch (action.type) {
case "SUBMIT":
// 今回ここでは状態の更新だけだが、action の値によってさらに別な値も変えたりするなど
return Object.assign({}, state, {
text: text,
number: number
});
case "SHOW":
return Object.assign({}, state, {
flag: true
});
case "HIDE":
return Object.assign({}, state, {
flag: false
});
default:
return state;
}
}
上記の状態の更新以外の部分では、例えば以下のような処理もできる
case "SUBMIT2":
let newFlag;
// 一例ですが、文字数によって他のパラメータも更新する、などの場合
if (number > 10) {
newFlag = true;
} else {
newFlag = false;
}
return Object.assign({}, state, {
text: text,
number: number,
flag: newFlag
});
7. Store の設定
つなぎこみの設定するだけです。
ここも色々な書き方ができるみたいですが、
- ミドルウェアの設定
- rootReducer (Index)の設定
をしてあげています。
./frontend/javascripts/store/ConfigureStore.js
import thunkMiddleware from 'redux-thunk';
import createLogger from 'redux-logger';
import { createStore, applyMiddleware } from 'redux';
import rootReducer from '../reducers/Index';
const loggerMiddleware = createLogger();
export default function createStoreWithMiddleware() {
const store = applyMiddleware(
thunkMiddleware, // lets us dispatch() functions
loggerMiddleware // neat middleware that logs actions
)(createStore);
return store(rootReducer);
}
一回書いたらそんなに変更することはないです。
redux-thunk というライブラリを使って複数の dispatch ができるようになる部分、 redux-logger というライブラリを使って開発中のデバッグができるようになるライブラリ(ミドルウェア)を組み込んで、 rootReducer である reducers/Index.js を読み込んでいる事が理解できるでしょうか。
上記の2つのミドルウェアは必須ではないので、自分の好みに合わせてつけてみるといいと思います。
8. エントリーポイントである index.js の作成
上記まででほぼほぼ完成です。
最後にエントリーポイントを作ります。
./frontend/javascripts/index.js
import React from "react"
import ReactDOM from "react-dom"
import {Provider} from "react-redux"
import createStoreWithMiddleware from "./store/ConfigureStore"
import Text from "./components/Text"
const store = createStoreWithMiddleware();
ReactDOM.render(
<Provider store={store}>
<Text />
</Provider>,
document.getElementById('content')
);
上記の import 文でわかるかと思いますが、 const store は 7. で前述した rootReducer の読み込みとミドルウェアの読み込み部分です。
9. トランスパイル
browserify 等を使って ./frontend/javascripts/index.js を元に、 ./index.js としてトランスパイルした結果を出力します。
ココらへんも書くと結構長くなるので今回は端折りますが、次回の記事で実例とともに書こうと思います。
ちなみに、この index.js には npm install した react や redux 等も含まれている状態とします。
10. index.html の作成
外部からアクセスできるディレクトリに
と書いたHTMLを準備すると、 React の DOM が描画されます。./index.html
<html> <body> <div id="content"></div> <script src="./index.js"></script> </body> </html>
もちろん上記の HTML の index.js は色々すっ飛ばして書いていますが、
これらについて今後別途サンプルのリポジトリを作成します。
11. 完成
内容としては以上です。
./index.js を読み込むと1〜7までに書いた react と redux の部分が処理されていることがわかるかと思います。
ブラウザのコンソール等を見て動きをチェックしてみてください。
終わりに
こんな形でだいたいの作り方は理解はできたでしょうか?
実際の完成形のコードや概念は理解できても、どこからどう作れば良いのかのノウハウが足りなかったので参考にしてもらえればと思います。
今後実際の動くコードや gulp を使ったトランスパイル例、また非同期通信等の実装などもかければと思っていますので応援よろしくお願いいたします!