React + useContext + TypescriptでDI
TL;DR
React
などで、DI的なことをしたくなることがあります。
具体的には、SDK
とかをMockしたりするときや、単純にDDD的な感じで実装を勧めている場合です。
大抵の場合、こういったClient
、Repository
、Service
のようなものは、React
の再描画を発生させるような状態を持たないことが多いと思います。なので、useContext
で全体で共有してしまっても大きな問題は起こりづらいです。
こういった思想のもと、useContext
を使ってDIする、的な発想は比較的よく見かけます。
- ReactでuseContextを利用してDIっぽくする
- ReactのContextをDI Containerとして使う
- React Context for Dependency Injection Not State Management
初期化で困る
適当なInterfaceとその実装を定義します。
interface SomeInterface {
test: () => string;
}
class SomeEntity {
constructor() {}
test() {
return "test";
}
}
これを単純に初期化すると以下のようになります。しかし、これはuseContext
をDIコンテナとして扱うには嬉しくないです。外からinterface
を定義したものを入れたいのに、最初から特定の実体を持つものが注入されていますし、この時点で何らかの実体に依存しています。
import { createContext } from "react";
export const SomeContext = createContext<SomeInterface>(new SomeEntity());
実用上で嫌な部分としては、実体に依存しているという点と、以下のようにProvider
に初期値を与える必要がない部分です。
import { useContext } from "react"
import { SomeContext } from "/path/to/context"
const Test = ({}) => {
const entity = useContext(SomeContext)
return <>{entity.test()}</>
}
const App = ({}) => {
return <SomeContext.Provier><Test /></SomeContext.Provider>
}
export default App;
undefined
で初期化
上で述べたように、初期値として何らかの実体を持ちたくないので、undefined
で初期化します。
import { createContext } from "react";
export const SomeContext = createContext<SomeInterface | undefined>(undefined);
これで実体への依存はなくなりましたが、初期化をSkipしてもエラーにならないという問題と、useContext
の帰り値がSomeInterface | undefined
になって、毎回undefined
のチェックが必要になる、という点が嬉しくありません。
これを解決するため、以下の記事を参考にしました。
function createCtx<T>() {
const ctx = React.createContext<T | undefined>(undefined);
function useCtx() {
const c = React.useContext(ctx);
if (!c) throw new Error("useCtx must be inside a Provider with a value");
return c;
}
return [useCtx, ctx.Provider] as const;
}
この関数を介することで、useCtx
の返り値はT
になり、Provider
を初期化しない場合にはエラーにすることができます。使い方としては以下のような感じです。
const [useSomeContext, SomeContextProvider] = createCtx<SomeInterface>()
const Test = ({}) => {
const entity = useSomeContext()
return <>{entity.test()}</>
}
const App = ({}) => {
return <SomeContextProvier value={new SomeEntity()}><Test /></SomeContextProvider>
}
export default App;
まとめ
Context
をundefined
で初期化して、DIしないとエラーにする、かつ返り値にundefined
を含まないカスタムフックを使うことで、型安全にDIっぽいことができるようになります。