T-CREATOR

React 状態管理の新星「Jotai」とは?基本概念から始め方まで完全ガイド

React 状態管理の新星「Jotai」とは?基本概念から始め方まで完全ガイド

React の状態管理で悩んでいる開発者の皆さん、こんにちは!プロップドリリングに疲れ、Redux の複雑さに困っていませんか?そんな皆さんに朗報です。今回は、2020 年に登場してから開発者の間で注目を集めている状態管理ライブラリ「Jotai」をご紹介します。

Jotai は「原子」を意味する日本語で、その名の通り最小単位の状態(atom)を組み合わせて大きな状態を作り上げるという革新的なアプローチを採用しています。従来の Redux や Context API とは一線を画すこの新しい手法は、React 開発の常識を変える可能性を秘めているんです。

本記事では、Jotai の基本概念から実際の導入方法、実践的な使い方まで、初心者の方でも安心して理解できるよう丁寧に解説していきます。記事を読み終える頃には、あなたも Jotai の魅力に取り憑かれ、すぐにでもプロジェクトに導入したくなることでしょう。

React 状態管理の現状と課題

従来の状態管理手法の限界

React 開発において状態管理は避けて通れない重要な要素です。しかし、多くの開発者が現在の状態管理手法に課題を感じているのが現状でしょう。

最も基本的な useStateuseReducer を使った状態管理では、コンポーネント間で状態を共有する際にプロップドリリング問題が発生します。親コンポーネントから子、孫コンポーネントへと延々と props を渡し続ける光景は、誰もが一度は経験したことがあるはずです。

typescript// プロップドリリングの例
function App() {
  const [user, setUser] = useState(null);

  return (
    <Header user={user} />
    <Main user={user} setUser={setUser} />
    <Footer user={user} />
  );
}

function Main({ user, setUser }) {
  return (
    <div>
      <Profile user={user} />
      <Settings user={user} setUser={setUser} />
    </div>
  );
}

function Settings({ user, setUser }) {
  return (
    <UserForm user={user} setUser={setUser} />
  );
}

Context API を使えばプロップドリリングは解決できますが、今度は別の問題が浮上します。Context の値が変更されると、その Context を購読しているすべてのコンポーネントが再レンダリングされてしまうのです。

大規模なアプリケーションになるほど、この不要な再レンダリングはパフォーマンスに深刻な影響を与えます。特にユーザー情報のように頻繁に参照される状態では、アプリ全体のレスポンスが悪化する原因となってしまいます。

Redux は確かに強力な状態管理ソリューションですが、学習コストが高く、ボイラープレートコードが大量に必要という課題があります。シンプルな状態管理のために、action、reducer、store の設定が必要で、小規模なプロジェクトでは明らかにオーバーエンジニアリングになってしまうことも少なくありません。

なぜ新しいアプローチが必要なのか

現代の React 開発では、アプリケーションの複雑さが年々増しています。SPA(Single Page Application)の普及により、クライアントサイドで管理する状態の量も爆発的に増加しました。

課題従来の解決策問題点
プロップドリリングContext API不要な再レンダリング
複雑な状態管理Redux学習コストとボイラープレート
パフォーマンス最適化memo、useMemo手動最適化の限界
開発体験各種ライブラリ統一性の欠如

さらに、React 18 で導入された Concurrent Features やサーバーサイドレンダリング(SSR)への対応も考慮すると、従来の状態管理ライブラリでは限界が見えてきます。

開発者が本当に求めているのは、シンプルで直感的パフォーマンスが良くスケーラブルな状態管理ソリューションです。そして、それを実現するためには、従来の設計思想から根本的に発想を転換する必要があったのです。

Jotai が解決する問題

ボイラープレートコードの削減

Jotai の最大の魅力の一つは、驚くほどシンプルな API です。Redux で 10 行以上必要だった状態定義が、Jotai なら 1 行で完結します。

typescript// Redux の場合
const initialState = { count: 0 };

const countReducer = (state = initialState, action) => {
  switch (action.type) {
    case 'INCREMENT':
      return { ...state, count: state.count + 1 };
    case 'DECREMENT':
      return { ...state, count: state.count - 1 };
    default:
      return state;
  }
};

