Ducks というデザインパターン
- Reduxのファイル構成は『Ducks』がオススメ – Qiita
- The Ducks File Structure for Redux – S.C. Barrus – Medium
- erikras/ducks-modular-redux: A proposal for bundling reducers, action types and actions when using Redux
Redux Way
React + Redux で構築する際のディレクトリ構成としてよく知られるのは、redux-way と呼ばれるやつです。Reduxの登場人物であるところの、container, reducer, action(action creator) それぞれについてディレクトリを分けてファイル単位で分割します。ディレクトリ構成はこんな感じになります。
src
├─── actions/
├─── constants/
├─── containers/
└─── reducers/
メリットはきっちりと役割単位で分けられること。デメリットは分離されすぎてどこに何があるかわかりずらいこと。
Ducks
それに対して Ducks というデザインパターンは、action(action creator), reducer, constants を module というファイルにまとめてしまいます。
どのみち密結合になっているので1ファイルにまとめたほうが見通しが良く管理しやすいという理由です。
こうすることですっきりと次のようなディレクトリ構成となります。
src
├─── containers/
└─── modules/
ではこのディレクトリ構成(Ducks)でサンプルアプリ(カウンター)を作ってみます。
環境構築
Reactの開発環境は create-react-app で作成しますので、まずはこれをインストールします。
$ npm install -g create-react-app
プロジェクトを作成します。プロジェクト名は適当です。
$ create-react-app redux-ducks
$ cd ./redux-ducks
必要なツールをインストールします。
$ npm install --save redux react-redux redux-logger
module
ducks における module とは、Action
, ActionCreateor
, Reducer
をひとまとめにしたものです。これらは別ファイルに分けてもどうせ密結合しがちだから、1ファイルにまとめて module として定義しようという考え方です。
export default
では、Reducer
を定義する。export
でActionCreator
を関数として定義する。action
は定数として定義する。
ひとまず上記の感じで実装します。いずれもピュアな実装です。
modules/Counter.js
// action type
const INCREMENT = 'COUNTER_INCREMENT';
const DECREMENT = 'COUNTER_DECREMENT';
const initialState = {
state: 0,
}
// reducer
export default function reducer(state=initialState, action) {
switch (action.type) {
case INCREMENT:
return {
...state,
count: state.count + 1
}
case DECREMENT:
return {
...state,
count: state.count - 1
}
default:
return state;
}
}
// action-creator
export function increment() {
return { type: INCREMENT };
}
export function decrement() {
return { type: DECREMENT };
}
createStore
ストアを生成するための処理を定義します。Rducerは module に定義されています。それを使って createStore
関数を定義します。
import { createStore as reduxCreateStore, applyMiddleware, combineReducers } from 'redux';
import logger from 'redux-logger';
import counterReducer from './modules/Counter';
export default function createStore() {
const store = reduxCreateStore(
combineReducers({
counter: counterReducer,
}),
applyMiddleware(
logger,
)
);
return store;
}
コンポーネント
コンポーネントを定義します。コンテナからマッピングされた dispatch と state を使います。
import React from 'react';
export default class Counter extends React.Component {
render() {
const { increment, decrement, counter } = this.props;
return(
<div>
<h2>カウンター</h2>
<button onClick={increment}>increment</button>
<button onClick={decrement}>decrement</button>
<div>
Count: {counter.count}
</div>
</div>
)
}
}
Container の定義
コンテナを定義します。dispatch に必要な ActionCreator は module で定義されているのでそれを使います。
import { connect } from 'react-redux';
import * as counterModule from '../modules/Counter';
import Counter from '../components/Counter';
const mapStateToProps = state => {
return {
counter: state.counter,
}
}
const mapDispatchToProps = dispatch => {
return {
increment: () => dispatch(counterModule.increment()),
decrement: () => dispatch(counterModule.decrement()),
}
}
export default connect(mapStateToProps, mapDispatchToProps)(Counter);
Redux を組み込む
最後に Redux を組み込んで、コンポーネントを表示させましょう。
index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import registerServiceWorker from './registerServiceWorker';
import { Provider } from 'react-redux';
import configureStore from './configureStore';
const store = configureStore();
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
registerServiceWorker();
App.js
import React, { Component } from 'react';
import './App.css';
import Counter from './containers/Counter';
class App extends Component {
render() {
return (
<div className="App">
<Counter />
</div>
);
}
}
export default App;
雑感
ファイル構成がすっきりしていい感じになったように思います。以降このスタイルを使っていきたいと感じました。
以上。
こちらの記事を参考に、Ducksの理解を深めることができました。
ありがとうございます。
一点確認したい点があり、
【modules/Counter.js】
の
“`
const initialState = {
state: 0
}
“`
部分ですが、
“`
const initialState = {
count: 0
}
“`
ではないでしょうか。