前回までのあらすじ
React の軽量状態管理ライブラリ jotai に入門しました
今回は jotai の Provider と store の使い方について試したみたのメモです
環境
- jotai
v 2.0.3
- React
v 18.2.0
- TypeScript
v 4.9.3
結論
- jotai の atom はデフォルトでは global として扱われる
- Provider の中で使用された atom は Provider の中で閉じられる
- Provider で囲われたコンポーネントから Provider の外の global な atom にはアクセスできない
- 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> }; }
👇 サンプル
Provider 外の atom にアクセスする
Provider の内部から外の atom にアクセスするには、
- 全体を Context で囲いそこに store としてアクセス可能な atom を登録する
- コンポーネントでは
useContext
を使い store を取得しする - 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 })
所管
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 を使った永続化が簡単だった件について - かもメモ
俺もおしまい!