const store = createStore(countReducer);

// Jotai の場合
const countAtom = atom(0);

この違いは歴然ですね!Jotai では、状態の定義から使用まで、一貫してシンプルな API で統一されています。開発者は複雑な設定やボイラープレートコードに時間を取られることなく、本来のビジネスロジックに集中できるのです。

さらに、TypeScript との親和性も抜群で、型安全性を保ちながら開発効率を向上させることができます。型推論が効くため、明示的な型定義すら不要な場合が多いんです。

パフォーマンスの最適化

Jotai のパフォーマンス最適化は、その設計思想に深く根ざしています。各 atom は独立した状態を持ち、atom の値が変更されたときは、その atom を直接使用しているコンポーネントのみが再レンダリングされます。

typescriptconst userNameAtom = atom('田中太郎');
const userAgeAtom = atom(25);

// UserNameコンポーネントは userNameAtom の変更時のみ再レンダリング
function UserName() {
  const [name] = useAtom(userNameAtom);
  return <div>{name}</div>;
}

// UserAgeコンポーネントは userAgeAtom の変更時のみ再レンダリング
function UserAge() {
  const [age] = useAtom(userAgeAtom);
  return <div>{age}歳</div>;
}

この細粒度な更新制御により、大規模なアプリケーションでも高いパフォーマンスを維持できます。Context API のように「一つの値が変わると関連するすべてのコンポーネントが再レンダリング」ということが起こりません。

また、Jotai は内部的に WeakMap を使用して atom の状態を管理しているため、メモリ効率も優秀です。不要になった atom は自動的にガベージコレクションの対象となり、メモリリークの心配も軽減されます。

開発体験の向上

開発者体験(DX)の向上も、Jotai が高く評価される理由の一つです。学習コストが低く、直感的な API により、チーム開発での導入もスムーズに進められます。

Jotai の atom は、まさに「レゴブロック」のような存在です。小さな atom を組み合わせて、複雑な状態を構築していく過程は、まるでパズルを解くような楽しさがあります。

typescript// 基本的なatom
const firstNameAtom = atom('太郎');
const lastNameAtom = atom('田中');

// 派生atom(組み合わせ)
const fullNameAtom = atom(
  (get) => `${get(lastNameAtom)} ${get(firstNameAtom)}`
);

デバッグ体験も優れており、React DevTools との連携により、atom の状態変化を視覚的に確認できます。どの atom がいつ更新されたのか、どのコンポーネントがその変更を受けているのかが一目瞭然です。

テスト時の扱いやすさも特筆すべき点でしょう。atom は単純な関数として定義されるため、単体テストが書きやすく、モックも簡単に作成できます。

Jotai の基本概念と特徴

atom ベースの設計思想

Jotai の核心となるのが「atom」という概念です。atom は日本語で「原子」を意味し、物質を構成する最小単位を指します。Jotai における atom も同様に、アプリケーション状態の最小単位として機能するんです。

この設計思想は、従来のトップダウン型の状態管理とは根本的に異なります。大きな一つのストアを定義してから必要な部分を切り出すのではなく、小さな atom を定義してから必要に応じて組み合わせていくのが Jotai のアプローチです。

typescript// 従来のアプローチ(トップダウン)
const appState = {
  user: {
    profile: { name: '', age: 0 },
    preferences: { theme: 'light' },
    notifications: { count: 0 },
  },
  posts: [],
  comments: [],
};

// Jotaiのアプローチ(ボトムアップ)
const userNameAtom = atom('');
const userAgeAtom = atom(0);
const themeAtom = atom('light');
const notificationCountAtom = atom(0);
const postsAtom = atom([]);
const commentsAtom = atom([]);

このボトムアップ型のアプローチには、多くのメリットがあります。まず、状態の依存関係が明確になります。どの atom がどの atom に依存しているかが、コードから直接読み取れるのです。

また、不要な状態の結合を避けることができます。従来の大きなストア構造では、関連のない状態同士が同じオブジェクト内に存在することがありましたが、atom ベースの設計では、論理的に独立した状態は物理的にも独立して管理されます。

