かもメモ

自分の落ちた落とし穴に何度も落ちる人のメモ帳

React 👻 jotai 👻 Provider 完全に理解した!

前回までのあらすじ

React の軽量状態管理ライブラリ jotai に入門しました
今回は jotai の Provider と store の使い方について試したみたのメモです

環境
  • jotai v 2.0.3
  • React v 18.2.0
  • TypeScript v 4.9.3

結論

  1. jotai の atom はデフォルトでは global として扱われる
  2. Provider の中で使用された atom は Provider の中で閉じられる
  3. Provider で囲われたコンポーネントから Provider の外の global な atom にはアクセスできない
  4. Provider の外の atom にアクセスしたい場合は、アクセス可能な atom を React の context に store として登録し context 経由でならアクセス可能

Provider で囲われてない atom は global

import { atom, useAtom, useAtomValue } from "jotai";

const countAtom = atom<number>(0);

const Counter: FC = () => {
  const [count, setCount] = useAtom(countAtom);
  const handleIncrement = () => {
    setCount((val) => val + 1);
  };

  return (
    <div>
      <label>Local Count: {count}</label>
      <button type="button" onClick={handleIncrement}>
        Increment
      </button>
    </div>
  );
};

export default function App() {
  const count = useAtomValue(countAtom);
  return {
    <div>
      <code>Count Atom: {count}</code>
      Counter 1: <Counter />
      Counter 2: <Counter />
    </div>
  };
}

Provider で囲われてないので countAtom は全て共通になっている (Counter 1 で更新しても Counter 2 で更新しても全ての count が同期される)

Provider は Context を作成する

Provider で囲った箇所は Context が作成され、その中で useAtom しているものは元の atom 設定が同じ変数でも別の状態として扱われる

import { atom, useAtom, useAtomValue, Provider } from "jotai";

const countAtom = atom<number>(0);

// 略

export default function App() {
  const count = useAtomValue(countAtom);
  return {
    <div>
      <code>Count Atom: {count}</code>
      Counter 1: <Counter />
      Counter 2: <Counter />
      <Provider>
         In Provider Counter
         <Counter />
         {/* countAtom が別ものとして扱われる */}
      </Provider>
    </div>
  };
}

jotai Provider
Provider で囲った箇所に Context が作成されている

👇 サンプル

Provider 外の atom にアクセスする

Provider の内部から外の atom にアクセスするには、

  1. 全体を Context で囲いそこに store としてアクセス可能な atom を登録する
  2. コンポーネントでは useContext を使い store を取得しする
  3. store を指定した方法 (useAtom(atom, { store });) で使いたい atom を取得する
import { createContext, useContext, FC} from "react";
import { atom, createStore, useAtom, useAtomValue, Provider } from "jotai";

// atom の作成
const countAtom = atom<number>(0);
// root に持たせる store と context の作成
const store = createStore();
store.set(countAtom, 1); // atom と初期値を設定
const RootContext = createContext<typeof store>(store);

export default function App(): JSX.Element {
  return (
    <div>
      {/* 全体を Context.Provider で囲う */}
      <RootContext.Provider value={store}>
        Parent Counter: <ParentCounter />
        {/* jotai の Provider でスコープを作成 */}
        <Provider>
          Child Counter1: <ChildCounter />
        </Provider>
        <Provider>
          Child Counter2: <ChildCounter />
        </Provider>
      </RootContext.Provider>
    </div>
  );
}

// Child Counter
function ChildCounter(): JSX.Element {
  // RootContext に登録されている countAtom の取得
  const store = useContext(RootContext);
  const parentCount = useAtomValue(countAtom, { store });

  // useAtom したものは Provider 内の local な atom になる
  const [count, setCount] = useAtom(countAtom);
  const handleIncrement = () => {
    setCount((val) => val + 1);
  };
  
  return (
    <div>
      <code>Parent Count: {parentCount}</code>
      <label>Local Count: {count}</label>
      <button type="button" onClick={handleIncrement}>
        Increment
      </button>
    </div>
  );
}

// Parent Counter
function ParentCounter(): JSX.Element {
  // useAtom(countAtom) は store とは別の local atom になる
  // 明示的に useContext で store を取得して useAtom に store を渡す必要がある
  const [count, setCount] = useAtom(countAtom);
  // => これは store に登録された atom とは別物扱いになる
  
  const store = useContext(RootContext);
  const [rootCount, setRootCount] = useAtom(countAtom, { store });

  const handleIncrement = () => {
    setRootCount((val) => val + 1);
  };

  return (
    <div>
      <label>Root Count: {rootCount}</label>
      <button type="button" onClick={handleIncrement}>
        Increment
      </button>
    </div>
  );
}

Provider 外の Atom にアクセスするサンプル

📝 jotai v1系 では Provider に scope プロパティを持たせることで実現していたが、v2系では廃止されている

Provider's scope prop
Previous API

const myScope = Symbol()
  // Parent component
  <Provider scope={myScope}>
    ...
  </Provider>

  // Child component
  useAtom(..., myScope)

New API

const MyContext = createContext()
const store = createStore()
  // Parent component
  <MyContext.Provider value={store}>
    ...
  </MyContext.Provider>

  // Child Component
  const store = useContext(MyContext)
  useAtom(..., { store })

cf. v2 API migration — Jotai

所管

Provider があるのに Provider の外の atom にアクセスするには React の Context を使うのが少し気持ち悪い気もしましたが、元の React の実装を利用することでライブラリのサイズを抑えてるんだろうな〜と感じました!
やり方が分かってしまえば Provider で atom のスコープを区切れるので便利そうです。一方で自由に簡単にアクセスができるとカオスが生まれる原因にもなるので、Recoil と同様に atom へのアクセスはカスタム hooks の中に閉じ込めてインターフェイスを絞るのが良さそうだな〜と思いました。

jotai の v1 系と v2 系とで API が結構ガラッと変わっていて検索して出てくる方法 (v1 系の書き方) だとエラーになってしまって、なんでダメなん?と v2 API migration のドキュメントに気づくまで結構ハマってしまいました… (急いでる時ほどドキュメントはちゃんと読もうな…

おわり

👉 jotai の基本 React 軽量状態管理ライブラリ👻 jotai 👻 さわってみた! - かもメモ
👉 つづき React 👻 jotai を使うと localStorage を使った永続化が簡単だった件について - かもメモ


俺もおしまい!