はじめに
Reactで状態管理を行う際、「どこまで状態を共有するか」というスコープ設計は避けて通れません。
アプリ全体で共通するデータ(ログインユーザーやテーマ設定など)は「グローバル状態」として扱いたい一方で、
ページやコンポーネント単位で完結する「ローカル状態」も存在します。
Jotaiは、軽量で柔軟な状態管理ライブラリとして人気ですが、<Provider>を使ってスコープを分けると、
同じAtomでもスコープごとに独立した状態を持つようになります。
これは便利な一方で、「ローカルスコープ内でグローバル状態を参照・更新したい」という場面で混乱を招くことがあります。
本記事では、
<Provider>で囲まれたローカルスコープの中から、意図的にグローバルスコープのAtomを参照・更新する方法
を中心に、コード例とともに解説します。
Jotaiのスコープの仕組みを理解する
まずは、Jotaiにおけるスコープの基本を理解しておきましょう。
Jotaiでは、「ストア (store)」という単位でAtomの状態を管理しています。
アプリを<Provider>で囲むと、その内部で独立したストアが生成され、
その範囲内のコンポーネントはすべて同じストアを共有します。
import { atom, useAtom, Provider } from 'jotai';
const counterAtom = atom(0);
const Counter = () => {
const [count, setCount] = useAtom(counterAtom);
return (
<button onClick={() => setCount(c => c + 1)}>
Count: {count}
</button>
);
};
// ルートでの利用
export const App = () => (
<>
{/* それぞれのProviderでストアが独立する */}
<Provider>
<Counter />
</Provider>
<Provider>
<Counter />
</Provider>
</>
);
上記のように<Provider>を2つ並べた場合、
両方のCounterは同じcounterAtomを使っているにも関わらず、状態が完全に独立します。
つまり、1つのボタンをクリックしても、もう一方には影響しません。
これはJotaiの柔軟な特徴ですが、逆に言えば「スコープをまたいで状態共有したいとき」には工夫が必要です。
課題:ローカルスコープ内でグローバル状態を参照したい
たとえば以下のようなユースケースを考えてみましょう。
- ページ内では独自のカウンターやフォームなどのローカル状態を持ちたい
- ただし、ログイン中のユーザー情報やテーマなどはアプリ全体で共有したい
このとき、ページをローカルスコープでラップしてしまうと、
その内部でuseAtom(userAtom)を呼び出しても、グローバルのuserAtomにはアクセスできません。
// NG例:ローカルProvider内ではグローバルAtomに届かない
<Provider>
<PageComponent />
</Provider>
これを解決するには、「グローバルストア」を明示的に作成し、useAtomなどのフックにそのストアを指定して呼び出す必要があります。
解決策:グローバルストアを明示的に作成・指定する
Jotaiのフック(useAtom, useAtomValue, useSetAtom)は、第二引数にオプションを受け取ります。
このオプションに { store: globalStore } のようにストアを明示的に渡すことで、
どのストアを操作するかを制御できます。
const [user, setUser] = useAtom(userAtom, { store: globalStore });
この方法を使えば、ローカルスコープ内からでもグローバルストアを参照できるようになります。
つまり、Providerで囲まれた「壁」を越えて、状態を共有することが可能です。
実装ステップ:グローバルとローカルを共存させる
ここからは、実際の実装手順を3ステップで解説します。
Step 1. グローバルストアの作成 (src/store.ts)
まずはアプリ全体で共有するグローバルストアを作成します。createStoreを使ってインスタンスを生成し、エクスポートしておきます。
// src/store.ts
import { createStore } from 'jotai';
// グローバルスコープとして利用するストアを作成
export const globalStore = createStore();
Atomの定義は別ファイルに分けておくと整理しやすいです。
// src/atoms.ts
import { atom } from 'jotai';
// グローバルで使うAtom
export const userAtom = atom<{ name: string } | null>(null);
// ページ単位で使うローカルAtom
export const pageCounterAtom = atom(0);
Step 2. アプリ全体をグローバルストアで包む (src/main.tsx)
アプリのルート(通常はmain.tsxまたはApp.tsx)で、<Provider>にstore={globalStore}を指定してアプリ全体を包みます。
// src/main.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import { Provider } from 'jotai';
import { globalStore } from './store';
import App from './App';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<Provider store={globalStore}>
<App />
</Provider>
</React.StrictMode>,
);
これで、グローバルスコープの基準が定義されました。
Step 3. ローカルスコープ内でグローバルAtomを参照する
次に、ローカルスコープを作成したいページで、
独自の<Provider>を使ってスコープを分離します。
その上で、グローバルなAtomを参照したい場合だけstoreを明示的に指定します。
// src/pages/MyScopedPage.tsx
import { Provider, useAtom } from 'jotai';
import { userAtom, pageCounterAtom } from '../atoms';
import { globalStore } from '../store';
const PageContent = () => {
// ローカルAtom(スコープ内)
const [count, setCount] = useAtom(pageCounterAtom);
// グローバルAtom(スコープをまたぐ)
const [globalUser, setGlobalUser] = useAtom(userAtom, { store: globalStore });
return (
<div>
<h3>Page Content (Scoped)</h3>
<h4>Local State</h4>
<button onClick={() => setCount(c => c + 1)}>
Page Counter: {count}
</button>
<hr />
<h4>Global State (accessed from local scope)</h4>
<p>Global User: {globalUser?.name || 'Not set'}</p>
<button onClick={() => setGlobalUser({ name: 'Updated from Page' })}>
Update Global User from Page
</button>
</div>
);
};
// ページ全体をローカルスコープでラップ
export const MyScopedPage = () => {
return (
<Provider>
<PageContent />
</Provider>
);
};
この構成により、
pageCounterAtomはページ内に閉じたローカル状態userAtomはアプリ全体で共有されるグローバル状態
として機能します。
内部動作を補足すると、
useAtom(pageCounterAtom)→ 一番近い<Provider>(このページ用)のストアを参照useAtom(userAtom, { store: globalStore })→ 明示的に指定したグローバルストアを参照
のように処理されるため、ローカルなスコープとグローバルなスコープを意図的に使い分けることができます。
まとめ
以下のようにストアの範囲を適切に指定してAtomを呼び出すことで、ローカルな状態とグローバルな状態を分けて利用できます。
| 呼び出し方 | 対象ストア | 用途 |
|---|---|---|
useAtom(atom) | 一番近いProviderのストア | ローカルスコープの状態を参照 |
useAtom(atom, { store: globalStore }) | 明示的に指定したグローバルストア | グローバルスコープの状態を参照 |
この方法を使うことで、
- コンポーネントの再利用性を高めつつ、
- グローバル状態も一貫して共有できる
という、非常に柔軟で拡張性の高い状態管理アーキテクチャを構築できます。

コメント