bottom-up アプローチとは

bottom-up アプローチとは、小さな部品から大きなシステムを構築していく設計手法です。Jotai では、このアプローチが状態管理の複雑さを劇的に軽減します。

従来の Redux のようなトップダウン型では、最初にアプリケーション全体の状態構造を設計する必要がありました。しかし、実際の開発では要件が変更されることが多く、初期設計が足かせになることも少なくありませんでした。

typescript// bottom-upアプローチの例

// Step 1: 基本的なatomを定義
const todoTextAtom = atom('');
const todosAtom = atom([]);

// Step 2: 派生atomで機能を拡張
const todoCountAtom = atom((get) => get(todosAtom).length);
const completedTodosAtom = atom((get) =>
  get(todosAtom).filter((todo) => todo.completed)
);

// Step 3: さらに複雑な状態を組み立て
const todoStatsAtom = atom((get) => ({
  total: get(todoCountAtom),
  completed: get(completedTodosAtom).length,
  remaining:
    get(todoCountAtom) - get(completedTodosAtom).length,
}));

このアプローチの素晴らしいところは、開発の進行に合わせて段階的に状態を構築できることです。最初はシンプルな atom から始めて、必要に応じて機能を追加していけば良いのです。

また、テストも段階的に書けるため、品質の高いアプリケーションを効率的に開発できます。小さな atom の単体テストから始めて、徐々に結合テストへと進めていけば、バグの早期発見にもつながります。

他ライブラリとの根本的違い

Jotai が他の状態管理ライブラリと根本的に異なる点を、比較表で整理してみましょう。

特徴ReduxZustandContext APIJotai
学習コスト
ボイラープレート極少
パフォーマンス良好(最適化必要)良好課題あり優秀
TypeScript 対応良好良好基本的優秀
デバッグ体験優秀良好基本的優秀
設計思想トップダウンシンプルコンテキストボトムアップ

最も大きな違いは、状態の共有方法にあります。Redux や Zustand は中央集権的なストアを持ちますが、Jotai は分散型の atom ネットワークを形成します。

Context API は階層的な状態共有を前提としていますが、Jotai はフラットな状態共有を実現します。これにより、コンポーネントツリーの構造に関係なく、自由に状態を共有できるのです。

typescript// Context APIの制約
const UserContext = createContext();
const ThemeContext = createContext();

function App() {
  return (
    <UserContext.Provider value={userValue}>
      <ThemeContext.Provider value={themeValue}>
        <DeepNestedComponent />
      </ThemeContext.Provider>
    </UserContext.Provider>
  );
}

// Jotaiの自由度
const userAtom = atom(userValue);
const themeAtom = atom(themeValue);

// どこからでも直接アクセス可能
function AnyComponent() {
  const [user] = useAtom(userAtom);
  const [theme] = useAtom(themeAtom);
  // ...
}

また、Jotai はSuspenseError Boundaryとの統合が設計レベルで考慮されています。非同期処理を含む atom は、自動的に Suspense と連携し、ローディング状態やエラー状態を適切に処理できます。

Jotai の導入と環境構築

インストール方法

Jotai の導入は非常にシンプルです。Yarn を使って、プロジェクトに Jotai を追加しましょう。

bash# 基本的なJotaiパッケージのインストール
yarn add jotai

# TypeScriptプロジェクトの場合(型定義は本体に含まれています)
yarn add jotai

# 追加のユーティリティが必要な場合
yarn add jotai-devtools jotai-immer jotai-optics

Jotai の素晴らしい点は、追加の設定がほとんど不要なことです。インストールが完了すれば、すぐに atom を定義して使い始めることができます。

既存の React プロジェクトへの導入も非常にスムーズです。段階的な移行が可能で、一部のコンポーネントから Jotai を使い始めて、徐々に適用範囲を広げていくことができます。

TypeScript 環境での設定

TypeScript プロジェクトで Jotai を使用する場合、特別な設定は基本的に不要です。Jotai は標準で TypeScript サポートが組み込まれており、優れた型推論を提供します。

typescript// TypeScriptでのatom定義例
import { atom } from 'jotai';

