Edited at

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

2019/10/24: 入門記事 書きました


typelessとは


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


(こちらより引用)

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

(過去に書いた紹介記事: typelessというReact向け状態管理ライブラリがめっちゃいい)

先日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より引用)を実現しやすくなった、というのが使ってみたり実装を読んでみての感想です。

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

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


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

これによって Action のnamespace衝突がなくなりました。

多分それだけ。


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

↓が不要になりました(参考)

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 の登録も任意になり、必要なものを必要な分だけ定義出来るようになっています

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 を利用するようになりました


Stateの定義

const [useCounterModule, CounterActions, getCounterState] = createModule(CounterSymbol)

.withActions({
increment: null,
decrement: null,
reset: null,
alertCount: null,
})
.withState<CounterState>();



epicでの例

useCounterModule.epic().on(CounterActions.alertCount, () => {

const { count } = getCounterState();
alert(`count: ${count}`);
return Rx.empty();
});


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の問題なんですけども。。

参考


interface.ts

export const [useCounterModule] = createModule(CounterSymbol);


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

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


interface.ts

const mod = createModule();

export const useCounterModule = mod[0]


Epicのテストがむずそう

このへん

Epic クラスのインスタンス( createModule(sym)[0].epic() で取得できる値)から特定の epic を直接テストするための使い勝手の良いアクセス手段が用意されていないようです。

そのため、特定の epic をテストするためにパッとはかけないような手順を踏まないとテストが書けません

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本体に書かれているテストも Epic 絡みのテストは Module を render させて Epic が動いた結果アプリケーションの状態が変化したことをテストしています

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


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

export const Hoge = () => {

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

return {count}
}

↑の例のように、 getCounterState()を実行してしまうと実行時の値が1度だけ取得されるだけになってしまう

正しくは getCounterState.useState()を実行しなければならないが、これを強制させる手段がないため、実行するまでわからない

割と使い慣れないとハマりそうでちょっとヤな感じがしますね


おわりに

アルファ版での良さに更に磨きがかかったという感触です

みなさん是非使っていきましょう