useContextとuseStateを使ったグローバル状態管理
ちゃんとグローバル状態管理をやるときはReduxなりを使うのが良いと思うのですが、そこまで大きくない場合は余計なライブラリ入れたくないですよね。
そんなときの解決法を実装したので共有します。
最終的な実装
個人的には、Contextをファイルの外に持ち出さない方がいいんじゃないかな~と思い、以下のようにしました。
SampleContext.tsx
import { createContext, Dispatch, ReactNode, SetStateAction, useContext, useState } from 'react';
// 型定義
export type SampleContext = { hoge: string; fuga: number };
type SampleContextType = { sampleContext: SampleContext; setSampleContext: Dispatch<SetStateAction<SampleContext>> };
const initSampleContext: SampleContext = { hoge: '', fuga: 0 };
// Context定義
// exportしないことで、context関連のロジックをこのファイルに閉じ込める。
const SampleContext = createContext<SampleContextType>({ sampleContext: initSampleContext, setSampleContext: () => {} });
// Application全体を囲うProvider
export const SampleContextProvider = (props: { children: ReactNode }) => {
const { children } = props;
const [sampleContext, setSampleContext] = useState<SampleContext>(initSampleContext);
return <SampleContext.Provider value={{ sampleContext, setSampleContext }}>{children}</SampleContext.Provider>;
};
// SampleContextを使うカスタムhook。基本はこっちを使って、参照&更新する。
export const useSampleContext = () => {
const { sampleContext, setSampleContext } = useContext(SampleContext);
const dispatcher = {
setHoge: (hoge: string) => setSampleContext((pre) => ({ ...pre, hoge })),
countUpFuga: () => setSampleContext((pre) => ({ ...pre, fuga: pre.fuga + 1 })),
};
return { sampleContext, dispatcher };
};
// 初回ロード等の時だけ、こちらのsetterでコンテキストを丸ごと置き換える。
export const useSetSampleContext = () => {
const { setSampleContext } = useContext(SampleContext);
return { setSampleContext };
};
使う方のサンプル実装
App.tsx
export const App = () => {
return (
<>
<SampleContextProvider>
<Child/>
<Disp/>
</SampleContextProvider>
</>
);
};
Child.tsx
export const Child = () => {
const { sampleContext: { hoge, fuga } } = useSampleContext();
return (
<div>
<div>hoge -- ${hoge}</div>
<div>fuga -- ${fuga}</div>
</div>
);
};
Disp.tsx
export const Disp = () => {
const { sampleContext: { fuga }, dispatcher: { setHoge, countUpFuga, } } = useSampleContext();
return (
<div>
<button onClick={() => setHoge('hoge - ' + fuga)}>hoge</button>
<button onClick={() => countUpFuga()}>fuga</button>
</div>
);
};
軽い説明
ポイントは、useContextフックの役割は「部品間でデータを共有すること」で、状態管理はいつも通り「useState」や「useReducer」が実施することです。
SampleContextProviderを見るとわかるように、
- 状態管理: useState
- グローバル化: createContext & useContext
という役割分担で作られてます。
export const SampleContextProvider = (props: { children: ReactNode }) => {
const { children } = props;
// ↓「状態を持つ」はここでやってる。Contextは関係ない。
const [sampleContext, setSampleContext] = useState<SampleContext>(initSampleContext);
// Providerに状態を渡すことで、Contextを通じて上記の [sampleContext, setSampleContext] が他の部品でも(間接的に)利用できるようになる。
return <SampleContext.Provider value={{ sampleContext, setSampleContext }}>{children}</SampleContext.Provider>;
};
Providerで状態管理したら、格納したContextを利用できる口を用意します。
自分は以下のような感じでstateの更新方法を制限しました。
SampleContextProviderの時点でuseStateではなくuseReducerを使っても良いかもしれませんが、やっぱりuseStateのほうが馴染みがあるのでこちらで。。。
// SampleContextを使うカスタムhook。基本はこっちを使って、参照&更新する。
export const useSampleContext = () => {
const { sampleContext, setSampleContext } = useContext(SampleContext);
const dispatcher = {
setHoge: (hoge: string) => setSampleContext((pre) => ({ ...pre, hoge })),
countUpFuga: () => setSampleContext((pre) => ({ ...pre, fuga: pre.fuga + 1 })),
};
return { sampleContext, dispatcher };
};
自分のアプリは、個人開発なところもあって、setterも開放しました。一番最初の初期化でこっちを使います。
// 初回ロード等の時だけ、こちらのsetterでコンテキストを丸ごと置き換える。
export const useSetSampleContext = () => {
const { setSampleContext } = useContext(SampleContext);
return { setSampleContext };
};