Edited at

typelessというReact向け状態管理ライブラリがめっちゃいい

typelessというreactの状態管理ライブラリが非常に良いので紹介します


What is typeless?

react+typescriptで使う前提のreduxラッパーライブラリ。reduxが抱える以下のような課題を解決する


  • 異常に多いボイラープレートコード

  • 重複した型注釈

  • 大量の依存ライブラリ

  • 欠如したコーディングガイドライン

これらの課題を踏まえて以下のコンセプトでデザインしている


  • TypeScriptフレンドリー

  • 実用に耐えうる機能を全て内包する

  • 容易なモジュール追加

  • 多くのユースケースに対する解決策をデフォルトで提供

*参考: https://typeless.js.org/introduction/motivation


reduxの抱える課題について

ドキュメントのmotivationの項で書かれていることは多くの人が課題をもっている


  • 異常に多いボイラープレート


    • actionTypeなどを頑張って定義しなきゃ使いにくかった

    • ボイラープレートや型注釈を減らすためのライブラリの隆盛





  • 重複した型注釈


    • 例えばredux-sagaでは yield を使ったときに型注釈なしに使うことがしにくい



  • 大量の依存ライブラリ


    • middlewareやrouter、その他 redux-formのようなユーティリティに依存していた



  • 欠如したコーディングガイドライン


    • reduxでは、実装をどこに持つべきか?という疑問に対して各々の開発者が自分で考える必要があった


      • とあるAPIリクエストを含んだ状態更新処理を、dispatch前にやるのか、middlewareでやるのか。状態更新処理に必要な情報をどうやって受け取るかなど

      • この議論( https://togetter.com/li/979237 )がまさにこの状況への課題感





要は、型安全に・シンプルに・簡単に実装するための道筋が足りない、ということかなと思う

自分自身、reduxでの設計は迷いが多くて苦労していたので、非常に賛同できたところだった


typelessコンセプト


  • TypeScriptフレンドリー


    • 全てがtypelessの世界観で型付けされているので、上手く型安全にやるにはどうすればいいだろうか?ということに苦心する必要がない

    • reduxはjsで動くことを前提にしているので、ここは本当にいつも悩まされているところだったので、非常に良さを感じられた


    • export type AppState = typeof store extends Store<infer S, any> ? S : never; とかを考えなくていいのほんと尊い



  • 実用に耐えうる機能を全て内包する


    • 現時点でreduxのベーシックな機能+ミドルウェア相当の機能を提供


      • reducer,action creator,map(State|Dispatch)ToProps,middleware(redux-observable相当)



    • 将来的にはformやroutingまで含めて機能を提供



  • 容易なモジュール追加


    • reduxの createStore やAngularの NgModule などのようにrootに追加する、という作業は不要

    • ただ新しい状態に関する実装を作ってそれをexportしておくだけで簡単にアプリケーションに反映される


      • パッと見で暗黙的挙動をするのでここは賛否あるかな、という気はする

      • 個人的にはblueprintを使った新規機能追加がしやすい・ちゃんと読めば明示的なので、ポジ





  • 多くのユースケースに対する解決策をデフォルトで提供


    • 新しい依存を追加しなくても十分使えるようになっている


      • reduxはmiddlewareなりの「副作用に対する解決策」をどこかしらからもってくる必要があった



    • 少なくとも自分が触った範囲では十分typelessだけの依存で使うことが出来た


      • *routerとか、実装予定のものは加味していない






個人的に非常に良いと思った設計思想


action,reducer,epicの概念が整理されている

actionはただのイベントのインターフェースで、epicがstateを組み立てるための情報を事前に処理、reducerでstateを更新という役割がすんなり理解できるような構造になっているのがとても良い

生のreduxでは、これを開発者が整理して、「この実装はここに持たせるべきだろう」というようなことを考える必要があったが、そこを考えなくて済むようになっている

こと action という事柄に関してだけ言っても、「どうactionを発火すれば良いのか?actionを発火する前に実装をもたせてしまえるじゃないか?」という議論を巻き起こしてしまっていた(Qiitaだけでも無数に記事がある)

実際、 mapDispatchToPropsにも reducerにも middleware にも処理は持ててしまう。

ちゃんとreduxなりfluxなりを考えてる人なら、「そんなのactionは最低限の情報からpayloadを作って投げるだけ」って言えるかもしれないが、個人的には悩む余地は減らしたい、と考えているのでreduxが実質どこにでも処理を持ててしまい、悩みやすいというのは良くないと常々思っていた

typelessは actioninterfaceであると称しており(サンプルコード参照)、ここから「実装を持つべきでない」ということが伝わるようになっていたりする

(実際はいくらでも実装は持ててしまうが、構造上実装を持つと変だと思えるようになっているのが大事)

悩みやすい epicmiddleware 相当)でstateを参照しちゃうのか、actionのpayloadに必要な情報含めるためにcomponentにUIと関係ない情報を流してしまうのか、という点も typeless上の actionの役割を考えたら悩むことなく実装に入れると思う


