【Reduxに疲れた人のための】Undux入門

巷のフロントエンドではReact+Reduxの技術スタックがどちゃクソ流行ってますね！

とはいえ、Reduxだとやはりどうしてもひとつアプリを作る際のボイラプレートコードの多さや、ステートの更新によってトリガされる副作用処理のハンドリングを[redux-saga](https://github.com/gaearon/redux-saga)や[redux-side-effects](https://github.com/gaearon/react-side-effect)なしではうまくやれないケースが多いです。

もし、Reduxとその周辺ライブラリのよせあつめにちょっとでもツラミを感じ始めたら、[Undux](https://github.com/bcherny/undux)を試すチャンスです。

# Unduxとは？

<img src="https://qiita-image-store.s3.amazonaws.com/0/18973/e63491fc-b6d2-3f32-cd42-3294d7e73dd9.png" width="60%" />

> Dead simple state management for React

Reactのための死ぬほどシンプルなステート管理ライブラリです。

## 特徴

- TypeScriptやFlowなどでの型付けを意識した設計。
- Reduxにおける`Action`, `Reducer`, `Dispatcher`, `Container`などの概念を必要としない。
- `get`と`set`という抽象化されたステート操作のみ。

の３つが大きなUnduxの特徴です。

使ってみると分かるのは、どちらかというとReduxよりもMobxに近いです。
学習コスト的には **Redux > Mobx > Undux** という感じではないかと感じます。

# 使ってみる
Unduxには**Store**の概念しかなく、定義したそのStoreをコンポーネントへ接続し、コンポーネントからストアに対する操作を行うというのが主な使い方になります

## Store定義
```ts
import { connect, createStore } from 'undux'

// Storeのインターフェースを定義しておく
interface Store {
  today: Date
  users: string[]
}

// Storeの初期値をセット
const store = createStore<Store>({
  today: new Date,
  users: []
})

// 接続先ストアを引数にとるHOCを作成
export const withStore = connect(store)
```

`connect`関数は命名こそreact-reduxで提供されているものと同じですが、使い方はMobxで言うところの`@inject`に近いです。

## Component定義
上で定義した`withStore`を以下のように使います。

```ts
import { withStore } from './store'

// Update the component when `today` changes
let MyComponent = withStore('today')(({ store }) =>
  <div>
    Hello! Today is {store.get('today')}
    <button onClick={() => store.set('today')(new Date)}>Update Date</button>
  </div>
)
```
`withStore`は引数に与えられたキー名を元にStoreサブスクライブするHOCを返しています。ここでは`today`のみを対象にしていますが、複数のキーを指定することも可能です。

```ts
const MyComponent = withStore('today', 'users')(({ store }) =>
  ...
)
```
これは`Lensed connect`と呼ばれる機能で、サブスクライブ対象のキー以外の更新はコンポーネントの描画をトリガしません。つまり、ひとつめのコンポーネント定義のように`today`のみを引数に与えている場合は、`users`の変更によってコンポーネントが描画されないということです。

**これだけでUnduxは使えます!**

（もちろん[ステートフルコンポーネントと一緒にも使えます](https://github.com/bcherny/undux#stateful-component-with-props)）

# その他の機能
Unduxは上で説明されたような基本的な機能に加えて

- 副作用処理をハンドリングするための**Effect**
- ミドルウェアに相当する**Plugin**

のふたつが最小限備わっています。

## Effect
UnduxのEffectは、MobxのReactionという機能に近いもので、**ステートの変更によってトリガされるアクション（副作用）**を定義できる機能です。

フロントエンド・アプリケーションを作っていると必ずと行っていいほど遭遇するのが、非同期処理です。UnduxのEffectを使うと、RxJSの記法を用いて効率的に非同期処理を扱うコードを書くことができます。

たとえば、以下は`userFetchingStatus`の状態をサブスクライブし、外部のAPIへのデータの取得を行うためのEffectの実装です。

```ts
  store
    .on('userFetchingStatus')
    .filter(s => s === FETCHING_STATUS.STARTED)
    .subscribe(() => {
      UserRepository.getAll().then((users: I.List<User>) => {
        succeededFetchingUsers(users);
      }).catch(() => {
        failedFetchingUsers();
      });
    });
```
Effectの存在によって、アクションの基点がかならずしもビューを経由したユーザーからの操作だけにとどまらなくなります。 

このような副作用ハンドリングがないと、ステートのマッピング先になるビューで条件分岐を記述しなければいけないため、ロジックがビューに散らばってしまいます。
（ReduxにはこのEffectに相当する機能がないため、redux-sagaなどを補助として使う必要があります）

## Plugin
Unduxのプラグインは、HOCの形で定義します。
以下はモデルの状態をLocalStorageへシームレスに永続化するプラグインの例です。

```ts
import { createStore, Plugin } from 'undux'

let withLocalStorage: Plugin = store => {

  // モデルの変更を検知して、新しい変更をローカルストレージへ保存
  store.beforeAll().subscribe(({ key, previousValue, value }) =>
    localStorage.set(key, value)
  )

  return store;
}
```
Unduxのストアは、`beforeAll`以外にも`onAll`, `before`, `on`の4つのイベントハンドラをサポートしています。`before`や`beforeAll`はストアに対する`set`によって変更が行われる直前で走るハンドラです。

定義されたハンドラはストアを食べる形で適用します。

```ts
import { createStore, withLocalStorage } from 'undux'

const store = withLocalStorage(createStore<Store>({...}))
```

# TODOアプリ実装例
さて、Undux+Reactを用いて簡単なTODOアプリを作ってみました。
実際に動くものは以下のCodeSandboxからどうぞ。
https://codesandbox.io/s/kw1vvv5mp5

![スクリーンショット 2018-02-15 15.24.45.png](https://qiita-image-store.s3.amazonaws.com/0/18973/f3e1f58b-7b22-dc8c-a8c3-55dc4c602e8d.png)

## Model
モデルは`Immutable#Record`を使った`Todo`モデルのみです

```ts:todo.ts
import * as I from "immutable";

export default class Todo extends I.Record({
  name: "",
  isDone: false
}) {
  static setDone(todo: Todo) {
    return todo.set("isDone", true);
  }
}
```

## Store
ここでは、`createStore`関数を用いて初期状態を指定したストアの定義と、Effectの登録を行っています。

```ts:store.ts
import * as I from "immutable";
import { connect, createStore } from "undux";
import { registerTodoEffect } from "./effects/todoEffect";
import Todo from "./models/todo";

export interface AppStore {
  newTodoName: string;
  todoCount: number;
  todos: I.List<Todo>;
}

const store = createStore<AppStore>({
  newTodoName: "",
  todoCount: 0,
  todos: I.List([])
});

registerTodoEffect(store);

export const withStore = connect(store);
```
今回はストアに格納されるデータの種類が少ないため、ストアの分割をしません。

## Action
このTodoアプリでは、Actionレイヤ[^1]を導入しています。

[^1]: ReduxのActionとは全くの別物です

Unduxはストアに対する操作が抽象化されているため、コードがある程度大きくなってくると、どうしてもコンポーネントにストアに対する操作のロジックが散らばってしまいます。Actionレイヤはそれらの操作を隠蔽する責務として、ストアを食べてアクションを返すという純粋関数として位置づけます。

```ts:todoAction.ts
import { Store } from "undux";
import { AppStore } from "../store";
import Todo from "../models/todo";

export const todoActions = (store: Store<AppStore>) => {
  return {
    updateNewTodoName(name: string) {
      store.set("newTodoName")(name);
    },

    addNewTodo() {
      const name = store.get("newTodoName");
      const currentTodos = store.get("todos");
      const todo = new Todo({ name });
      const updatedTodos = currentTodos.push(todo);
      store.set("todos")(updatedTodos);
      store.set("newTodoName")("");
    },

    markTodoDone(index: number) {
      const currentTodos = store.get("todos");
      const nextTodos = currentTodos.update(index, (todo: Todo) =>
        Todo.setDone(todo)
      );
      store.set("todos")(nextTodos);
    }
  };
};
```
コンポーネント側は、ストアのデータ構造を詳しくしらなくても、アクションによって提供される関数を呼ぶだけで、ストアに対する具体的な操作ができるようになります。

## Effect
ストアの変更の副作用として、`todoCount`を更新しています[^2]。

[^2]: そもそも　`todoCount` などというステートを持たせなくても、Immutable.jsの`size`プロパティで取れますが、ここではEffectを使うために敢えてこのステートを持たせています。

```ts:todoEffect.ts
import * as I from "immutable";
import Todo from "../models/todo";
import { Store } from "undux";
import { AppStore } from "../store";

export function registerTodoEffect(store: Store<AppStore>) {
  store.on("todos").subscribe((todos: I.List<Todo>) => {
    store.set("todoCount")(todos.size);
  });
}
```

## Component
コンポーネントでは、さきほどstore.tsで定義された`withStore`関数を使って、ストアとの接続をします。

`newTodo`コンポーネントはフォームの状態のみにしか関心がないため、ストアから`newTodoName`のみをサブスクライブします。

```ts:newTodo.tsx
import * as React from "react";
import { withStore } from "../store";
import { todoActions } from "../actions/todoActions";

export const NewTodo = withStore("newTodoName")(({ store }) => {
  const { addNewTodo, updateNewTodoName } = todoActions(store);
  const todoName = store.get("newTodoName");

  return (
    <div style={{ marginBottom: 5 }}>
      <input
        type="text"
        value={todoName}
        placeholder="Put your todo here..."
        onChange={e => {
          updateNewTodoName(e.target.value);
        }}
        style={{ marginRight: 5 }}
      />
      <button
        disabled={todoName === ""}
        onClick={() => {
          addNewTodo();
        }}
      >
        Add
      </button>
    </div>
  );
});
```
一方で、`todoList`コンポーネントは、`todos`と`todoCount`の変更をサブスクライブしています。

```ts:todoList.tsx
import * as React from "react";
import { withStore } from "../store";
import { todoActions } from "../actions/todoActions";
import Todo from "../models/todo";

export const TodoList = withStore("todos", "todoCount")(({ store }) => {
  const { markTodoDone } = todoActions(store);

  const $todos = store
    .get("todos")
    .toArray()
    .map((todo: Todo, index: number) => (
      <div
        className="todolist-item"
        key={index}
        onClick={() => {
          markTodoDone(index);
        }}
        style={{
          textDecoration: todo.get("isDone") ? "line-through" : "none"
        }}
      >
        {todo.get("name")}
      </div>
    ));

  const todoCount = store.get("todoCount");

  return (
    <div>
      <div>Todo count: {todoCount}</div>
      <ul className="todolist">{$todos}</ul>
    </div>
  );
});
```

コンポーネントでは、ストアからのデータの取り出しに`get`を何度も使っていますが、もっと大規模なアプリケーションになってくると、コンポーネントで必要になるデータ構造と、ストアのデータ構造を切り離して疎結合にしたいケースがでてきます。その場合には、今回の例で導入したアクションのように、ストアを食ってコンポーネントに固有なデータ構造を返すようなレイヤを導入してみてもいいかもしれません。

# 所感
個人的にUnduxがとても良いなと思う理由は

- ボイラプレートコードが不要なので記述量が少ない。
- Undux自体のソースコードがめちゃめちゃ小さいのですぐ読める。
- 副作用のハンドリング方法がビルトインで用意されている。

の3点で、基本的にはすごく使い勝手の良いステート管理ライブラリなんではないかなと思います。僕自身、上で紹介したTodoアプリとほぼほぼ同じレイヤー区分で、すでにUnduxをReactNativeとの開発で使い始めています。

とはいえ、用意されているAPIがシンプルであるがゆえに、Reduxと比べるとアプリケーションのレイヤリングをしっかりやっていかないとすぐにコンポーネントとストアとの関係が密結合になってしまう危険性もあるような気がするので、ここは注意が必要かもしれません。開発をするチームのメンバーで具体的な設計方針をあらかじめ考えておくと、スピードとメンテナビリティを共存させた、うまいアプリケーション開発ができるようになるはずです :muscle: