React Context API with Hooks をご機嫌に使うためのライブラリ( tiny-context )を作った


React Context API with Hooks をご機嫌に使うための tiny-context というライブラリを作ったので紹介します。

背景

ちゃかちゃかと State とそれを操作する Action を定義したいけど、 Redux を使うのはちょっと腰が重い・・。
React Context API が Hooks で使えるようになってご機嫌な感じ。だけど Action を合わせて持たせようとすると微妙にハマりポイント多い・・。
React Context API with Hooks に近い感覚で State と Action のセットをお気軽にコンテキストに詰め込めるライブラリを作ってみよう・・・という感じで作りました。

利用イメージ

(少しライブラリアップデートしたのでこの記事もあわせて更新)

初めに管理したい State の型を定義します。

type CounterState = { count: number };

ライブラリが提供する createTinyContext を呼び出します。
先ほど定義した State を型引数として指定した上で、それを操作する処理をオブジェクトとして引数に渡します。
定義する action の各メソッドは 第1引数 および 戻り値 が State となる形で定義する必要があります。
カウンタアプリな例だと以下の感じです。

import { createTinyContext } from 'tiny-context';

const { Provider, useContext } = createTinyContext<CounterState>().actions({
  increment: (state, amount) => ({ count: state.count + amount })
});

これで Provider と、 useContext の Hooks が取得できます。これらは通常の Context API とほぼ同じ使用感です。

Consumerでは useContext()stateactions を内包したオブジェクトを取得し使用します。 actions は先に actions メソッドに指定した実装のうち、第1引数が取り除かれた function となっています。

具体的には以下の感じです。

const Buttons = () => {
  const { actions: { increment } } = useContext();
  return <button onClick={() => increment(1)}>+</button>;
};

const Display = () => {
  const { state: { count } } = useContext();
  return <span>{count}</span>;
};

Provider も React Context API と同じで、以下の感じで利用できます。

const CounterApp = () => (
  <Provider value={{ count: 0 }}>
    <Buttons />
    <Display />
  </Provider>
);

売りなところ

上の利用イメージの通り Context API とほぼ同じ感覚で使えるはず。Context API を使ったことある方であれば理解しやすいかと思います。
Action は State を引数にとり State 返すという形で実装するのでいい感じにテストしやすいはず。
Action は State を単純に返すもののほか、 asyncasync generator で定義することも可能で、以下の通り状態が何度か切り替わるような Action もお手軽に作れます。 redux-saga のような使い方ができるはず。。

class ActionsImpl implements InternalActions<State, Actions> {
  setLock(state: State, lock: boolean) {
    return { ...state, lock };
  }
  async *increment(state: State) { // Async Generator
    state = yield this.setLock(state, true);

    await wait(); // network delays...
    state = yield { ...state, count: state.count + 1 };

    return this.setLock(state, false);
  }
}

利用上の注意点

同一コンテキスト上の Aciton はとにかくシーケンシャルに実行されます。並行で何か処理したいという要件がある場合は Action の中でとじるように処理するか、Action の外で制御する必要があるかと思います。

fetch の非同期処理が進んでる途中でのキャンセルは AbortSignal を引数に渡すことで行ってください。 (Example)

最後に

この記事を見て 興味湧いた!応援したい! という方いましたら Github でスターいただけると励みになります。
今回初めて npm で自作ライブラリ公開したので、変なところ見つけたら指摘いただけたらと思います。