状態管理ライブラリを使わずやってみよう
Hooks APIができてからあんまりReact触ってないくらいには遠ざかっていたので、初歩的な話題ではありますがグローバルのステート管理をどうするのか復習してみました。
現在はちょっと用意したいだけなら外部の状態管理ライブラリを導入しなくてもそれなりに管理できるようでしたので、Hooks APIのみでやっていきたいと思います。
管理したいデータ構造を決める
まずは、管理したい構造体を明記していきます。
ここはいったんReact非依存で書いておきます。
// Globalで管理したい構造体
export type UIState = {
readonly user? : {
name : string
mail : string
}
readonly login : boolean
readonly currentTime : number
readonly updatedCount : number
}
// 初期化ファクトリ
export const newUIState = () : UIState => {
return {
currentTime: 0, login: false, updatedCount: 0
}
}
// 変更アクセサ (interface)
type ChangeUIState = {
setUser(name : string , mail : string) : ChangeUIState
setLogin(b : boolean) : ChangeUIState
setCurrentTime(t : number) : ChangeUIState
update(): UIState
}
// 変更アクセサ (実装)
export const changeUIState = (prev: UIState) : ChangeUIState => {
return {
setUser(name: string, mail: string): ChangeUIState {
return changeUIState({...prev , user : {name , mail}})
},
setLogin(b: boolean): ChangeUIState {
return changeUIState({...prev , login : b})
},
setCurrentTime(t: number): ChangeUIState {
return changeUIState({...prev , currentTime : t})
},
update(): UIState {
return {...prev , updatedCount : prev.updatedCount +1}
}
}
}
immutableな変更メソッドを用意するのに趣味でbuilderパターンっぽくしてますが、ここは何でも良い気はします。(class使わないだのなんだの最近はいろいろあるようですが、どうせ一定規模以上はimmerとかつかうだろう、とかあるし)
管理したいデータ構造← → Reactの橋渡しを作る
import {createContext, Dispatch, ReactNode, SetStateAction, useContext, useState} from "react";
import {newUIState, UIState} from "./state";
// React.Contextで管理したい構造体
type UiStateContextItem = {
// データ構造そのもの
state: UIState;
// データ構造変更のためのuseStateなHook
setState: Dispatch<SetStateAction<UIState>>;
};
// Contextの識別子
// 初期値宣言するしかない...?
export const UIStateContext = createContext<UiStateContextItem | null>(null);
// Context使用側のHook
export const useUIStateContext = (): [
UIState,
Dispatch<SetStateAction<UIState>>
] => {
const uiCtx = useContext(UIStateContext)!;
return [uiCtx.state, uiCtx.setState];
};
// state = UiStateContextItem の初期化
const newContextItem = (): UiStateContextItem => {
const [state, setState] = useState<UIState>(newUIState);
return {
state,
setState,
};
};
// Context宣言側のHook
export const UIStateContextProvider = (props: {children: ReactNode}) => {
return <UIStateContext.Provider value={newContextItem()}>{props.children}</UIStateContext.Provider>;
};
React.Contextで管理する値には構造体そのものとは別に、更新系メソッドを設定しておきます。 つまりここではここではuseStateの型と同義になっています。
使ってみる
import {changeUIState, } from "./state";
import {UIStateContextProvider, useUIStateContext} from "./state-react";
export const DemoApp = ()=> {
return (
<UIStateContextProvider>
<WidgetA />
<WidgetB />
</UIStateContextProvider>
);
}
export const WidgetA = ()=> {
const [state] = useUIStateContext()
return (
<div>
<h1>A</h1>
<div>{state.currentTime}</div>
</div>
);
}
export const WidgetB = ()=> {
const [state , setState] = useUIStateContext()
const changeCurrentTime = (currentTime : number)=> {
setState(prevState => {
return changeUIState(prevState).setCurrentTime(currentTime).update()
});
}
return (
<div>
<h1>B</h1>
<input
type="number"
value={state.currentTime}
onChange={e => changeCurrentTime(Number(e.target.value))}
/>
</div>
);
}
WidgetB コンポーネントから変更したデータがWidgetAにも反映されるようになりました。
グローバルステート管理と銘打ってますが、単なるリアクティブなシングルトン、というわけでもありません。
あくまでDemoAppコンポーネントが所有しているローカルステートに対してReact.Contextを通じてバケツリレーをせずにアクセスできる、といった感覚が正しいかと思います。
なので、DemoAppコンポーネントの破棄とともに管理している状態も掃除できます。