🐻

zustandで依存性注入(DI)

2024/03/01に公開

zustand

zustandはドイツ語で「状態」を表す言葉で、Reactで(以外でも)使える状態管理ライブラリです。
https://github.com/pmndrs/zustand

「ザスタンド」と発音してましたが、ドイツ語では「ツゥースタント」みたいに発音するようです。(余談)

npm trendsで見てみるとRedux一強に見えますが、
昔から開発されてきたプロジェクトで他ライブラリへの移行が難しい、みたいなケースが多いと想像すると、主要な状態管理ライブラリの中ではかなり採用率が高そうです。
https://npmtrends.com/jotai-vs-react-redux-vs-valtio-vs-zustand

現在担当しているプロジェクトでは、開発開始当初は機能もそこまで多くなかったため、状態管理の手法としてReact Contextが採用されていました。
しかし、開発を進めるうちに機能も増え、React Contextのみを利用した状態管理だと下記のような課題が出てきました。

  • Contextの階層が複雑化し、キャッチアップのコストが高い
  • この画面でどのContextが使えるのか、使えるべきなのか、といったように実装や設計時に考慮すべきことが多い
  • 本来Contextで管理すべきでない情報も管理できてしまうことで、開発者によってコードの書き方に大きくばらつきが発生する

これらの課題を解決する手段として状態管理ライブラリの利用を考え始め、
シンプルなAPI、かつReactを知っていれば最低限処理を追えるようなライブラリを探していたところ、zustandがそれに当てはまり、拡張性も高そうだったので試しに使ってみることにしました。

依存性注入(DI)

依存性の注入自体に関しては一般論なので深くは触れませんが、具体的に依存性注入をする必要があった経緯をご紹介します。
担当プロジェクトではFirebaseを利用しており、かつ対象プラットフォームがWebだけでなく、今後の計画としてモバイルアプリの開発(React Nativeで開発予定)も計画されています。

Web - React Native間で状態管理周りの処理は似たようなものになるだろうということを想像すると出来る限り処理は統一したいのですが、FirebaseはWebとモバイルでSDKが異なります。

https://firebase.google.com/docs/firestore/client/libraries?hl=ja

つまり、例えばFirestoreとローカルの状態を同期したい場合などに

import { getFirestore } from "@firebase/firestore"; // web用のSDK

のようなimportをするとその時点でReact Nativeでは利用できないコードになってしまいます。

そのため、

interface FirestoreRepository {
  startSync: () => void;
}

のようにinterfaceを定義して、この中身の処理はweb、モバイルそれぞれで実装し、依存関係としてFirestoreRepositoryを受け渡すことで状態管理を共通化できないか考え始めました。

実践

実はzustandのREADMEや公式ドキュメントにも記載があるのですが、Contextと併用することで実現可能です。

https://github.com/pmndrs/zustand?tab=readme-ov-file#react-context
https://docs.pmnd.rs/zustand/previous-versions/zustand-v3-create-context#migration

ドキュメントの例だとイメージしづらいので、もう少し具体的なコードとしてはこんなイメージになります。

const StoreContext = createContext(null)

type Props = {
  firestoreRepository: FirestoreRepository;
}

const StoreProvider = ({ children, firestoreRepository }: PropsWithChildren<Props>) => {
  const storeRef = useRef()
  if (!storeRef.current) {
    storeRef.current = createStore((set) => ({
      // ここでfirestoreRepositoryを使える
      // ...
    }))
  }
  return (
    <StoreContext.Provider value={storeRef.current}>
      {children}
    </StoreContext.Provider>
  )
}

const useStoreInContext = (selector) => {
  const store = useContext(StoreContext)
  if (!store) {
    throw new Error('Missing StoreProvider')
  }
  return useStore(store, selector)
}

利用イメージ

const App = () => {
  // 該当プラットフォームのSDKを利用した FirestoreRepository のインスタンスを生成
  const firestoreRepository = useFirestoreRepository()

  return <StoreContext firestoreRepository={firestoreRepository}>
    <Child />
  </StoreContext>
}

const Child = () => {
  const store = useStoreInContext();
  // ...
}

もっと良いやり方あるよ!などありましたらコメント頂けますと幸いです 🙏

Discussion