// プリミティブ型のatom
const countAtom = atom(0); // number型として推論
const nameAtom = atom(''); // string型として推論
const isLoadingAtom = atom(false); // boolean型として推論

// オブジェクト型のatom
interface User {
  id: number;
  name: string;
  email: string;
}

const userAtom = atom<User | null>(null); // User | null型として推論

// 派生atomも型安全
const userNameAtom = atom(
  (get) => get(userAtom)?.name ?? 'ゲスト'
); // string型として推論

より厳密な型チェックを行いたい場合は、tsconfig.jsonで以下の設定を推奨します。

json{
  "compilerOptions": {
    "strict": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true,
    "noUncheckedIndexedAccess": true
  }
}

必要な依存関係

Jotai は最小限の依存関係で動作するよう設計されています。基本的にはjotaiパッケージのみで十分ですが、プロジェクトの要件に応じて追加パッケージを検討しましょう。

パッケージ用途導入タイミング
jotai基本機能必須
jotai-devtools開発ツール開発時推奨
jotai-immerイミュータブル更新複雑な状態更新時
jotai-opticsレンズ型操作深いオブジェクト操作時
jotai-valtioValtio 連携プロキシベース状態管理時
jotai-zustandZustand 連携既存 Zustand コードとの統合時

開発環境では、jotai-devtoolsの導入を強く推奨します。これにより、ブラウザの開発者ツールで atom の状態変化を視覚的に確認できるようになります。

typescriptimport { useAtomDevtools } from 'jotai-devtools';

function MyComponent() {
  const [count, setCount] = useAtom(countAtom);

  // 開発環境でのデバッグサポート
  useAtomDevtools(countAtom, 'count');

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount((c) => c + 1)}>
        増加
      </button>
    </div>
  );
}

パフォーマンス監視が必要な本格的なプロジェクトでは、React DevTools との連携も考慮しましょう。Jotai は標準で React DevTools の Profiler と連携し、コンポーネントの再レンダリング情報を詳細に提供します。

基本的な使い方を実例で学ぶ

シンプルなカウンターアプリ

まずは、Jotai の基本的な使い方を、誰もが理解しやすいカウンターアプリで学んでみましょう。この例を通じて、atom の作成から使用まで、一連の流れを体験できます。

typescriptimport React from 'react';
import { atom, useAtom } from 'jotai';

// 1. atomの定義
const countAtom = atom(0);

// 2. カウンターコンポーネント
function Counter() {
  const [count, setCount] = useAtom(countAtom);

  return (
    <div>
      <h2>カウンター: {count}</h2>
      <button onClick={() => setCount(count + 1)}>
        +1
      </button>
      <button onClick={() => setCount(count - 1)}>
        -1
      </button>
      <button onClick={() => setCount(0)}>リセット</button>
    </div>
  );
}

// 3. 別のコンポーネントからも同じ状態にアクセス
function CounterDisplay() {
  const [count] = useAtom(countAtom);

  return (
    <div>
      <p>現在のカウント: {count}</p>
      {count > 10 && <p>🎉 10を超えました!</p>}
    </div>
  );
}

// 4. アプリケーション
function App() {
  return (
    <div>
      <h1>Jotai カウンターアプリ</h1>
      <Counter />
      <CounterDisplay />
    </div>
  );
}

この例で注目すべきポイントは、Providers や Context の設定が一切不要という点です。atom を定義すれば、アプリケーション内のどこからでも同じ状態にアクセスできます。

さらに、Counterコンポーネントでカウントを変更すると、CounterDisplayコンポーネントも自動的に更新されます。これは、両方のコンポーネントが同じcountAtomを購読しているためです。

atom の作成と使用方法

atom の作成は、atom関数を呼び出すだけで完了します。初期値を渡すことで、atom の型も自動的に推論されるのが素晴らしいポイントですね。

typescriptimport { atom } from 'jotai';

// プリミティブ型のatom
const countAtom = atom(0);
const nameAtom = atom('田中太郎');
const isVisibleAtom = atom(true);

