React Native ~ Reduxの導入1 ~
Reduxの導入
前回に引き続き、reduxの導入をやっていきたいと思います。
今回、reduxの導入はreact-native-router-fluxを用いようと思います。
reduxの導入は、react-native-router-fluxを使わずともできますが、 flux/reduxに対応しており導入が楽なのと、 routingの際、stateが自動的にpropsとして渡ってきてとても便利です。
次の構成を目指して構築していきます。
続きを書きました。
app ├── actions // 関数。http通信や、データの処理をする ├── components // containerからpropsを受け取る部品 ├── containers // reducerの受け口、pageのような部品 ├── reducers // action受け、新しいstateを返す └── route.js // routingを行う。storeが一番上から渡る。
reduxの内容に関しては、ここではまとめず、良記事に委ねることにします。
Reduxの登場人物(Action、Reducers、Storeの詳しい説明。連携方法)をまとめてくれています。
必要なlibralyのインスコ
react-native-router-fluxが入った前提で、進めます。
1.redux 言わずとしれた今回の主役。インスコ npm i -S redux
2.react-redux reduxは独自で動くものなので、Componentと紐づける必要があります。react-reduxはProviderやconnectで繋いでくれます。 これもインスコ npm i -S react-redux
3.各種ミドルウェア Reduxはthin framworkを目指しているので、基本機能以外は外出しです。 今回は非同期処理と、ログ機能のミドルウェアを入れたいと思います。 npm i -S redux-thunk redux-logger
これで必要なものの準備は整いました。 それではReduxを導入していきましょう
Goal
みんな大好きTodo App的なものを目指していきたいと思います。
Actionの作成
Actionは、主にユーザーの行動で発生したイベントによって変更した値をstoreへと送る関数となります。 ActionでHTTP通信や、データの加工を行うのが正しいようです。 どのようにStoreに送るかというと後述にはなりますが、Containersで
dispatch(action())
ラップすることによってStoreに通知がいきます。
まずはファイルを作成しましょう。名前は何でもいいですが、できる限りReducerと対になっている方がわかりやすいです。
actions ├── todo.js
続いて、中身を作成していきます。 Actionsでは、必ず一つのタイプ(Reducerで識別するための定数)と、任意の値を返却します。
export const TEXT_UPDATE = 'TEXT_UPDATE' export function fetchTextUpdate(text){ return{ type: TEXT_UPDATE, text: text, } }
Actionには必ず、タイプの返却が必要です。 なぜなら、ActionはStoreに変更を通知しますが、ボールを投げるだけです。 どこに行くなんか知ったこっちゃないです。
それを華麗に受け取り、新しいstateを返すのがreducerの役割です。 僕はここの理解で一度つまづきました。。。
Reducer
ReducerはActionからStoreに投げられた通知を受け取り、Stateを更新するための関数です。
次の構成を目指します。
reducers ├── index.js ├── todo-reducer.js /// 下にreducerが増えていく
index.jsは、Reducerをまとめ上げる役割としてあります。(子Reducer) 何故この機能が必要かというと、アプリが大きくなるごとに1つのReducerでは複雑かつわかりにくくなるからです。
MVCのModel設計のようなものと捉えています。増えない事はまずありえないので、初めからこの設計にしておくと良いです。
index.jsでは子Reducerをまとめて返してあげます。 こうすることで、1つのstoreに複数のReducerがぶら下がったStateを作ることができます。combineReducersといいます。
// index.js import { combineReducers } from 'redux'; import todoReducers from './todo-reducer'; export default combineReducers({ todoReducers, aReducers, // 作るたび追加していく bReducers, })
Reducerを作成します。 Actionで返された値は、Storeを通じで全Reducerが受け取ります。ここで識別するためにtypeが必要となります。Reducerではtypeで条件分岐して新しいStateを返します。
// actionのtypeをimportしておく import { TEXT_UPDATE, } from '../actions/todo'; // 初期化 const initialState = { text: '', }; // 入り口は一つにしてswitchで分ける。 // stateは前回のstateもしくはinitialStateが // actionはactionでreturnしたvalueが渡る。 // reducer内ではstateを更新することはせず、新しいstate(assignして)を返す。 export default function todoReducer(state = initialState, action = {}){ // typeでswitchする switch (action.type){ case TEXT_UPDATE: // 空のObnectにマージする return Object.assign({}, state, { text: action.text, }) // 初期状態を返してあげる必要がある。 default: return state; } }
Store
アプリケーション全体の状態(State)を管理するオブジェクトです。 Routingと紐づけるために、react-naitve-router-fluxをimportしているroute.jsに記述します。
必要なlibralyをimportします。
import { Router, Scene, Modal, Switch } from 'react-native-router-flux'; import { createStore, applyMiddleware, compose } from 'redux'; // redux。storeの作成やmiddlewareの提供 import { Provider, connect } from 'react-redux'; // reduxとreact-nativeを関連づけてくれる import thunkMiddleware from 'redux-thunk'; // reduxの非同期処理(middlewareの例) import createLogger from 'redux-logger'; // log出力(middlewareの例)
router-fluxとstoreを関連づけます。 connectはclassとreduxを関連づけてくれるmethodです。 Containerでも登場します。
import { Router, Scene, Modal, Switch } from 'react-native-router-flux'; import reducers from './reducers'; const RouterWithRedux = connect()(Router);
reducers、middlewareを持った、storeを作成します。 Storeはアプリケーションに1つでStoreがアプリケーションの状態(State)を保持します。
// create store... const loggerMiddleware = createLogger(); const middleware = [thunkMiddleware, loggerMiddleware]; const store = compose( applyMiddleware(...middleware) )(createStore)(reducers);
Storeが作成できたので、Routingと紐付けをしましょう。
render() { return ( <Provider store={store}> <RouterWithRedux> <Scene key="root"> <Scene key="drawer" component={Drawer}> <Scene key="tabbar" tabs={true} > <Scene key="BasicComponents" hideNavBar={false} hideTabBar={true} initial={true} component={BasicComponents} title="Basic" icon={TabIcon}/> <Scene key="ApiComponents" hideNavBar={false} component={ApiComponents} title="Api" icon={TabIcon}/> <Scene key="AwesomeLibralies" hideNavBar={false} component={AwesomeLibralies} title="Awesome" icon={TabIcon}> </Scene> <Scene key="Hardwares" hideNavBar={false} component={Hardwares} title="Hardwares" icon={TabIcon}/> </Scene> </Scene> <Scene key="ImagePage" component={ImagePage} title="ImagePage" icon={TabIcon}/> <Scene key="Blur" hideNavBar={false} component={Blur} title="Blur" sceneStyle={{marginTop: 64,}} /> <Scene key="Video" hideNavBar={false} component={Video} title="Video" sceneStyle={{marginTop: 64,}} /> <Scene key="ParallaxView" hideNavBar={false} component={ParallaxView} title="ParallaxView" sceneStyle={{marginTop: 64,}} /> </Scene> </RouterWithRedux> </Provider> ) }
Reduxが導入できているか確認
Componentの作成はまだですが、実はReduxが導入できたか確認する事ができます。 何故なら初期状態はreducersのinitialStateで定義されているからです。
アプリを起動してcommand + DでDebug Js Remotelyを選択します。
これでDebugができるようになります。 Chrome Developerから確認することができます。
redux-loggerにより、logが出力されています。 初期状態を空にしてしまったのでわかりにくいですが、しっかりと、nextStateの下にtodoReducerがぶら下がっているのが確認できますね。
Viewとの接続までやりたかったのですが、長くなっってきましたので、次回にしたいと思います。
次回は、導入できたreduxをどうViewと紐づけるかをやってみたいと思います。