Nextremer Advent Calendar 2017の2日目。
一年ほどReact/Redux使って開発してきたので、ここらで一度現在のReact/Redux環境周りを晒してみる
歴史
2016/4〜
流行りにのって、React/Reduxを採用
この頃はMiddlewareにはredux-thunkを利用
2016/6〜
激しくスパゲティ化してしまい、thunkでの運用に限界を感じたため、thunkの利用を諦め、redux-sagaに鞍替え
この頃から複数のプロジェクトでReact/Reduxの運用を開始
2017/6〜
Reduxの通常構成(Action, Reducer, Saga)の冗長さに辟易していたので、Ducksパターンの採用を検討。
その際、Moducksという素晴らしいライブラリを発見したため、これを採用した。
同時に、Reducerから取得した値をViewで加工するのにもうんざりしていたため、reselectも合わせて採用。
いまここ
最新のReact/Redux周りの構成
使用ライブラリ
フレームワーク
ビルド
テスト
ディレクトリ構成
|-- _tests__ # jest
|-- src
| |-- css
| `-- js
| |-- components
| | `-- # React Components
| |-- config
| | `-- # Configuration Files
| |-- containers
| | `-- # React-Redux Containers
| |-- modules
| | `-- # Moducks Modules
| |-- selectors
| | `-- # Reselect Selectors
| |-- utils
| | `-- # Utility Files
| |-- constants.js
| |-- index.js
| |-- reducers.js
| |-- routes.js
| |-- sagas.js
| `-- store.js
|-- package.json
`-- webpack.config.js
アーキテクチャ
さっくりと。基本的にRedux周りはすべてModucksに閉じ込める。Moducks->Viewには適宜Selectorをかますようにしている。
ビルド
GruntやGulpは使わずに、NPM ScriptでWebapckを走らせる構成。
WebpackでBabelやらPostCSSやらを実行する。
FuseBoxとか使ってみたい...
実装例
この構成で実装するならこんな感じというのをざっと書いてみる
例えばToDo作るならこんな感じかな(動作未検証)。
modules(Moducks)
import { createModule } from 'moducks';
import { select } from 'redux-saga/effects';
const defaultState = {
list: [],
};
export const {
todo, sagas,
addTodo, createTodo,
} = createModule('todo', {
ADD_TODO: {
reducer: (state, { payload }) => ({
...state,
list: [...state,list, payload],
}),
},
CREATE_TODO: {
saga: function* ({ payload }) {
const { info: userInfo } = yield select(state => state.user);
return addTodo({ text: payload, userInfo, done: false });
},
},
}, defaultState);
selectors(Reselect)
import { createSelector } from 'reselect';
export const getTodo = createSelector(
[
state => state.todo.list,
state => state.user.info,
], (todoList, userInfo) => {
// 空チェックとかユーザ情報チェックとか
const list = todoList.filter(todo => todo.userInfo.id === userInfo.id);
return {
list,
incomplete: list.filter(todo => !todo.done),
complete: list.filter(todo => todo.done),
};
},
);
View/Container
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createTodo } from '~/modules/todo';
import { getTodo } from '~/selectors/todo';
@connect(state => ({
todo: getTodo(state),
}), {
createTodo,
})
export default class TodoContainer extends Component {
// 省略
}
Componentは省略。Containerから渡されたtodo
とcreateTodo
を使ってよしなに。
テスト
テストにはJestを利用する。
Moducks
構成
基本的にはModucks公式サンプルのテストと同じような構成で。
Moducksモジュールに対して、1モジュール1テストファイル。ActionCreator, Reducer, Sagaのテストを1ファイル内に書く。
構成はこんな感じ。
describe('Todo', () => {
describe('creators', () => {
// ActionCreatorのテスト
});
describe('reducer', () => {
// Reducerのテスト
});
describe('saga', () => {
// Sagaのテスト
});
});
ActionCreator
ActionCreatorのテストは、ActionCreatorメソッドを呼び出した際に適切なAction、パラメータが発行されているかのテストを行う。
addTodo
のテストは以下のような感じ。
describe('creators', () => {
it('addTodo', () => {
expect(addTodo({ text: 'hoge' })).toEqual({
type: 'hoge/ADD_TODO',
payload: { text: 'hoge' },
});
});
});
Reducer
Reducerのテストは、ActionCreatorが呼ばれた際にstateの内容が適切に更新されているかのテストを行う。
addTodo
のテストは以下のような感じ。
describe('reducer', () => {
let state;
/* 初期値のテスト(todo.listは空) */
it('defaultState', () => {
state = todo(state, ActionTypes.INIT);
expect(state).toEqual({
list: null,
});
});
/* addTodoが呼ばれた場合のテスト */
it('addTodo', () => {
state = todo(state, addTodo({ text: 'hoge' }));
expect(state).toEqual({
list: [{ text: 'hoge' }],
});
});
});
Sagaのテスト
Sagaのテストは、基本的にはredux-sagaのテストの書き方に習う。
この辺は説明長くなるのでこの記事とかで勉強してください(丸投げ
describe('saga', () => {
it('createTodo -> addTodo', () => {
const saga = retrieveWorkers(sagas).createTodo(createTodo('hoge'));
let current = saga.next();
current = saga.next({ info: { id: 1, name: 'Taro' } }); // yield select(state => state.user)
expect(current).toEqual({
done: false,
value: put(addTodo({ text: 'hoge', userInfo: { id: 1, name: 'Taro' }, done: false })),
});
current = saga.next();
expect(current).toEqual({
done: true,
value: undefined,
});
});
});
まとめ
Moducks、Selectorを導入したことでコードが格段に追いやすくなった。Moducksマジ神ですわ。
メンバーにも好評なのでしばらくはこの構成でいこうと思う。