// 配列型のatom
const todosAtom = atom([
  { id: 1, text: '買い物', completed: false },
  { id: 2, text: '掃除', completed: true },
]);

// オブジェクト型のatom
const userAtom = atom({
  id: 1,
  name: '田中太郎',
  email: 'tanaka@example.com',
});

// null許容型のatom
const selectedItemAtom = atom<string | null>(null);

atom の値を更新する方法は、useStateと同様に非常に直感的です。新しい値を直接設定するか、更新関数を使用して現在の値を基に新しい値を計算できます。

typescriptfunction TodoApp() {
  const [todos, setTodos] = useAtom(todosAtom);

  const addTodo = (text: string) => {
    setTodos((prevTodos) => [
      ...prevTodos,
      { id: Date.now(), text, completed: false },
    ]);
  };

  const toggleTodo = (id: number) => {
    setTodos((prevTodos) =>
      prevTodos.map((todo) =>
        todo.id === id
          ? { ...todo, completed: !todo.completed }
          : todo
      )
    );
  };

  return (
    <div>
      {todos.map((todo) => (
        <div key={todo.id}>
          <input
            type='checkbox'
            checked={todo.completed}
            onChange={() => toggleTodo(todo.id)}
          />
          <span
            style={{
              textDecoration: todo.completed
                ? 'line-through'
                : 'none',
            }}
          >
            {todo.text}
          </span>
        </div>
      ))}
      <button onClick={() => addTodo('新しいタスク')}>
        タスク追加
      </button>
    </div>
  );
}

useAtom フックの活用

useAtomフックは、Jotai の最も基本的なフックですが、実は非常に柔軟な使い方ができます。用途に応じて、値の読み取り専用や書き込み専用での使用も可能です。

typescriptimport { useAtom, useAtomValue, useSetAtom } from 'jotai';

function ExampleComponent() {
  // 完全なアクセス(読み取り + 書き込み)
  const [count, setCount] = useAtom(countAtom);

  // 読み取り専用
  const countValue = useAtomValue(countAtom);

  // 書き込み専用
  const setCountValue = useSetAtom(countAtom);

  return (
    <div>
      <p>現在の値: {countValue}</p>
      <button
        onClick={() => setCountValue((prev) => prev + 1)}
      >
        増加
      </button>
    </div>
  );
}

この使い分けは、パフォーマンス最適化の観点でも重要です。例えば、値を表示するだけのコンポーネントではuseAtomValueを使用し、操作のみを行うコンポーネントではuseSetAtomを使用することで、不要な再レンダリングを避けることができます。

特に、useSetAtomは非常に強力で、setter 関数自体は決して変更されません。これにより、useCallbackでのメモ化が不要になり、子コンポーネントへの不要な再レンダリングを防げるのです。

typescript// 親コンポーネント
function ParentComponent() {
  const setCount = useSetAtom(countAtom);

  // setCountは決して変更されないため、
  // ChildComponentは不要に再レンダリングされない
  return <ChildComponent onIncrement={setCount} />;
}

// 子コンポーネント
const ChildComponent = React.memo(({ onIncrement }) => {
  return (
    <button onClick={() => onIncrement((prev) => prev + 1)}>
      増加
    </button>
  );
});

より実践的な機能の活用

複数の atom を組み合わせる

実際のアプリケーション開発では、複数の関連する atom を組み合わせて使用することが多くなります。Jotai では、この組み合わせが非常にエレガントに行えるんです。

typescriptimport { atom, useAtom } from 'jotai';

// 基本的なatom群
const firstNameAtom = atom('太郎');
const lastNameAtom = atom('田中');
const ageAtom = atom(25);
const emailAtom = atom('tanaka@example.com');

