※この記事を書いたのは2016年4月です。Qiitaでは記事をアップデートするとその日付のみが表示されていまうため、新しい記事のように見えるかもしれませんが、現代ではもっと進化していることにご注意ください。素直にReact Hooks を使いましょう。あと Redux は用法用量を守って気をつけて使ってください。なんならReduxは使わない方がいいでしょう。
最近のモダンなウェブフレームワークと言えば、React+Reduxですよね。でも、なんか難しそうとか、ReactってPHPみたいにViewにロジック混ざりそうとか感じて尻込みしていませんか?それはただの誤解かもしれません。React+Reduxはそんなに難易度の高いものではありません。ただ単に、新しい概念で構成されているから、カルチャーショックのようなものがある、というだけのことです。React+Reduxに入門してみましょう。
僕自身初心者なので、大いなる勘違いをしているかもしれませんので、是非識者の方は間違いなどがありましたら、突っ込みや補足などをいただけると幸いです。
さて、日本のWebエンジニアの大半が、変化に対応しきれなくなっている件について。 - 日々、とんは語る。っていうブログが話題になっていますが、実際のところはウェブフロントの世界を知らない人、特にJavaやPHPやってるような老害な人達、もしくは技術力がなくてもウェブフロントならやれると思い込んでた人達が騒いでるだけなのでは?という状況のようです。
JavaScriptにはむしろもっと抽象化がもたらされるべきという記事を書きました。合わせて読んでみてください。
ただ、Reactに限らずJS界隈は日本語の資料があまりよくないので、英語をあたるしかないケースがきわめて多いです。そういう意味では英語を読みたくない人にはJavaScriptは敷居が高いでしょう。
JavaScriptやるなら、必ず最初に公式を見て、次に公式や GitHub を見てから、最後にStackoverflowやZennを読みましょう。
ぶっちゃけ、ReactのTutorialとReduxのドキュメントをつまみ食いするだけで覚えられます。(それだけでOKな人は、以下を読む必要ないかもしれませんが)
React+Reduxのカルチャーショックポイント
- Reactはただ与えられたプロパティを元に描画するだけのView
- Reduxで状態管理を奪い取る事でReact部分を簡単にする
- 状態遷移を元にプロパティを生み出して描画に反映させる
簡単に言えば、(分割統治もできる)一つの大きな状態遷移図があります。状態が遷移したらそれが勝手にViewに反映されます。状態遷移の管理と描画を切り分ける事ができるというものです。ウェブプログラミングもやっとまともになりました!
Reactとは何か
Reactとは、コンポーネントの考え方を推し進めた、新しい概念のちょっとかしこいViewライブラリです。Reactの伝道師mizchi氏のなぜ仮想DOMという概念が俺達の魂を震えさせるのか - Qiitaとか読んでみてください。
さっきも書きましたが、React+Reduxに限定すれば、Reactは与えられたプロパティを元に描画を組み立てるだけの簡単なお仕事です。ここではReactのコンポーネントについてフォーカスして説明していきます。
コンポーネント
ES2015ならコンポーネントは単に React.Component を継承したクラスに過ぎません。JSXという少し気持ちの悪いJavaScriptの拡張は、JS上のコンポーネントクラスと、描画上のコンポーネントを一致させるためのアイデアにすぎません。つまりPHPのようなViewにロジックを混ぜ込ませる為のものとは考え方そのものが違います。
コンポーネントは階層構造になっています。
- トップコンテナ(ただのidを振られたdiv要素)
- App コンポーネント
- Hoge パラメータ: 1
- Hoge パラメータ: 2
- Hoge パラメータ: 3
- App コンポーネント
import React from 'react'
import { render } from 'react-dom'
import App from './app'
render(<App />, document.getElementById('root'))
このようなコードで、rootというIDを持つdivコンテナにAppを流し込みます。これは同じディレクトリにあるapp.jsxというファイルでexportされているAppクラスです。ここで注意したいのは、たとえば<App />
のように書いても特にエラーも何もでず、描画がされないだけだというトラップです。
import React from 'react'
import Hoge from './hoge'
export default class App extends React.Component {
render() {
return <div>
<Hoge param=1 />
<Hoge param=2 />
<Hoge param=3 />
</div>
}
}
AppではReact.Component
を継承して、render
メソッドを実装します。コンポーネントは使い回すためにパラメータを渡します。Hogeというコンポーネントをparam=1,2,3をそれぞれセットして呼び出します。
import React from 'react'
export default class Hoge extends React.Component {
render() {
return <div>ほげ{this.props.param}</div>
}
}
Appと同様ですが{this.props.param}
という新しいものが出てきました。this.props
は、パラメータとして渡された全プロパティが入っています。Appでparam=1などが渡されている為、this.props.param
としてアクセスができます。
さて、React+Reduxでは、ここまで覚えればもう大丈夫です。React公式を含めてReactを説明した文章ではpropsだけではなくて、stateというものも扱われるのですが、Reduxを併用する限りいったん忘れてかまいません。(別のところで別のstateが出てくるけど)。Reactは単に与えられたpropsを元にViewを作るだけのものです。簡単ですよね!!
React+Redux
ReduxはReactから状態管理を奪い取り、ある一箇所で状態遷移を関数型言語ちっくにいじろうっていうものです。
詳しくはRedux入門【ダイジェスト版】10分で理解するReduxの基礎 - QiitaとRedux入門 6日目 ReduxとReactの連携(公式ドキュメント和訳) - Qiitaを読めば大体わかると思います。
それを踏まえて、React+Reduxがどういうものかまとめてみます。ReduxやFluxアーキテクチャと呼ばれるものは、データを一方通行にする事で簡易化するという考え方があります。初期状態から順に追いかけますが、その前にいったん、ReactとReduxをつなぐ方法を見ていきましょう。
ReactとReduxをつなぐ
react-redux
というモジュールを使います。
import React from 'react'
import { render } from 'react-dom'
import { createStore } from 'redux'
import { Provider } from 'react-redux'
import App from './container/app'
import reducer from './reducer'
const store = createStore(reducer)
render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)
初期状態
Reduxでは状態遷移をreducerというもので管理します。
const initialState = {
fuga: 1
}
export default function reducer(state = initialState, action) {
switch(action.type) {
case 'INCREMENT':
...
default:
return state
}
}
reducerでは、action.typeに応じたアクションを元に状態遷移を行うのですがいったん後回しにします。default: return state
がキモで、一番最初、initialStateがセットされるような呼び出し方がされます。それによって、state が initialState である { fuga: 1 }
になります。
container
containerはReduxのStoreが管理する状態遷移をReactのプロパティとして流し込む役割を持ちます。正確にはもう一つdispatchという重要な役割があるのですがいったん後回しにします。
import React from 'react'
import { connect } from 'react-redux'
import App from '../component/app'
function mapStateToProps(state) {
return state
}
export default connect(mapStateToProps)(App)
connectは、ReduxとReactのコンポーネントを繋ぎ込む為のメソッドです。mapStateToPropsは、一枚岩のでっかいstateの中から、対象のコンポーネントに合ったプロパティを生成する為のものです。
実のところ、これは説明としてはへたくそです。mapStateToPropsで何もやってないので。よくあるパターンはstate自体が複雑な時に必要なステートを取り出して、プロパティに変換するようなコードです。
初期化直後であれば先ほどのreducerでの初期化によってstateのfugaは1になっているはずです。なので、Appコンポーネントにはprops.fugaとして1が渡ります。後は、propsを使って描画をするだけです。これはさきほどReactの説明でした通りなので省きます。
ユーザーアクションを受け付ける方法
さきほどcontainerで説明を省略したdispatchの扱いをしてみましょう。
import React from 'react'
import { connect } from 'react-redux'
import App from '../component/app'
import { increment } from '../action/app'
function mapStateToProps(state) {
return state
}
function mapDispatchToProps(dispatch) {
return {
handleClick: () => { dispatch(increment()) }
}
}
export default connect(mapStateToProps, mapDispatchToProps)(App)
mapDispatchToPropsは、dispatch関数を受け取ってプロパティに変換します。dispatch関数はstoreにアクションを流し込む為のもので、たとえば、上記の例だとhandleClickというプロパティは引数なしの関数で実行するとincrement()した戻り値をdispatchします。
increment()はActionCreatorと呼ばれるものです。
export default {
increment: () => {
return { type: 'INCREMENT' }
}
}
Actionとは、どういうアクションを行うか?というtypeと、他の任意のパラメータを持ったプレーンなオブジェクトです。
最後にReactで繋ぎ込みましょう。
import React from 'react'
export default class App extends React.Component {
render() {
return <div>
<span>{this.props.fuga}</span>
<button onClick={ () => this.props.handleClick() }>増加</button>
</div>
}
}
おさらいをしましょう。
- containerでは
mapDispatchToProps
で、increment (ActionCreator) を呼び出して{ type: 'INCREMENT' }
をdispatchする関数を生成してhandleClickという名前のプロパティとして返す - componentでは
<button onClick={ () => this.props.handleClick() }>増加</button>
でbuttonのonClickイベントによって、プロパティのhandleClick()を呼び出す
状態遷移する
最後に、reducerでactionを元に状態遷移をします。
const initialState = {
fuga: 1
}
export default function reducer(state = initialState, action) {
switch(action.type) {
case 'INCREMENT': {
return { fuga: state.fuga + 1 }
}
default:
return state
}
}
ただ、これだけです。action.type: INCREMENTをうけつけると、stateのfugaを1増加するだけです。注意点は元のstateを変更してはいけないことです。変化した後の値を返すだけで、元の値に手を加えてはいけません。(個人的にはこの制約納得いかない。)
終わりに
Electron + React + Redux でポモドーロタイマーを作ってみました。