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
を利用するようになりました
const [useCounterModule, CounterActions, getCounterState] = createModule(CounterSymbol)
.withActions({
increment: null,
decrement: null,
reset: null,
alertCount: null,
})
.withState<CounterState>();
useCounterModule.epic().on(CounterActions.alertCount, () => {
const { count } = getCounterState();
alert(`count: ${count}`);
return Rx.empty();
});
// 略
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の問題なんですけども。。
export const [useCounterModule] = createModule(CounterSymbol);
のように作った useCounterModule
を別ファイルから利用したとき、 useCounterModule
を利用してる場所一覧を取れなくなります(VSCodeでtsserverのlogにも出てないのでおそらくtsserverが変なんだと思うんですが。。自分だけだったらごめんなさい)
ワークアラウンドとして、↓みたいに回避してます。つらい。
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()
を実行しなければならないが、これを強制させる手段がないため、実行するまでわからない
割と使い慣れないとハマりそうでちょっとヤな感じがしますね
おわりに
アルファ版での良さに更に磨きがかかったという感触です
みなさん是非使っていきましょう