// ユーザー情報を管理するコンポーネント
function UserProfile() {
  const [firstName, setFirstName] = useAtom(firstNameAtom);
  const [lastName, setLastName] = useAtom(lastNameAtom);
  const [age, setAge] = useAtom(ageAtom);
  const [email, setEmail] = useAtom(emailAtom);

  return (
    <div>
      <h3>ユーザープロフィール</h3>
      <div>
        <label>姓: </label>
        <input
          value={lastName}
          onChange={(e) => setLastName(e.target.value)}
        />
      </div>
      <div>
        <label>名: </label>
        <input
          value={firstName}
          onChange={(e) => setFirstName(e.target.value)}
        />
      </div>
      <div>
        <label>年齢: </label>
        <input
          type='number'
          value={age}
          onChange={(e) => setAge(Number(e.target.value))}
        />
      </div>
      <div>
        <label>メール: </label>
        <input
          type='email'
          value={email}
          onChange={(e) => setEmail(e.target.value)}
        />
      </div>
    </div>
  );
}

このように複数の atom を独立して管理することで、各フィールドの更新が他のフィールドに影響することがありません。例えば、名前を変更しても年齢の入力フィールドは再レンダリングされないため、ユーザーの入力体験が向上します。

また、atom の組み合わせは、異なるコンポーネント間での柔軟な状態共有を可能にします。あるコンポーネントでは名前のみを表示し、別のコンポーネントでは年齢のみを表示するという使い分けも簡単に実現できるでしょう。

派生 atom の作成

派生 atom(derived atom)は、Jotai の真の力を発揮する機能の一つです。他の atom の値を基に計算された値を持つ atom を作成できます。

typescript// 派生atomの例
const fullNameAtom = atom(
  (get) => `${get(lastNameAtom)} ${get(firstNameAtom)}`
);

const isAdultAtom = atom((get) => get(ageAtom) >= 20);

const userSummaryAtom = atom((get) => {
  const fullName = get(fullNameAtom);
  const age = get(ageAtom);
  const email = get(emailAtom);
  const isAdult = get(isAdultAtom);

  return {
    fullName,
    age,
    email,
    isAdult,
    description: `${fullName}さん(${age}歳)は${
      isAdult ? '成人' : '未成年'
    }です。`,
  };
});

// 派生atomを使用するコンポーネント
function UserSummary() {
  const summary = useAtomValue(userSummaryAtom);

  return (
    <div>
      <h3>ユーザーサマリー</h3>
      <p>
        <strong>氏名:</strong> {summary.fullName}
      </p>
      <p>
        <strong>年齢:</strong> {summary.age}歳
      </p>
      <p>
        <strong>メール:</strong> {summary.email}
      </p>
      <p>
        <strong>ステータス:</strong>{' '}
        {summary.isAdult ? '成人' : '未成年'}
      </p>
      <p>{summary.description}</p>
    </div>
  );
}

派生 atom の素晴らしい点は、依存する atom が変更されたときに自動的に再計算されることです。そして、再計算された値が前の値と同じ場合は、その atom を購読しているコンポーネントは再レンダリングされません。

書き込み可能な派生 atom も作成できます。これにより、複数の atom を統合的に操作することも可能になります。

typescript// 書き込み可能な派生atom
const userAtom = atom(
  // getter: 複数のatomから統合オブジェクトを作成
  (get) => ({
    firstName: get(firstNameAtom),
    lastName: get(lastNameAtom),
    age: get(ageAtom),
    email: get(emailAtom),
  }),
  // setter: 統合オブジェクトから各atomに値を設定
  (get, set, newUser) => {
    set(firstNameAtom, newUser.firstName);
    set(lastNameAtom, newUser.lastName);
    set(ageAtom, newUser.age);
    set(emailAtom, newUser.email);
  }
);

function UserForm() {
  const [user, setUser] = useAtom(userAtom);

  const handleSubmit = (formData) => {
    // 一度に全てのatomを更新
    setUser({
      firstName: formData.firstName,
      lastName: formData.lastName,
      age: formData.age,
      email: formData.email,
    });
  };

  return (
    <form onSubmit={handleSubmit}>
      {/* フォーム要素 */}
    </form>
  );
}

非同期処理の実装

現代の Web アプリケーションでは、API からのデータ取得などの非同期処理は不可欠です。Jotai は、非同期処理を驚くほどシンプルに扱えるように設計されています。

typescript// 非同期atomの例
const userIdAtom = atom(1);

