34
23

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

NextremerAdvent Calendar 2017

Day 2

最近のReact/Redux周り晒す

Last updated at Posted at 2017-12-01

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

34
23
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
34
23

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?