はじめに
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 }) | 明示的に指定したグローバルストア | グローバルスコープの状態を参照 | 
この方法を使うことで、
- コンポーネントの再利用性を高めつつ、
- グローバル状態も一貫して共有できる
という、非常に柔軟で拡張性の高い状態管理アーキテクチャを構築できます。

 
  
  
  
  
コメント