ライフサイクルAPIが提供されいてる

画面の初期化のためにroot componentの componentDidMountthis.props.initialize()と書いたことがある人、多いと思います

実際画面の初期化処理などの典型的ライフサイクルイベントはcomponentだけでなく、状態管理の観点でも欲しくなることは無数にあり、このAPIがデフォルトで提供されているのはとても良かった

以下 https://typeless.js.org/api/createactions より引用


import { createActions } from 'typeless';

const UserActions = createActions('user', {
fetchUser: (id: number) => ({ payload: { id } }),
deleteUser: null,
// lifecycle actions, optional
$mounted: null,
$remounted: null
$unmounting: null,
$unmounted: null,
});

UserActions.fetchUser(1)
// => { type: 'user/FETCH_USER', payload: { id } }

UserActions.deleteUser()
// => { type: 'user/DELETE_USER' }

このサンプルコードの $mounted などがそれにあたる。

UIの事情ではなく、アプリケーションの状態という観点で必要なAPIの提供である、という点がこれこれ、という感じだった


スターターキットの異常な充実ぶり

とりあえずこのリポジトリをcloneすればもう新規プロジェクト始められる、という次元ですべてが揃っていて異常

https://github.com/typeless-js/create-react-app-starter


本家reduxよりよっぽどElm Architecture

reduxはElmに影響を受けているというが、実際はElmよりも遥かに冗長で型安全性がなく、概念が増えて理解が難しい状態になっていた

typelessは(意識してるかはわからないが)非常にシンプルさが増していて、reduxよりもずっとElm Architectureになっていると感じる

Elm

import Browser

import Html exposing (Html, button, div, text)
import Html.Events exposing (onClick)

main =
Browser.sandbox { init = 0, update = update, view = view }

type Msg = Increment | Decrement

update msg model =
case msg of
Increment ->
model + 1

Decrement ->
model - 1

view model =
div []
[ button [ onClick Decrement ] [ text "-" ]
, div [] [ text (String.fromInt model) ]
, button [ onClick Increment ] [ text "+" ]
]

typeless(Elmとの比較のために一部の命名をElmに寄せてます)

import React from 'react';

import ReactDOM from 'react-dom';
import { createActions, createEpic, createReducer, initialize, useActions, useMappedState, useModule } from 'typeless';

export const MODULE = 'counter';

export const Msg = createActions(MODULE, {
increment: null,
decrement: null,
});

export interface CounterState {
count: number;
}

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

const epic = createEpic(MODULE);

const reducer = createReducer({ count: 0 })
.on(Msg.increment, state => {
state.count++;
})
.on(Msg.decrement, state => {
state.count--;
});

function Main() {
useModule({
epic,
reducer,
reducerPath: ['count'],
});

const { increment, decrement } = useActions(Msg);
const { count } = useMappedState(state => state.count);
return (
<div>
<button onClick={decrement}>-</button>
<div>{count}</div>
<button onClick={increment}>+</button>
</div>
);
}

const { TypelessProvider } = initialize();

ReactDOM.render(
<TypelessProvider>
<Main />
</TypelessProvider>,
document.getElementById('root'),
);

さすがにElmと比べたらコード量は多くなるけど、それでもかなり近い雰囲気で書けるんじゃないかなと思う

Elmは非常にシンプルかつ堅牢に書けるところが良さだと思っていて、その良さを限りなくTypeScriptの世界で現実的なラインで落とし込んでいるな、と感じられて非常に良い。

*上記サンプルコードは こちら


(個人的に)APIが好き

Actionをsubscribeするということをreducerもepicも .on(ActionType, ActionHandler)というインターフェースで提供していて、どちらも「このアクションをsubscribeしている」ということがとてもわかりやすい。

reduxのreducerがreduxの世界観におけるイベント発火に対する更新処理だということを理解するにはドキュメントの読み込みなり、ある程度事前知識が必要だったと思うが、typelessのこのAPIは見た目に非常にわかりやすくなっていて好ましい。 なーにが switch(action.type){} じゃ、それでどうやってpub/subモデルだと読み取れってんだこの野郎

また、middlewareの選択の自由があった関係上仕方ないことだけど reducermiddlewareactionを特定する手段が異なり、理解しにくさがあったという問題にも対処できている。

ここは All in oneの良さかなと思う

(ある程度は好みの問題だとは思う)


おわりに

ということでtypelessめっちゃいいのでぜひ使っていきましょう