const userDataAtom = atom(async (get) => {
  const userId = get(userIdAtom);
  const response = await fetch(`/api/users/${userId}`);

  if (!response.ok) {
    throw new Error('ユーザーデータの取得に失敗しました');
  }

  return response.json();
});

// 非同期atomを使用するコンポーネント
function UserDisplay() {
  const [userData] = useAtom(userDataAtom);

  return (
    <div>
      <h3>{userData.name}</h3>
      <p>Email: {userData.email}</p>
      <p>登録日: {userData.createdAt}</p>
    </div>
  );
}

// Suspenseと組み合わせた使用例
function App() {
  return (
    <div>
      <h1>ユーザー情報</h1>
      <Suspense fallback={<div>読み込み中...</div>}>
        <ErrorBoundary
          fallback={<div>エラーが発生しました</div>}
        >
          <UserDisplay />
        </ErrorBoundary>
      </Suspense>
    </div>
  );
}

非同期 atom は、React 18 の Suspense と完全に統合されています。atom が非同期の値を返す場合、React は自動的に Suspense のfallbackを表示し、値が解決されるとコンポーネントをレンダリングします。

エラーハンドリングも同様に、Error Boundary と自然に統合されます。非同期処理中にエラーが発生すると、最も近い Error Boundary がそのエラーをキャッチしてくれるのです。

より複雑な非同期処理では、依存関係のある複数の atom を組み合わせることも可能です。

typescript// 複雑な非同期処理の例
const searchQueryAtom = atom('');
const searchFiltersAtom = atom({
  category: 'all',
  sortBy: 'date',
});

const searchResultsAtom = atom(async (get) => {
  const query = get(searchQueryAtom);
  const filters = get(searchFiltersAtom);

  if (!query.trim()) {
    return { results: [], total: 0 };
  }

  const params = new URLSearchParams({
    q: query,
    category: filters.category,
    sortBy: filters.sortBy,
  });

  const response = await fetch(`/api/search?${params}`);
  return response.json();
});

// デバウンス機能付きの検索
const debouncedSearchAtom = atom(async (get) => {
  const query = get(searchQueryAtom);

  // 300ms待機してから検索実行
  await new Promise((resolve) => setTimeout(resolve, 300));

  // 待機中にクエリが変更された場合はキャンセル
  if (get(searchQueryAtom) !== query) {
    throw new Error('Search cancelled');
  }

  return get(searchResultsAtom);
});

まとめ

React 開発において状態管理は永遠の課題とも言えるテーマですが、Jotai の登場により、その解決策が大きく進歩しました。本記事でご紹介したように、Jotai は従来の複雑な状態管理から開発者を解放し、よりシンプルで直感的な開発体験を提供します。

Jotai の最大の魅力は、その学習コストの低さ高いパフォーマンスの両立にあります。atom という小さな単位から始めて、必要に応じて組み合わせていくボトムアップ型のアプローチは、まさに現代的な開発手法と言えるでしょう。

特に注目すべきは、TypeScript との優れた統合性です。型推論が効果的に働くため、型安全性を保ちながらも開発効率を大幅に向上させることができます。また、React 18 の新機能である Suspense や Concurrent Features との自然な統合も、将来性の高さを物語っています。

実際のプロジェクトへの導入においても、段階的な移行が可能で、既存のコードベースを壊すことなく新しい機能から順次適用していけます。小さなコンポーネントから始めて、徐々に適用範囲を広げていくアプローチは、チーム開発においても非常に現実的でしょう。

非同期処理の扱いやすさも特筆すべき点です。従来の状態管理ライブラリでは複雑になりがちな API との連携やエラーハンドリングが、Jotai では驚くほどシンプルに記述できます。

今後の React 開発において、Jotai は間違いなく重要な選択肢の一つとなるでしょう。まだ触れたことのない方は、ぜひ小さなプロジェクトから始めて、その魅力を体験してみてください。きっと、React 開発の新しい可能性を発見できるはずです。

状態管理ライブラリの選択に迷われている方、既存の複雑な状態管理に課題を感じている方には、ぜひ Jotai の導入を検討していただきたいと思います。その直感的な API と優れたパフォーマンスが、あなたの開発体験を大きく向上させてくれることでしょう。

関連リンク