🎉React向け状態管理ライブラリtypelessの1.0.0リリースされました🎉


2019/10/24: [入門記事](https://qiita.com/sisisin/items/7900685f8155864c8c71) 書きました

# typelessとは

> typeless is a toolkit for building React apps using Typescript, and RxJS.

（[こちら](https://typeless.js.org/introduction/quick-start)より引用）

typelessとは、Reactのhooks APIを利用し、Redux+redux-observableライクなAPIでTypeScriptフレンドリーに状態管理を実現するライブラリです


（過去に書いた紹介記事: [typelessというReact向け状態管理ライブラリがめっちゃいい](https://qiita.com/sisisin/items/1ee2c798309da614c648)）

先日1.0.0が正式にリリースされてAPIも変わったので紹介しようと思います

サンプルリポジトリ
- index.tsxにカウンターアプリ書いたやつ: https://github.com/sisisin-sandbox/simple-typeless/blob/master/src/index.tsx
- フォルダ構造もうちょいちゃんとしてユニットテスト少し書いたやつ: https://github.com/sisisin-sandbox/try-typeless-ut

＊概ねアルファ版触った人向けの内容になってます（触ったことない人向け記事は気が向いたら書くかもしれない）

# アルファ版との違い

hooksベースのAPIや、Action定義からそのreducer,epicをタイプセーフ・アノテーションなしで書けるという思想自体には変化大きな変化はないですが、ボイラープレートコードの削減などを実現しています

- 脱Reduxした
- Actionのnamespaceが文字列からsymbolになった
- グローバルのinterface拡張が不要になった
- ActionとStateが必要な分だけ定義ができるようになった
- Stateを参照するためのAPIがGlobalのStateではなく個別のStateを参照するように変わった
- FormとRouter（！）が提供されるようになった

## 脱Reduxした

しれっとRedux依存がなくなりました
結果として中身が全部自前で実装されています（マジか）
Reduxを使わなくなったことにより、Symbolを利用したり、`No RootAction, RootEpic, RootState or other helper types.`([BaseConcept](https://typeless.js.org/introduction/motivation)より引用)を実現しやすくなった、というのが使ってみたり実装を読んでみての感想です。

読めない規模ではないので、使うときにバグ踏んでもまあなんとかなるんちゃうか、という気持ちではあります

ただ、Redux周りのツール（redux-dev-toolなど）が使えなかったりするのでそこはなんともですね。。


## Actionのnamespaceが文字列からsymbolになった

これによって `Action` のnamespace衝突がなくなりました。
多分それだけ。

## グローバルのinterface拡張が不要になった

↓が不要になりました（[参考](https://github.com/sisisin-sandbox/simple-typeless/commit/43bdacbb2654144e215b7561c6a2192c10c9ea47#diff-38682f47b0cf12c516bc3714fc45d0fdL16)）

```tsx
declare module 'typeless/types' {
  interface DefaultState {
    count: CounterState;
  }
}
```

アルファ版ではいくつかのAPIが全体のStateを取得できるようになっていました（`useMappedState` や、 `epic`に渡すhandler関数の第二引数にいる `getState`など）
ここで全体の `State` を参照するために `DefaultState` を拡張する記述が必要だったのですが、今回のバージョンアップで全体の `State` を参照するデザインではなくなった[^1]ために不要になったものと思われます


## ActionとStateが必要な分だけ定義ができるようになった

`createModule`関数の `withState` , `withActions`を利用することで必要なときにだけ `State` や `Action` を定義できるようになりました
また、 `epic` , `reducer` の登録も任意になり、必要なものを必要な分だけ定義出来るようになっています

```tsx
const [useCounterModule, CounterActions, getCounterState] = createModule(CounterSymbol)
  .withActions({
    increment: null,
  })
  .withState<CounterState>();

useCounterModule
  .reducer({ count: 0 })
  .on(CounterActions.increment, state => {
    state.count++;
  });

const CounterModule = () => {
  useCounterModule();
  return <CounterView />
}
```

以上の例以外にも、 `reducer` 不要な場合（特定の `Action` を受けて別の `Action` を発行する `epic` のみ定義したいなど）ときは `withState` を利用せずに `epic` と `Action` だけを定義といった風に出来ます

## Stateを参照するためのAPIがGlobalのStateではなく個別のStateを参照するように変わった

`Epic` や `Component`で `State`を参照したいときは `createModule().withState()`で取得できる `StateGetter` を利用するようになりました


```ts:Stateの定義
const [useCounterModule, CounterActions, getCounterState] = createModule(CounterSymbol)
  .withActions({
    increment: null,
    decrement: null,
    reset: null,
    alertCount: null,
  })
  .withState<CounterState>();

```

```tsx:epicでの例
useCounterModule.epic().on(CounterActions.alertCount, () => {
  const { count } = getCounterState();
  alert(`count: ${count}`);
  return Rx.empty();
});
```

```tsx:componentでの例
// 略
  const { count } = getCounterState.useState();
  return (
    <div>
      <div>{count}</div>
    </div>
  );
}

```

component内ではhooksAPI（ `StateGetter#useState` ）経由で取得するようになっています
epicの方では `StateGetter`をそのまま関数として呼び出すことで取得できます
複数の `StateGetter`を参照する場合は `useMappedState` や `createDeps`を利用することでいい感じに書けます。気になる方は公式Docまで。

## FormとRouter（！）が提供されるようになった

typelessのライフサイクルで `Action` が提供される Form , Router がパッケージとして公開されました
Router は機能が全然足りてないので正直現時点では使いものにならないと思います
現状は3rdのRouterライブラリと組み合わせるほうが現実的

参考：naviと組み合わせた例 https://github.com/watiko/typeless-navi

Formはまだ使ってみてないのでなんとも。


# 個人的に触ってて気になった点

## `createModule`の返り値を分割代入した変数を別のファイルから参照したとき、変数を使ってる場所をエディタで出せないので不便

まあこれ多分typelessじゃなくてTypeScriptの問題なんですけども。。

[参考](https://stackoverflow.com/questions/56196057/why-typescript-peek-references-that-array-desturctured-variable-dosent-work)

```tsx:interface.ts
export const [useCounterModule] = createModule(CounterSymbol);
```

のように作った `useCounterModule`を別ファイルから利用したとき、 `useCounterModule`を利用してる場所一覧を取れなくなります（VSCodeでtsserverのlogにも出てないのでおそらくtsserverが変なんだと思うんですが。。自分だけだったらごめんなさい）

ワークアラウンドとして、↓みたいに回避してます。つらい。

```tsx:interface.ts
const mod = createModule();
export const useCounterModule = mod[0]
```

## Epicのテストがむずそう

[このへん](https://github.com/sisisin-sandbox/try-typeless-ut/blob/master/src/features/counter/module.test.tsx#L12)

`Epic` クラスのインスタンス( `createModule(sym)[0].epic()` で取得できる値)から特定の `epic` を直接テストするための使い勝手の良いアクセス手段が用意されていないようです。
そのため、特定の `epic` をテストするためにパッとはかけないような手順を踏まないとテストが書けません

```ts
it('epic', () => {
  const epic = e.handlers.get(fooSymbol)!.get((FooActions.bar() as any).type[1])![0];
  expect(epic(undefined, undefined as any, undefined)).toStrictEqual(FooActions.baz('barbar'));
});
```

~~なんだこの地獄~~

もしかしたらEpicについては単体のhandlerをテストするより、もう少し大きい範囲でのテストを書くことを想定してるのかもしれません
[typeless本体に書かれているテスト](https://github.com/typeless-js/typeless/blob/master/packages/typeless/__tests__/custom-scheduler.test.tsx#L80)も `Epic` 絡みのテストは `Module` を `render` させて `Epic` が動いた結果アプリケーションの状態が変化したことをテストしています

流石にめんどいのでもうちょいなんとかする方法が欲しいところ。

## Component内で状態を取得するのに `getState.useState()` を使わないといけないけど、 `getState()`でもコンパイルエラーにならない

```tsx
export const Hoge = () => {
  const { count } = getCounterState(); // ←これでもコンパイルエラーにはならないが、 `count` の値は更新されない・・・

  return {count}
}
```

↑の例のように、 `getCounterState()`を実行してしまうと実行時の値が1度だけ取得されるだけになってしまう
正しくは `getCounterState.useState()`を実行しなければならないが、これを強制させる手段がないため、実行するまでわからない
割と使い慣れないとハマりそうでちょっとヤな感じがしますね

# おわりに

アルファ版での良さに更に磨きがかかったという感触です
みなさん是非使っていきましょう

[^1]: 「[Stateを参照するためのAPIがGlobalのStateではなく個別のStateを参照するように変わった](#Stateを参照するためのAPIがGlobalのStateではなく個別のStateを参照するように変わった)」の項目を参照のこと