typelessに入門する

# typelessとは

Reactで利用するためのfluxライブラリ
TypeScriptフレンドリーで、「type annotation less」にアプリケーションが書けることを標榜している

React hooksによって様々な機能を実現・提供しているので、Reactでのみ利用できる

特徴として、

- TypeScriptフレンドリーに作られており、型アノテーションなしに型安全を実現できる
- ActionCreator,Reducer,Epic(SideEffect操作)を1ライブラリですべて取り扱える
- ActionCreator+Reducer+Epicを1featureと言う単位で取り扱い、このfeatureはReact hooksを使ってReactComponentとして表現される
- モジュールはモジュールComponentがマウントされている間だけ有効化されており、明示的にマウントしていないモジュールはReducerやEpicが動かない

まあ大体Reduxの抱える課題をまるっと解決してTypeScriptで安全にアプリケーションを書けるというライブラリとなっている
このtypelessというライブラリについて、[ReduxのBasicチュートリアル](https://redux.js.org/basics/basic-tutorial)を参考に、Todoアプリをお題に基本的な使い方を解説をしようと思う

---

### Moduleの定義

typelessではReducer/Epicを作るためにまず `createModule` 関数で `ModuleHandler` を定義する必要がある
ここで定義した`ModuleHandler` にReducer/Epic処理を登録していくことになる
ちなみにこの `ModuleHandler` は内部的に `Store` というクラスのインスタンスを持っていて、そこにReducer,Epic処理を持つという仕組みになっている

`ModuleHandler` は React hooksになっていて、React Component内でのみ呼び出すことが可能
そして、 **Componentとしてマウントされている間のみReducer,Epicは有効となる**

```tsx
const TodosSymbol = Symbol('todos');

const [useTodosModule] = createModule(TodosSymbol);

// Reducer,Epic登録
// ...

// このTodosModuleコンポーネントがマウントされている間のみTodosModule内のReducer/Epicが有効
const TodosModule = () => {
  useTodosModule()

  return <div>some todo views</div>;
};
```
---


### Actions,ActionCreators

Action及びActionCreatorを定義する
typelessでは `createModule` 及び `withActions` 関数を使ってこれらを定義する
`withActions` を呼び出すと返り値の配列にActionCreatorが含まれるようになる

Reduxチュートリアルで定義されていた `addTodo` , `toggleTodo` , `setVisibilityFilter` に加えてAPIからTodo一覧を取得するためのActionも定義してみる

```diff
const TodosSymbol = Symbol('todos');


-const [useTodosModule] = createModule(TodosSymbol);
+const [useTodosModule, TodosActions] = createModule(TodosSymbol)
+  .withActions({
+    $mounted: null,
+    fetchTodo: null,
+    fetchTodoFulfilled: (todos: Todo[]) => ({ payload: { todos } }),
+    addTodo: (text: string) => ({ payload: { text } }),
+    toggleTodo: (idx: number) => ({ payload: { idx } }),
+    setVisibilityFilter: (filter: VisibilityFilter) => ({ payload: { filter } }),
+  });
+
+type VisibilityFilter = 'SHOW_ALL' | 'SHOW_COMPLETED' | 'SHOW_ACTIVE';

```

`withActions` は、引数のオブジェクトのkeyにActionの名称、valueにActionCreator関数の実装を渡す
上の例だと、 `addTodo` というActionは `string` を引数にとって、 `{ payload: { text: string } }` というオブジェクトを返すActionCreatorによって生成される、という形になる

実際に利用する際は `TodoActions.addTodo` 関数を呼び出すことによってActionオブジェクトを得られる
このとき、ActionTypeはtypeless側で補完してくれて、 `createModule` に渡したSymbolを元に一意なActionTypeが生成される

```ts:実際に生成されるActionの例
console.log(TodosActions.addTodo('hoge');
// => {payload: {text: 'hoge'}, type: [Symbol('todos'), 'ADD_TODO']}
```


---

### Reducers

#### Stateの定義

`createModule` で `TodosModule` におけるReducerで利用するStateの型を定義し、 `StateGetter` を取得する。
`withState<T>` メソッドを呼び出すと返り値の配列に `StateGetter` が含まれるようになり、Reducerが利用できるようになる

```diff
-const [useTodosModule, TodosActions] = createModule(TodosSymbol)
+const [useTodosModule, TodosActions, getTodosState] = createModule(TodosSymbol)
  .withActions({
    $mounted: null,
    fetchTodo: null,
    fetchTodoFulfilled: (todos: Todo[]) => ({ payload: { todos } }),
    addTodo: (text: string) => ({ payload: { text } }),
    toggleTodo: (idx: number) => ({ payload: { idx } }),
    setVisibilityFilter: (filter: VisibilityFilter) => ({ payload: { filter } }),
- });
+ })
+ .withState<TodosState>();

type VisibilityFilter = 'SHOW_ALL' | 'SHOW_COMPLETED' | 'SHOW_ACTIVE';


+interface Todo {
+  text: string;
+  completed: boolean;
+}
+interface TodosState {
+  todos: Todo[];
+  visibilityFilter: VisibilityFilter;
+}

```

この `Todos` に関する状態を取得するときは、ここで得られた `getTodosState` を利用することになる

#### Actionに対してのReducer/Epic処理を定義する

NOTE: API処理は↓を使うものとする

```API.ts
import { Todo } from 'features/todos/interface';

let todos: Todo[] = [{ text: 'initial todo', completed: true }, { text: 'initial todo2', completed: false }];

const sleep = () =>
  new Promise(done => {
    setTimeout(() => done(), 300);
  });

export const getAllTodos = () => sleep().then(() => todos);

export const addTodo = async (text: string) => {
  const todo: Todo = { text, completed: false };
  todos = [...todos, todo];
  await sleep();
};

export const toggleTodo = async (idx: number) => {
  todos = todos.map((todo, i) => {
    if (i === idx) {
      return { ...todo, completed: !todo.completed };
    } else {
      return todo;
    }
  });
  await sleep();
};
```

##### Reducer

typelessのReducerは一般的なReducerと同様、 `initialState` と任意のActionに対しての状態更新処理関数の実装によって定義される
このReducer処理はReduxなどのように純粋関数でなければならない（副作用を発生させてはならない）

Reducer定義は `.reducer(initialState)` 関数で初期状態を定義、 `.on`関数で、第一引数に指定したActionのときに、どのように状態を更新するかを定義する

```ts
const initialState: TodosState = {
  visibilityFilter: 'SHOW_ALL',
  todos: [],
};

useTodosModule
  // ここで初期状態を定義する
  .reducer(initialState)
  // ここで任意のActionに対する状態更新処理を定義する
  .on(TodosActions.setVisibilityFilter, (state, { filter }) => {
    state.visibilityFilter = filter;
  })
```

上記の例では `visibilityFilter`が `SHOW_ALL`, `todos` が `[]` という初期状態を `useTodosModule.reducer(initialState)` の呼び出しによって定義、続く `on` 関数で任意のActionがdispatchされたときの状態更新処理を記述している

注意点として、 [immer](https://github.com/immerjs/immer)　を利用しているので `state` の更新処理は **mutableな変更** でなければならない
immutableな変更をしたい場合は `replace` 関数を利用することになる

##### Epic

typeless における副作用はEpicで行う
EpicもReducer同様に `.on` 関数を利用して、指定したActionに対して実施する副作用関数を定義していくことになる

この副作用のあとに新しくActionを発火したい場合は、ハンドラ関数の返り値にActionをreturnしてやれば良い
また、このハンドラ関数はPromise,Observableを返り値として取れるので、 `Promise<Action>` や `Observable<Action>` を返すことも可能
この副作用関数の処理の後に特にActionを発火したくないという場合は、 `null` | `Promise<null>` | `Observable<null>` を返せばよい

```ts
useTodosModule
  .epic()
  .on(TodosActions.fetchTodo, async () => {
    const todos = await API.getAllTodos();
    return TodosActions.fetchTodoFulfilled(todos);
  })
```

##### Todo一覧をAPIから取得し、追加・変更が出来るようにReducer,Epic を追加する

まずEpicから

```ts:Epic全体像
useTodosModule
  .epic()
  .onMany([TodosActions.$mounted, TodosActions.fetchTodo], async () => {
    // todoを全件取得してfetchTodoFulfilled Actionを発火させる
    const todos = await API.getAllTodos();
    return TodosActions.fetchTodoFulfilled(todos);
  })
  .on(TodosActions.addTodo, async ({ text }) => {
    // todoを追加して、追加が済んだらfetchTodo Actionを発火させて一覧を最新のものにする
    await API.addTodo(text);
    return TodosActions.fetchTodo();
  })
  .on(TodosActions.toggleTodo, async ({ idx }) => {
    // addTodoと同じく変更・一覧更新をする
    await API.toggleTodo(idx);
    return TodosActions.fetchTodo();
  });
```

`.onMany` という関数で複数のActionを待ち受ける事ができるので、一覧取得処理をここで一括で定義している
また、 画面初期ロード時に一覧が取得したいので `$mounted` という組み込みActionを利用している。これはモジュールのマウント時に自動的に発火させてくれる
よくあるパターンだが、initializeのようなActionを手で発火させる必要がなくて便利

注意点として、 `withActions` 関数で **明示的に定義されていないと発火されない** という点が挙げられる。
どうも全ての組み込みActionが勝手に発火すると開発者ツールで見れるログが非常にやかましくなるので抑制する作りになっているっぽい


次にReducer

```ts:Reducer全体像
const initialState: TodosState = {
  visibilityFilter: 'SHOW_ALL',
  todos: [],
};

useTodosModule
  .reducer(initialState)
  .on(TodosActions.setVisibilityFilter, (state, { filter }) => {
    state.visibilityFilter = filter;
  })
  .on(TodosActions.fetchTodoFulfilled, (state, { todos }) => {
    state.todos = todos;
  })
  .on(TodosActions.addTodo, (state, { text }) => {
    state.todos.push({ text, completed: false });
  })
  .replace(TodosActions.toggleTodo, (state, { idx }) => {
    return {
      ...state,
      todos: state.todos.map((todo, i) => {
        if (i === idx) {
          return { ...todo, completed: !todo.completed };
        } else {
          return todo;
        }
      }),
    };
  });
```

`addTodo` と `toggleTodo` はAPIで一覧を取得するのだから必要ないと思われるかもしれないが、 ~~説明の都合上仕方なく~~API呼び出しを待たずに画面に反映することでUXを向上させるという試みで入れてある

先述した `.replace` を`toggleTodo` で利用しているが、配列操作をmutableに書くと読みにくい場合なんかに使い分けたり、 `initialState` をセットし直したい、といった場合に便利なAPIとなっている

---

#### Moduleを分割・・・しない

ReduxチュートリアルではReducerを分割してるけど正直あまりいい例じゃないと思うので一旦なしで

typelessにおける module の分割をしたければ、別途 `createModule` を利用して新しい `ModuleHandler` を作ってあげれば良い
多くのSPAでは1つのURLに対して1モジュールになると思う

具体例は公式のexapmlesなどを参照ということで
https://typeless.js.org/introduction/examples

---

### RegistryとStore

#### Store

typelessにおけるStoreはRedux同様dispatch関数とstateを保持して発火されたActionに応じて必要なら自身のstateを更新する役割を持つ
Reduxとは異なる点としては

- `createModule` によって生成される1単位である
    - つまりアプリ内で複数生成されることになる
- 有効・無効の概念があり、ReducerやEpicの処理は有効化されている間にのみ働く
- Epicを内包しており、Storeに紐付いた副作用に関する処理も紐付いている

#### Registry

RegistryはtypelessにおけるRedux Storeみたいなもので、全ての状態を中央集権的に管理するクラス
各 `ModuleHandler` （ここまでの例だと `useTodosModule` ）　で定義されたReducerはこの `Registry` に登録されることになる
後述する `DefaultTypelessProvider` を利用する場合は特に利用者側で触る必要はない

#### Actionをdispatchする

typelessのRegistryもdispatch関数を持っていて、ユーザーが自由にこの関数を叩くことで明示的にdispatchを呼ぶことが出来る
ただし、Registryにはsubscribe関数は用意されていないので、もしsubscribeをしたい場合はStoreインスタンスを取得する必要がある

```tsx:自前でActionをdispatchしてみる例
// TodosModuleによって定義されたStoreインスタンスを取得し、状態更新をsubscribeする
defaultRegistry.getStore(TodosSymbol).subscribe(() => {
  console.log(getTodosState());
});

export const TodosModule = () => {
  // モジュールの有効化
  useTodosModule();

  // Actionをdispatchする
  defaultRegistry.dispatch(TodosActions.addTodo('hoge'));
  defaultRegistry.dispatch(TodosActions.toggleTodo(0));

  return null;
};

```

---

### Data Flow

typelessもRedux同様厳密な単一方向データフローになっている（Fluxなんだからそれはそう
Storeは各々独立に以下のデータ更新サイクルに基づいた振る舞いをする

- Module Componentがマウントされる
- 初期Stateがセットされる
- Actionのdispatchを待ち受ける
    1. なんらかのActionがdispatchされる
    2. 発火されたActionに対応するReducer/Epic に登録されたハンドラを実行する
        - ReducerであればStoreの持つ状態を更新する
    3. 1,2を繰り返す 
- Module Componentがアンマウントされる
    - dispatchの待ち受けを解除する

---

### Componentでこれらの状態を参照する

typelessではComponentから各種Storeの状態を参照したりActionをdispatchする場合、React hooksとして提供されているAPIを利用することになる

- `useMappedState(StateGetter[], (...state) => state)`
    - 引数に渡した `StateGetter` から状態を取得する
    - mapStateToPropsみたいなもん
- `useActions(ActionCreator)`
    - 引数に渡した `ActionCreators` をdispatchする関数を取得する
    - mapDispatchToPropsみたいなもん

このAPIを利用したComponentはReduxでいう `ContainerComponent` に相当する働きをする

#### TodoアプリのComponent設計

- VisibleTodoList
    - visibilityFilterの状態に応じて、現在表示すべきtodo一覧を出す
- AddTodo
    - input要素に与えられたテキストを元に新たなTodoを追加する
- VisibilityFilters
    - VisibilityFilterを操作する
- TodosView
    - 上記3つのコンポーネントを配置した、Todoに関するRootコンポーネント
- TodosModule
    - TodosViewのために必要なTodosModuleを有効化するコンポーネント
- App
    - このTodoアプリのルートコンポーネント
    - TypelessProviderを記述している

#### VisibleTodoList

- `useMappedState` でvisibilityFilterに応じたtodoを取得してComponentで描画
- 各Todoの要素には `onClick` で状態をtoggleする処理をもたせる


```tsx
const VisibleTodoList = () => {
  const todos = useMappedState([getTodosState], ({ todos, visibilityFilter }) => {
    switch (visibilityFilter) {
      case 'SHOW_ALL':
        return todos;
      case 'SHOW_COMPLETED':
        return todos.filter(t => t.completed);
      case 'SHOW_ACTIVE':
        return todos.filter(t => !t.completed);
    }
  });
  const { toggleTodo } = useActions(TodosActions);

  return (
    <ul>
      {todos.map(({ text, completed }, i) => (
        <li
          key={i}
          onClick={() => toggleTodo(i)}
          style={{
            textDecoration: completed ? 'line-through' : 'none',
          }}
        >
          {text}
        </li>
      ))}
    </ul>
  );
};

```


#### AddTodo

- formのsubmit時にinput要素の値を `addTodo` Actionの引数として渡してやるだけ

```tsx
const AddTodo = () => {
  const input = React.useRef<HTMLInputElement>();
  const { addTodo } = useActions(TodosActions);
  return (
    <div>
      <form
        onSubmit={e => {
          e.preventDefault();
          if (!input.current!.value.trim()) {
            return;
          }
          addTodo(input.current!.value);
          input.current!.value = '';
        }}
      >
        <input
          ref={node => {
            input.current = node!;
          }}
        />
        <button type="submit">Add Todo</button>
      </form>
    </div>
  );
};

```

#### VisibilityFilters

- `FilterLink` コンポーネントは、クリックされたときに指定された `VisibilityFilter` について `setVisibilityFilter` Actionを発火する
- 現在利用されているFilterと指定された `VisibilityFilter` が同じだったらボタンではなく単なるテキストを表示する
 `VisibilityFilters` コンポーネントは、 `FilterLink` をフィルター種別分だけ列挙しているだけ

```tsx
const FilterLink: React.FC<{ filter: VisibilityFilter }> = ({ children, filter }) => {
  const { setVisibilityFilter } = useActions(TodosActions);
  const { visibilityFilter } = useMappedState([getTodosState], state => state);
  if (visibilityFilter === filter) {
    return <span>{children}</span>;
  }

  return (
    <button
      onClick={e => {
        e.preventDefault();
        setVisibilityFilter(filter);
      }}
    >
      {children}
    </button>
  );
};

const VisibilityFilters = () => {
  return (
    <p>
      Show:<FilterLink filter="SHOW_ALL">ALL</FilterLink>
      {', '}
      <FilterLink filter="SHOW_ACTIVE">Active</FilterLink>
      {', '}
      <FilterLink filter="SHOW_COMPLETED">Completed</FilterLink>
    </p>
  );
};

```


#### App

- typelessのProviderを記述している
    - DefaultTypelessProviderを利用する場合はregistryは組み込みの `defaultRegistry` が利用されている
    - defaultRegistryはimportして普通に使うことも可能

```tsx
export const App: React.FC = () => {
  return (
    <DefaultTypelessProvider>
      <TodosModule />
    </DefaultTypelessProvider>
  );
};

```

ちなみに自前でregistryをインスタンス化する場合はこう

```tsx
const registry = new Registry();

export const App: React.FC = () => {
  return (
    <TypelessContext.Provider value={{ registry }}>
      <TodosModule />
    </TypelessContext.Provider>
  );
};
```

*余談
dispatchにmiddleware相当の処理を挟みたい場合は、このRegistryクラスを継承してdispatch関数をoverrideした自前Registryクラスのインスタンスを渡してあげれば良い

#### Appをレンダリング

いつものindex.tsx

```tsx
import React from 'react';
import ReactDOM from 'react-dom';
import { App } from './App';

ReactDOM.render(<App />, document.getElementById('root'));
```

#### TodoアプリのComponent設計についての考察

今回作った `VisibleTodoList` , `AddTodo` , `VisibilityFilters` コンポーネントらはどれも typeless moduleに依存するContainerComponent相当の作りになっている

これらをmoduleに依存しない Presentational Components として実装しなくてよいのか？という点については、自分は必要ならそのとき分割すればよい、と考えている
基本的にはよっぽど汎用的なコンポーネント（例えばボタンとか）の粒度じゃなければContainerComponentとして書いてしまっても問題がなかろうという考えで、理由としては

- 分割したいときに、苦労しないで済む
    - hooksの制約上、if文などの中で呼び出しができない = Componentのトップレベルで、しかも基本的にコンポーネント処理の最初に記載しなければならない
    - この制約によって、いざ分割がしたいと思った場合、該当部分を切り取って親子関係に分離するだけで完了する
    - typelessを利用する場合、TypeScriptを利用するのがほぼ前提なので、リファクタ時に静的型の恩恵を受けるのは前提となる
        - 静的型の支援があれば一般的にはリファクタ時に事故りにくい
- テスタビリティにも影響が少ない
    - ContainerComponentをテストしようと思ったらそもそも大変
    - typelessでテストをする場合、テストコード内でProviderを定義してやれば十分テスト可能
        - ボタン押下でAction発火のテスト→`Registry#dispatch` をspyしてやればよい
        - 指定した状態におけるレンダリングのテスト→PresentationalComponentとして分離されていたとしても渡すべき状態が減るわけじゃないので一緒
- Storybookで運用する際に問題になるのでは？
    - テストのところでも述べたような理由で問題にならない
        - Storybook内で利用するためのヘルパ関数（TypelessProviderでラップする関数）を用意してやればよい
    - むしろreducerを操作すれば任意の状態を再現できるので便利
        - `ModuleHandler#reducer` で初期状態をセットできるが、これを各storyで叩いてやれば自由に状態を定義できる

以上の理由でContainerComponentどんどん使っていく、で良いと思っている


---

# おわりに

というわけでtypeless入門解説でした
ユニットテストやStorybookでの運用方法、実装パターン集とかはまたそのうちやりたい

今回のサンプル実装を置いたリポジトリはこちら↓
https://github.com/sisisin-sandbox/sample-typeles-todo/tree/master/src