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をかますようにしている。
スクリーンショット 2017-08-22 16.24.16.png

ビルド

GruntやGulpは使わずに、NPM ScriptでWebapckを走らせる構成。
WebpackでBabelやらPostCSSやらを実行する。

FuseBoxとか使ってみたい...

実装例

この構成で実装するならこんな感じというのをざっと書いてみる
例えばToDo作るならこんな感じかな(動作未検証)。

modules(Moducks)

modules/todo.js
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)

selectors/todo.js
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

containers/TodoContainer.js
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から渡されたtodocreateTodoを使ってよしなに。

テスト

テストにはJestを利用する。

Moducks

構成

基本的にはModucks公式サンプルのテストと同じような構成で。
Moducksモジュールに対して、1モジュール1テストファイル。ActionCreator, Reducer, Sagaのテストを1ファイル内に書く。
構成はこんな感じ。

todo.test.js
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マジ神ですわ。
メンバーにも好評なのでしばらくはこの構成でいこうと思う。