状態管理関数が意外とシンプルに作れた
React を使わずに TypeScript で開発していたプロジェクトで、状態管理がしたいと思う機会がありました。
サーバに置いてすべてのクライアントで共有するわけではないから個々のクライアント側がローカルに持っていればいい。
クライアント側のコードではグローバルにいろんなファイルから特定の変数を参照したい…という状況でした。
sessionStorage
を使う方法もありますが、文字列など特定のデータしか保存できないという制約があったり、セキュリティ的に中身が見られたくないという場合もあるかもしれません。
そこで、ライブラリを使わずに状態管理をしてみたら、意外とシンプルにまとまったという話です。
変数を直接定義
まずは変数を定義して、それを export
することで対応しました。
states.ts1export let someState: string = '';2export let anotherState: number = 0;
main.ts1import { someState, anotherState } from 'states';23...45if (someCondition) {6someState = 'used';7}8...
しかし、もしこれらが同じファイル内にあれば問題ないのですが、import
された変数を書き換えることはできません。
なので書き換えるための関数を定義して、それを使って書き込みを行うことにしました。
states.ts1export let someState: string = '';23export const setSomeState = (state: string) => {4someState = state;5};67export let anotherState: number = 0;89export const setAnotherState = (state: number) => {10anotherState = state;11};
main.ts1import { someState, anotherState } from 'states';2import { someState, setSomeState, anotherState } from 'states';34...56if (someCondition) {7someState = 'used';8setSomeState('used');9}10...1112console.info(someState);
さて、プログラムは動きはするのですが…変更したはずの someState
が変更されません。
どうやら、この例において console.info
で参照されたタイミングでは import
してきた someState
が参照されるので、
初期値である ''
が渡されるだけであって、変更後のリアルタイムな値は参照されないようです。
変更が反映された値を取得するには、state の値を取得する関数を呼び出せばいいです。
states.ts1export let someState: string = '';2let someState: string = '';34export const setSomeState = (state: string) => {5someState = state;6};78export const someStateValue = () => someState;910export let anotherState: number = 0;11let anotherState: number = 0;1213export const setAnotherState = (state: number) => {14anotherState = state;15};1617export const anotherStateValue = () => anotherState;
main.ts1import { someState, setSomeState, anotherState } from 'states';2import { someStateValue, setSomeState, anotherStateValue } from 'states';34...56if (someCondition) {7setSomeState('used');8}9...1011console.info(someState);12console.info(someStateValue());
これで値の読み書きがちゃんとできるようになりました。
さて、state をまとめたコードはこのようになりました。
states.ts1let someState: string = '';23export const setSomeState = (state: string) => {4someState = state;5};67export const someStateValue = () => someState;89let anotherState: number = 0;1011export const setAnotherState = (state: number) => {12anotherState = state;13};1415export const anotherStateValue = () => anotherState;
たった 2 つだけの state なのに、無駄に長い気がしますし、どこからどこまでがひとまとまりかがわかりづらいです。
そこで React の useState
や Recoil の useRecoilValue
などのように、自分でそのような状態管理の関数を作ろうとなりました。
状態管理関数を作成
例えば Recoil はどうなっているのか考えると、
atom
は引数にデフォルト値を取り、state に関する変数を返します。
使用するときには useRecoilValue
や useSetRecoilState
の引数にその変数を指定することで、state を参照するための値やそれを変更するための関数を得ることができます。
それと同じようなことをやればいいです。
ところで、先ほどは各 state をファイルの上階層で直接定義していましたが、 今度は何が来るかわからないため、state を格納するオブジェクトや Map を作成します。
そこの各 state にアクセスするためには一意なキーが必要になります。
このへんが Recoil で atom を作成するときにグローバルにユニークなキーが必要になる理由なのかもしれません。
一応自分で指定しなくても一意になるように乱数でも生成して設定すればよいので、今回はそうしています。
stateManager.ts1type State<T> = { key: string; dflt: T };23type SetState<T> = (newState: T) => void;45type UseStateReturnType<T> = [ T, SetState<T> ];67/** すべての state を格納する Map */8const states: Map<string, any> = new Map();910const generateRandomKey = () => String(Math.random()).slice(-10);1112/** state を初期化する */13export const createState = <T>(dflt: T = null): State<T> => {14let key = generateRandomKey();15while (states.has(key)) {16key = generateRandomKey();17}18states.set(key, dflt);19return { key, dflt };20};2122export const useValue = <T>(state: State<T>): T => {23return states.get(state.key);24};2526export const useSetState = <T>(state: State<T>): SetState<T> => {27return (newState: T) => {28states.set(state.key, newState);29};30};3132export const useState = <T>(state: State<T>): UseStateReturnType<T> => [33useValue(state),34useSetState(state),35];
ジェネリクスのおかげで型推論してくれるので、使う側でも型の恩恵を受けられます。
state の定義や使用は次のようになります。
states.ts1export const someState = createState('');23export const anotherState = createState(0);
main.ts1import { useValue, useSetState } from 'stateManager';2import { someState, anotherState } from 'states';34...56const setSomeState = useSetState(someState);78if (someCondition) {9setSomeState('used');10}11...1213const someStateValue = useValue(someState);1415console.info(someStateValue);
定義も使用もコードがだいぶすっきりしました。
バリデーション
自作なので、setState するときに特定の条件を満たす値のみを受け入れる、みたいなことももちろんできます。
stateManager.ts1type IgnoreConditionFunction<T> = (state: T) => boolean;23type State<T> = { key: string; dflt: T };4type State<T> = { key: string; dflt: T; ignore?: IgnoreConditionFunction<T> };56...78/** state を初期化する */9export const createState = <T>(dflt: T = null): State<T> => {10export const createState = <T>(dflt: T = null, ignore?: IgnoreConditionFunction<T>): State<T> => {11let key = generateRandomKey();12while (states.has(key)) {13key = generateRandomKey();14}15states.set(key, dflt);16return { key, dflt };17return { key, dflt, ignore };18};1920...2122export const useSetState = <T>(state: State<T>): SetState<T> => {23const ignore = state.ignore;24return (newState: T) => {25if (ignore !== undefined && ignore(newState)) {26return;27}2829states.set(state.key, newState);30};31};3233...
states.ts1/** 1 以上 8 以下の整数と null のみ許容する */2export const exampleState = createState<number>(3null,4(state) => state !== null && (!Number.isInteger(state) || state <= 0 || state >= 9)5);
そんなにコードの記述量も多くないですが、このように状態管理をすることができました。
この記事が参考になれば幸いです。
では