Redux と仲直りする
一昨日、Node.js 再入門の日記で Flux を試した。 今回はこの勢いで React な方向に歩を進めて Redux と仲直りしたいと思う。
Flux ネタはいろいろな技術を組み合わせすぎてサンプルが繁雑になってしまったため、今回は React に焦点を絞る。
本日の題材
本日の題材の元ネタは、「WEB+DB PRESS Vol.92」にあったカウンタ的なプログラムである。
[+] ボタンを押すとカウントアップ、[-] ボタンを押すとカウントダウンするだけのものだ。
面白みはないが、今はきちんとした日本語の説明を書く余裕がないため、後からコードを読み返して思い出しやすいようサンプルはシンプルな方がよいだろう。
準備
前準備として、create-react-app
で React のプロジェクトを作っておこう。
React 実装(ノーマル)
はじめに、Flux も Redux も使わず、 React のみでカウンタを書いてみる。
src/App.js
import React, { Component } from 'react' class App extends Component { constructor(props) { super(props) this.state = { counter: 0 } } incrementCounter() { this.setState({ counter: this.state.counter + 1 }) } decrementCounter() { this.setState({ counter: this.state.counter - 1 }) } render() { return ( <div> <div>{this.state.counter}</div> <button onClick={() => this.incrementCounter()}>+</button> <button onClick={() => this.decrementCounter()}>-</button> </div> ) } } export default App;
Flux 適用版
上記のコードに対して Flux を適用してみた。 これはあくまで通過点というか、参考情報というか。
$ npm i --save flux
src/App.js
import React, { Component } from 'react' import { Dispatcher } from 'flux' // Store const CounterStore = { counter: 0, onChange: null } // Action const ActionType = { INCREMENT_COUNTER: 'INCREMENT_COUNTER', DECREMENT_COUNTER: 'DECREMENT_COUNTER' } const Actions = { incrementCounter: () => { AppDispatcher.dispatch({ actionType: ActionType.INCREMENT_COUNTER, newValue: CounterStore.counter + 1 }) }, decrementCounter: () => { AppDispatcher.dispatch({ actionType: ActionType.DECREMENT_COUNTER, newValue: CounterStore.counter - 1 }) } } // Dispatcher const AppDispatcher = new Dispatcher() AppDispatcher.register(payload => { switch (payload.actionType) { case ActionType.INCREMENT_COUNTER: case ActionType.DECREMENT_COUNTER: CounterStore.counter = payload.newValue CounterStore.onChange() break default: break } }) class App extends Component { constructor(props) { super(props) this.state = { counter: 0 } CounterStore.onChange = () => { this.setState({ counter: CounterStore.counter }) } } render() { return ( <div> <div>{this.state.counter}</div> <button onClick={() => Actions.incrementCounter()}>+</button> <button onClick={() => Actions.decrementCounter()}>-</button> </div> ) } } export default App;
Redux 適用版(ノーマル)
ここからいよいよ Redux を適用する。
まずは混じりけのない Redux で先のコードをリファクタリングしてみる。
$ npm i --save redux react-redux
src/App.js
import React from 'react' import { connect } from 'react-redux' // Action Creators function incrementCounter() { return { type: 'INCREMENT_COUNTER' }; } function decrementCounter() { return { type: 'DECREMENT_COUNTER' }; } // Reducer export function counter(state = { counter: 0 }, action) { switch (action.type) { case 'INCREMENT_COUNTER': return { counter: state.counter + 1 }; case 'DECREMENT_COUNTER': return { counter: state.counter - 1 }; default: return state; } return state; } // Reactコンポーネント let App = ({ counter, dispatch }) => ( <div> <div>{counter}</div> <button onClick={() => dispatch(incrementCounter())}>+</button> <button onClick={() => dispatch(decrementCounter())}>-</button> </div> ); // ReactとReduxの接続 function mapStateToProps(state) { return { counter: state.counter } } App = connect(mapStateToProps)(App) export default App
src/index.js
import React from 'react' import ReactDOM from 'react-dom' import App, { counter } from './App' import { createStore } from 'redux' import { Provider } from 'react-redux' import './index.css' import registerServiceWorker from './registerServiceWorker' const store = createStore(counter) ReactDOM.render( <Provider store={store}><App /></Provider>, document.getElementById('root') ); registerServiceWorker();
Redux 適用版(Reducer 分割版)
Redux でコーディングしていると、どうしても Reducer はメタボ化するらしい。
そんな Reducer を一箇所に固めて書くとどうしてもモンスターコード化して保守しづらくなるので、Reducer は分割定義しておき、あとで combineReducers
でひとまとめにするのがよい実装らしい。
ただし combineReducers
はいくつかはまりどころがある。 一度わかってしまえばなんということもないが、たまに忘れるので参考になる Qiita を自分用に貼っておく。
なお、先ほどのコードを combineReducers
使用版に書き換えた例が以下。 べつに Reducer を複数に分けているわけではないため、あまり実用性はないか。
src/App.js
import React from 'react' import { combineReducers } from 'redux'; import { connect } from 'react-redux' // Action Creators function incrementCounter() { return { type: 'INCREMENT_COUNTER' }; } function decrementCounter() { return { type: 'DECREMENT_COUNTER' }; } // Reducer function counter(state = 0, action) { switch (action.type) { case 'INCREMENT_COUNTER': return state + 1; case 'DECREMENT_COUNTER': return state - 1; default: return state; } } const reducer = combineReducers({ counter }); export { reducer } // Reactコンポーネント let App = ({ counter, dispatch }) => ( <div> <div>{counter}</div> <button onClick={() => dispatch(incrementCounter())}>+</button> <button onClick={() => dispatch(decrementCounter())}>-</button> </div> ); // ReactとReduxの接続 function mapStateToProps(state) { return { counter: state.counter } } App = connect(mapStateToProps)(App) export default App
src/index.js
import React from 'react'; import ReactDOM from 'react-dom'; import { createStore } from 'redux'; import { Provider } from 'react-redux' import App, { reducer } from './App'; import './index.css'; import registerServiceWorker from './registerServiceWorker'; const store = createStore(reducer) ReactDOM.render( <Provider store={store}><App /></Provider>, document.getElementById('root') ); registerServiceWorker();
Redux-Actions 適用版
最後に、Action (と Reducer)まわりの記述がラクになる Redux-Actions を試す。
無駄な switch 文が消えてスッキリするので、私はこちらの書き方が好みだ。
$ npm i --save redux-actions
src/App.js
import React from 'react' import { connect } from 'react-redux' import { createActions, handleActions } from 'redux-actions'; // Actions const actions = createActions( 'INCREMENT_COUNTER', 'DECREMENT_COUNTER' ); // Reducer const counter = handleActions({ [actions.incrementCounter]: (state, action) => ({ counter: state.counter + 1 }), [actions.decrementCounter]: (state, action) => ({ counter: state.counter - 1 }) }, { counter: 0 }); // 初期値 export { counter as reducer } // Reactコンポーネント let App = ({ counter, dispatch }) => ( <div> <div>{counter}</div> <button onClick={() => dispatch(actions.incrementCounter())}>+</button> <button onClick={() => dispatch(actions.decrementCounter())}>-</button> </div> ); // ReactとReduxの接続 function mapStateToProps(state) { return { counter: state.counter } } App = connect(mapStateToProps)(App) export default App
Redux-Actions 非使用版との diff
diff --git a/src/App.js b/src/App.js index 6d6ada4..4e7fd39 100644 --- a/src/App.js +++ b/src/App.js @@ -1,34 +1,30 @@ import React from 'react' -import { combineReducers } from 'redux'; import { connect } from 'react-redux' +import { createActions, handleActions } from 'redux-actions'; -// Action Creators -function incrementCounter() { return { type: 'INCREMENT_COUNTER' }; } -function decrementCounter() { return { type: 'DECREMENT_COUNTER' }; } +// Actions +const actions = createActions( + 'INCREMENT_COUNTER', + 'DECREMENT_COUNTER' +); // Reducer -function counter(state = 0, action) { - switch (action.type) { - case 'INCREMENT_COUNTER': - return state + 1; - case 'DECREMENT_COUNTER': - return state - 1; - default: - return state; - } -} - -const reducer = combineReducers({ counter }); -export { reducer } +const counter = handleActions({ + [actions.incrementCounter]: (state, action) => + ({ counter: state.counter + 1 }), + [actions.decrementCounter]: (state, action) => + ({ counter: state.counter - 1 }) +}, { counter: 0 }); // 初期値 +export { counter as reducer } // Reactコンポーネント let App = ({ counter, dispatch }) => ( <div> <div>{counter}</div> <button onClick={() => - dispatch(incrementCounter())}>+</button> + dispatch(actions.incrementCounter())}>+</button> <button onClick={() => - dispatch(decrementCounter())}>-</button> + dispatch(actions.decrementCounter())}>-</button> </div> );