Contents
React
React そのものは次の記事も参照。(React 公式よりももっと低レベルなところから始める、React チュートリアル, React チュートリアル 日本語翻訳) React は State を元にバーチャルDOMを作成し、バーチャルDOMを元にDOMを生成するシステムを担うライブラリ。state の更新があった場合、その差分だけを DOM に反映することができ、パフォーマンスが良い。つまり、PC への負担が少ない。React を用いる利点のうち、一番大きなものは、状態 state と view 実際の見た目をどうするか、という「二つの問題を完全に切り分ける」ことができる。つまり、state をどう変化させるか、という問題と、state を DOMにどう反映させれるか、の2つを完全に切り分けて実装できる。
React Component
React はコンポーネントと呼ばれる最小単位を、積み重ねることでバーチャルDOM全体の構成を行う。具体的には、HTMLに直接紐付けられたトップのコンポーネントの中に、さらにコンポーネントを配置し、またさらにその下にコンポーネントを配置していくことができる。入れ子状に。(マトリョーシカのように一つのコンポーネントの中に、一つのコンポーネントしか配置できないわけではない。ある階層のコンポーネントには、複数の下位コンポーネントを配置することができる。)
state を持つコンポーネントと持たないコンポーネント
React コンポーネントは、コンポーネント自身が状態 state を持つものと(一般的にES6のクラス構文で書かれ、extended React.componentし、コンストラクタ内でsuper(props)によってReact.componentを継承し、this.stateに状態をオブジェクトとして持たせる / 公式ref もしくはこれ 参照)ともたない functional component がある。
持つコンポーネントの場合、stateはprivateであり、つまり状態はコンポーネントの外からアクセスできない。アクセスできるようにするためには、react の機能だけで実装する場合には、stateを下位のコンポーネントへpropsとして渡す方法がある。(反対に上位のコンポーネントに渡すことも原理的には可能だが、上下二方向にデータが流れると設計が破綻することが多く、そのため基本的には下位コンポーネントへstateを流していく。)
原理的にはすべてのコンポーネントにstateを持たせることができるが、reduxと連携する場合には、stateはすべてreduxによって管理するため、コンポーネントにstateを持たせず、すべてのコンポーネントをfunctional componentで書く。(ただし、UI用のstateを持たせることはある)
state の更新と取得
stateの更新は、this.state.setState(オブジェクト)によって行われ、その際に新しいstate用のオブジェクトを与えれば良い。stateの取得は、this.state.value によって可能である
Redux
Redux は state 管理のためのライブラリで、必ずしもReactと連携する必要はなく、単体で動作するが(公式チュートリアル参照)、React と共に用いられることが多いのは事実である。ここでは基本的にReactと連携することを前提に進める。また、当然だが React の状態管理は必ずしもReduxを用いる必要はなく、前述したreactコンポーネントのstateを用いることが第一の方法である。
Redux によって、React を用いたアプリケーションの状態を管理させる第一の利点は、データフローの設計と構造のベストプラクティスのうちの一つを、同時に取り込むことができる点にある。つまりどのようにstateを更新するのかという仕組みと、それを実装するための手段を、Reduxによって同時に取り込むことができる。Redux は具体的には、stateを1つの単なるオブジェクトで管理し、その更新にはreducerというこれまた単なる関数を用いる。Reducer ファンクションに対して二つの引数、現在の状態と、その変更に必要なパラメーターをactionを渡し、その二つの引数を元に更新されたstateを生成し、これをreturnする。
returnしたstateがどのように、更新されるか。それはstore = createStore(reducer)によってreducerを紐付けて、state管理用のstoreという変数を作ることからはじまる。これによって、reducerのreturnしたstateが、storeに返され、stateが更新される。
Redux と Flux の関係
Reduxは、stateの管理とその更新のシステムを書く方法を、単一に定義している。そのシステムは基本的にFluxに従っている。Fluxは状態管理の設計思想である。そのためライブラリではない。(ただしfluxというそのためのライブラリもある) つまりFluxが概念としては一番大きいもので、ReduxはFluxに基づいた実装のための方法の一つを担うライブラリである。
Redux は、そのシステムに則って書けば、結果的にFluxになるので、初学者にはむしろ易しい。Reduxを書くことによって、Fluxの概念を理解することが容易になるのではないか。またReduxは公式チュートリアルが非常に丁寧であり、その点もFluxを理解するためによい。
またReduxはstateを一箇所だけで管理するために、実際の業務においては、これでは問題が起きるシーンがある。そういった場合には、Fluxに則っり、しかしReduxを使わずに、自分でstateの管理を実装する必要が出てくる。そのため、単にReduxの「書き方」のように覚えてしまうのではなく、reactとstateの管理、変更について深い理解をすることが必要になると思われる。
Reduxによるstateの作成
reduxからcreateStoreを引っ張ってきて、createStoreでreducerを紐付けることで作ったstoreでstateを管理する。
createStoreの準備
import { createStore } from 'redux';
storeの作成。reducerと紐付ける。(reducerはステイトの初期値、変更、といった管理業務を一手に引き受けている)
let store = createStore(counter);
providerによってreactのコンポーネントとstoreを結びつける
react-reduxからproviderを引っ張ってきて、providerでreactコンポーネントの一番上の層(一番上でなくてもいいが、そのコンポーネント以下から参照可能になる)とstoreを紐付けることで、その下のコンポーネントにおいてconnectを使うことが可能になる。(connectの役割は、storeとあるコンポーネントを紐付けて、そのコンポーネント内でstoreの参照ができるようにすること。)
Providerを引っ張ってくる。
import { Provider } from 'react-redux';
コンポーネントの最上部にstoreをProviderによって紐付ける。ここではTestコンポーネント以下にstoreを紐付けるために、TestコンポーネントをProviderでラップした上で、それにstore={ store } というpropsを与えることで実現している。
const AppRoot = () => ( <Provider store={ store }> <Test /> </Provider> );
ただしこれだけですべてのコンポーネントからstoreが参照できるわけではない。次の手順を加える。
providerによって紐付けられたstoreを、各コンポーネント内で参照できるように、connectによってコンポーネントとstoreの紐づけを行う
react-reduxからconnectを引っ張ってくる
import { connect } from 'react-redux';
仮に以下のようなTestというコンポーネントを用意しており、これとstoreを結びつけるとする。
const Test = ({dispatch, num}) => ( <div> {num} <button onClick={()=>{dispatch({ type: 'INCREMENT' })}}>+</button> <button onClick={()=>{dispatch({ type: 'DECREMENT' })}}>-</button> <Test2/> </div> );
まずmapStateToPropsという中間的な役割を担う関数で、storeのstateのうち、どのstateを必要とするか、定義する。ここではstate全体を引っ張ってきて、numという値として渡すことにしている。
Testコンポーネントをexport defaultで書き出す際に、connect(関連づけるstate)(関連づけるTestコンポーネント)という記述で、conncetされたTestコンポーネントを書き出している。
function mapStateToProps(state) { return { num: state } } export default connect(mapStateToProps)(Test)
紐付けられたコンポーネントにおいては、以下のように、dispatchがグローバルな変数として使うことができる。また{num}のようにmapStateToPropsで紐付けたstateを参照できる。
const Test = ({dispatch, num}) => ( <div> {num} <button onClick={()=>{dispatch({ type: 'INCREMENT' })}}>+</button> <button onClick={()=>{dispatch({ type: 'DECREMENT' })}}>-</button> <Test2/> </div> );
手順まとめ
- createStoreでstoreを作る。storeでstateを管理する。createStoreをする際に、reducerを紐付ける。reducerはstateの更新を担当するもの。storeにreducerが紐付いているので、store.dispatch({action:content})でstateを変更できる。
- providerを用いてreactコンポーネントの階層の一番上にstoreをprospsとして渡すことで、その下の階層でconnectを使うことができる。そのためにはreactコンポーネント階層の一番上のコンポーネントをProviderコンポーネントでラップし、Providerコンポーネントに、storeという名前で{ store } を渡す。
- Providerコンポーネントによって渡されたstoreを、様々な階層の位置にあるコンポーネントから参照するためにはconnect()を用いる。コンポーネントを出力するexport defalut 以降に export defalut connect(propsを紐付ける中間関数)(紐付けるコンポーネント)とする。propsを紐付ける中間関数は、storeに保持されているstateのうち、必要なものだけを選んで使用するためのもの。
reducer や action の効率的な取りまとめ
ReducerをcombineReducersでsplitする
https://codesandbox.io/s/kR1jXvD7X
次のようなstateをreduxで管理する場合、counterとtext、それぞれのpropertyのvalueに対して、1つのreducerをわり当てる方が、全体をたった一つのreducerで管理するよりも、関心を分離できるので、開発に有利。
state = {counter:0,text:”text”}
そのためには、reducersフォルダの中にindex.jsを作り、ここで全てのreducerをimportし、それをcombineReducerでまとめる。そしてこれをimport reducer from ‘./reducers’として、reducerをcreateStore(reducer)で紐付ける。
reduxのcombineReducerを用いる。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | // reducers/index.js //ここで全てのreducerをimportする import counter from './counter' import text from './text' //複数reducerをimportした場合には、combineReducerでまとめる import { combineReducers } from 'redux' const main = combineReducers({ counter, text }); export default main; <strong> //こうすると、stateが、 //{counter: 内容, text:内容}というオブジェクトになる。 //合体している。 </strong> |
importの対象をディレクトリにすると、その直下にあるindex.jsを読み込むことになる。結果として上記コードのmainをimportすることになる。そしてそれをstoreと紐付ける。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | import React from 'react' ; import { createStore } from 'redux' ; import { Provider } from 'react-redux' ; import Test from './Test' ; <strong> //reducerが複数入っているディレクトリ。 //ディレクトリを指定するだけで、そのなかのindex.jsでexportされているものが読み込まれる import reducer from './reducers' ; </strong> //redux の createStore で Reducer と結びつける const store = createStore(reducer); const AppRoot = () => ( <Provider store={ store }> <Test /> </Provider> ); store.subscribe(() => console.log(store.getState()) ); export default AppRoot; |
Actionを作成するaction creator
一つの方法として、actionsディレクトリ内にindex.jsというファイルを作り、その中でAction Creatorを作成し、作成したものをexportしておく。それをContainer層で読み込み、 同様にContainer層で読み込まれたComponentと結びつけるために、mapDispatchToProps内dispatchを呼び出す際に使用する。
疑問 ToDoListというcomponentを引っ張ってきて、Container層で各種要素をconnectしたものをVisualTodoListとしてexportしている。