T-CREATOR

Jotai の atom とは?React の状態管理を革新する最小単位の概念を理解する

Jotai の atom とは?React の状態管理を革新する最小単位の概念を理解する

React の状態管理について学んでいる皆さん、「atom」という言葉を聞いて何を思い浮かべますか?おそらく多くの方は、中学理科で習った「物質を構成する最小単位」を思い出すでしょう。実は、Jotai の atom も、まさにその発想から生まれた革命的なアイデアなのです。

従来の React 状態管理では、大きな一つの状態オブジェクトを定義し、それを分割して使用するアプローチが主流でした。しかし Jotai は、この常識を覆します。最小単位の状態(atom)から出発し、必要に応じて組み合わせていく「ボトムアップ」な発想こそが、現代の複雑なアプリケーション開発に求められていたのです。

本記事では、atom という概念の深層に迫り、なぜこのアプローチが React 開発に革命をもたらすのかを理論的かつ実践的に解説します。物理学の原子論からプログラミングの世界まで、atom の持つ普遍的な価値を紐解いていきましょう。記事を読み終える頃には、あなたも atom の魅力に完全に魅了されているはずです。

atom という革命的な発想の原点

物理学における「原子」の概念

atom の理解を深めるために、まずはその語源である物理学の「原子(atom)」について考えてみましょう。古代ギリシャの哲学者デモクリトスが提唱した原子論は、「物質は、それ以上分割できない最小単位(atomos = 分割できないもの)から構成される」という画期的な概念でした。

この考え方が革命的だったのは、複雑な世界を理解するために「シンプルな要素の組み合わせ」という視点を提供したからです。水も金も空気も、すべては原子の配列によって説明できる。これこそが、Jotai の設計思想の核心にある発想なのです。

typescript// 物理学の原子のように、状態も最小単位から構成する
const nameAtom = atom(''); // 水素のような基本要素
const ageAtom = atom(0); // 酸素のような基本要素
const emailAtom = atom(''); // 炭素のような基本要素

// 複雑な分子(状態)は原子の組み合わせで作る
const userAtom = atom((get) => ({
  name: get(nameAtom),
  age: get(ageAtom),
  email: get(emailAtom),
})); // H2O のような化合物

原子論の美しさは、無限に複雑に見える現象を、有限の基本要素の組み合わせで説明できることにあります。Jotai の atom も同様に、アプリケーションの複雑な状態を、シンプルな基本要素の組み合わせで表現するのです。

プログラミングにおける Atomic 操作

プログラミングの世界では、「Atomic(原子的)」という概念が重要な意味を持ちます。Atomic 操作とは、分割できない一つの単位として実行される操作のことです。データベースのトランザクションや並行プログラミングにおいて、この概念は不可欠です。

typescript// 非Atomic操作(問題がある例)
let counter = 0;
function increment() {
  // この操作は実際には3つのステップに分かれている
  // 1. counterの値を読み取る
  // 2. 1を加算する
  // 3. 結果をcounterに書き戻す
  counter = counter + 1; // 並行実行時に競合状態が発生する可能性
}

// Atomic操作(Jotaiでの例)
const counterAtom = atom(0);
function AtomicCounter() {
  const [count, setCount] = useAtom(counterAtom);

  // この操作は原子的に実行される
  const increment = () => setCount((c) => c + 1);

  return <button onClick={increment}>{count}</button>;
}

Jotai の atom は、この Atomic 操作の概念を状態管理に応用しています。一つの atom への操作は常に原子的であり、部分的な更新によって不整合が生じることがありません。これにより、複雑な状態更新でも安全性が保証されるのです。

React 状態管理への応用とブレイクスルー

React の状態管理において、atom という概念がなぜブレイクスルーとなったのでしょうか。その答えは、粒度の革命にあります。

従来のアプローチでは、状態を大きな塊として定義し、必要な部分を切り出すというトップダウン式の発想が主流でした。しかし、この方法には根本的な問題がありました。

typescript// 従来のトップダウン式アプローチ
interface AppState {
  user: {
    profile: { name: string; age: number };
    preferences: { theme: string; language: string };
    activity: { lastLogin: Date; loginCount: number };
  };
  posts: Post[];
  comments: Comment[];
  ui: {
    sidebar: { isOpen: boolean; width: number };
    modal: { isOpen: boolean; content: string };
  };
}

// 問題1: 関連のない状態が結合している
// 問題2: 部分的な更新が困難
// 問題3: 再レンダリングの範囲が広すぎる

Jotai の atom は、この問題を根本から解決しました。状態を最小単位に分割し、必要に応じて組み合わせるボトムアップ式のアプローチによって、以下の革命的な改善を実現したのです。

従来のアプローチJotai の atom
トップダウン設計ボトムアップ設計
粗い粒度の更新細粒度の更新
強い結合疎な結合
複雑な依存関係明確な依存関係
手動最適化自動最適化
typescript// Jotaiのボトムアップ式アプローチ
const userNameAtom = atom('');
const userAgeAtom = atom(0);
const themeAtom = atom('light');
const sidebarOpenAtom = atom(false);

// 必要に応じて組み合わせる
const userProfileAtom = atom((get) => ({
  name: get(userNameAtom),
  age: get(userAgeAtom),
}));

// 利点1: 独立した更新が可能
// 利点2: 必要な部分のみ再レンダリング
// 利点3: 依存関係が明確
// 利点4: テストが容易

この革命的な発想転換により、React 開発者は複雑性と戦うのではなく、複雑性を管理できるようになったのです。

Jotai の atom が解決する根本的な問題

従来の状態管理における「粒度」の課題

React アプリケーションの規模が大きくなるにつれて、状態管理の「粒度」は開発者にとって深刻な課題となります。粒度が粗すぎると無駄な再レンダリングが発生し、細かすぎると管理が複雑になる。この絶妙なバランスを取ることが、従来の手法では非常に困難でした。

粗い粒度の問題例

typescript// Context APIでの粗い粒度の例
const AppContext = createContext();

function AppProvider({ children }) {
  const [state, setState] = useState({
    user: { name: '', email: '' },
    posts: [],
    ui: { theme: 'light', sidebarOpen: false },
    notifications: [],
  });

  return (
    <AppContext.Provider value={{ state, setState }}>
      {children}
    </AppContext.Provider>
  );
}

// 問題:themeを変更しただけで、user情報を表示している
// 全てのコンポーネントが再レンダリングされる
function ThemeToggle() {
  const { state, setState } = useContext(AppContext);

  const toggleTheme = () => {
    setState((prev) => ({
      ...prev,
      ui: {
        ...prev.ui,
        theme: prev.ui.theme === 'light' ? 'dark' : 'light',
      },
    }));
  };

  return (
    <button onClick={toggleTheme}>テーマ切り替え</button>
  );
}

細かすぎる粒度の問題例

typescript// Reduxでの細かすぎる粒度の例
const userNameReducer = (state = '', action) => {
  switch (action.type) {
    case 'SET_USER_NAME':
      return action.payload;
    default:
      return state;
  }
};

const userEmailReducer = (state = '', action) => {
  switch (action.type) {
    case 'SET_USER_EMAIL':
      return action.payload;
    default:
      return state;
  }
};

// 問題:単純な状態に対してもボイラープレートが大量に必要
// アクション、リデューサー、セレクターの定義が必要

Jotai が実現する理想的な粒度

typescript// 適切な粒度での状態管理
const userNameAtom = atom('');
const userEmailAtom = atom('');
const themeAtom = atom('light');

// 各atomは独立して更新可能
function UserProfile() {
  const [name] = useAtom(userNameAtom);
  // nameが変更された時のみ再レンダリング
  return <div>こんにちは、{name}さん</div>;
}

function ThemeToggle() {
  const [theme, setTheme] = useAtom(themeAtom);
  // themeが変更された時のみ再レンダリング
  return (
    <button
      onClick={() =>
        setTheme((t) => (t === 'light' ? 'dark' : 'light'))
      }
    >
      {theme}モード
    </button>
  );
}

Jotai では、開発者は粒度について悩む必要がありません。atom という自然な単位で状態を定義すれば、適切な粒度が自動的に実現されるからです。

モノリシック state vs アトミック state

従来の状態管理手法の多くは、**モノリシック(一枚岩)**なアプローチを採用していました。アプリケーション全体の状態を一つの大きなオブジェクトとして管理し、そこから必要な部分を取り出すという考え方です。

モノリシック state の特徴と問題

typescript// モノリシックstateの典型例
interface AppState {
  // ユーザー関連(頻繁に変更される)
  currentUser: User | null;
  userPreferences: UserPreferences;

  // データ関連(API経由で不定期に更新)
  posts: Post[];
  comments: Comment[];

  // UI関連(ユーザー操作で頻繁に変更)
  modals: { [key: string]: boolean };
  loading: { [key: string]: boolean };

  // 設定関連(滅多に変更されない)
  appConfig: AppConfig;
}

// 問題1: 更新頻度が異なる状態が混在
// 問題2: 関連性のない状態が結合
// 問題3: 部分的な型安全性の確保が困難
// 問題4: テストが複雑

このモノリシックなアプローチには、以下のような構造的な問題があります:

問題領域具体的な課題開発への影響
更新の波及範囲一部の変更が全体に影響予期しない再レンダリング
依存関係の管理暗黙的で複雑な関係性バグの温床となりやすい
並行開発同じファイルの競合チーム開発の効率低下
テスタビリティ大きな状態の準備が必要テストコードの複雑化

アトミック state の革新性

Jotai のアトミック state は、これらの問題を根本から解決します。

typescript// アトミックstateのアプローチ
// 各関心事が独立したatomとして定義される

// ユーザー関連atom
const currentUserAtom = atom<User | null>(null);
const userPreferencesAtom = atom<UserPreferences>({
  theme: 'light',
  language: 'ja',
});

// データ関連atom
const postsAtom = atom<Post[]>([]);
const commentsAtom = atom<Comment[]>([]);

// UI関連atom
const modalStateAtom = atom<Record<string, boolean>>({});
const loadingStateAtom = atom<Record<string, boolean>>({});

// 設定関連atom
const appConfigAtom = atom<AppConfig>({
  apiEndpoint: process.env.REACT_APP_API_URL,
});

// 必要に応じて組み合わせる(派生atom)
const userDisplayNameAtom = atom((get) => {
  const user = get(currentUserAtom);
  const prefs = get(userPreferencesAtom);

  if (!user) return 'ゲスト';
  return prefs.language === 'ja'
    ? user.nameJa
    : user.nameEn;
});

アトミック state の利点:

  1. 独立性: 各 atom は独立して更新・テスト可能
  2. 組み合わせ可能性: 必要に応じて複数の atom を組み合わせ
  3. 型安全性: 各 atom の型が明確で推論が効く
  4. 再利用性: atom は異なるコンポーネントで再利用可能
  5. デバッグ容易性: 問題のある atom を特定しやすい

「関心の分離」を状態管理で実現する方法

ソフトウェア設計における「関心の分離(Separation of Concerns)」は、システムを独立した機能ごとに分割する重要な原則です。Jotai の atom は、この原則を状態管理の領域で完璧に実現しています。

従来の手法での関心の混在

typescript// 関心が混在している例(アンチパターン)
const useAppState = () => {
  const [state, setState] = useState({
    // ユーザー認証の関心事
    isLoggedIn: false,
    user: null,

    // UI表示の関心事
    theme: 'light',
    sidebarOpen: false,

    // ビジネスロジックの関心事
    cart: [],
    totalPrice: 0,

    // 非同期処理の関心事
    isLoading: false,
    error: null,
  });

  // すべての関心事が一つの関数に混在
  const login = async (credentials) => {
    setState((prev) => ({ ...prev, isLoading: true }));
    try {
      const user = await authAPI.login(credentials);
      setState((prev) => ({
        ...prev,
        isLoggedIn: true,
        user,
        isLoading: false,
        cart: [], // なぜここでカートをリセット?
      }));
    } catch (error) {
      setState((prev) => ({
        ...prev,
        error,
        isLoading: false,
      }));
    }
  };

  return { state, login };
};

Jotai による関心の適切な分離

typescript// 認証に関する関心事
const isLoggedInAtom = atom(false);
const currentUserAtom = atom<User | null>(null);
const authErrorAtom = atom<string | null>(null);

const loginAtom = atom(
  null,
  async (get, set, credentials: LoginCredentials) => {
    set(authErrorAtom, null);

    try {
      const user = await authAPI.login(credentials);
      set(isLoggedInAtom, true);
      set(currentUserAtom, user);
    } catch (error) {
      set(authErrorAtom, error.message);
    }
  }
);

// UI表示に関する関心事
const themeAtom = atom<'light' | 'dark'>('light');
const sidebarOpenAtom = atom(false);

const toggleThemeAtom = atom(null, (get, set) => {
  const currentTheme = get(themeAtom);
  set(
    themeAtom,
    currentTheme === 'light' ? 'dark' : 'light'
  );
});

// ショッピングカートに関する関心事
const cartItemsAtom = atom<CartItem[]>([]);
const cartTotalAtom = atom((get) => {
  const items = get(cartItemsAtom);
  return items.reduce(
    (total, item) => total + item.price * item.quantity,
    0
  );
});

const addToCartAtom = atom(
  null,
  (get, set, item: CartItem) => {
    const currentItems = get(cartItemsAtom);
    set(cartItemsAtom, [...currentItems, item]);
  }
);

// 非同期処理の状態管理
const loadingStatesAtom = atom<Record<string, boolean>>({});

const setLoadingAtom = atom(
  null,
  (
    get,
    set,
    { key, isLoading }: { key: string; isLoading: boolean }
  ) => {
    const current = get(loadingStatesAtom);
    set(loadingStatesAtom, {
      ...current,
      [key]: isLoading,
    });
  }
);

この分離により、以下のメリットが得られます:

1. 独立したテスト可能性

typescript// 認証ロジックのみをテスト
test('ログイン処理', async () => {
  const store = getDefaultStore();

  await store.set(loginAtom, {
    email: 'test@example.com',
    password: 'password',
  });

  expect(store.get(isLoggedInAtom)).toBe(true);
  expect(store.get(currentUserAtom)).toBeTruthy();
});

// UIロジックのみをテスト
test('テーマ切り替え', () => {
  const store = getDefaultStore();

  store.set(toggleThemeAtom);

  expect(store.get(themeAtom)).toBe('dark');
});

2. 独立した開発とメンテナンス

typescript// チームメンバーAは認証機能に集中
// auth/atoms.ts
export { isLoggedInAtom, currentUserAtom, loginAtom };

// チームメンバーBはUI機能に集中
// ui/atoms.ts
export { themeAtom, sidebarOpenAtom, toggleThemeAtom };

// チームメンバーCはビジネスロジックに集中
// cart/atoms.ts
export { cartItemsAtom, cartTotalAtom, addToCartAtom };

3. 再利用性の向上

typescript// UIatomは他のページでも再利用可能
function Header() {
  const [theme] = useAtom(themeAtom);
  const [, toggleTheme] = useAtom(toggleThemeAtom);

  return (
    <header className={theme}>
      <button onClick={toggleTheme}>テーマ切り替え</button>
    </header>
  );
}

// 認証atomは複数のコンポーネントで使用
function LoginForm() {
  const [, login] = useAtom(loginAtom);
  const [authError] = useAtom(authErrorAtom);

  // ログイン処理...
}

function UserProfile() {
  const [user] = useAtom(currentUserAtom);
  const [isLoggedIn] = useAtom(isLoggedInAtom);

  // プロフィール表示...
}

Jotai の atom による関心の分離は、単なる技術的な利点を超えて、チーム開発の効率性、コードの保守性、そしてアプリケーションの拡張性を大幅に向上させるのです。

atom の種類と特性を完全理解

Primitive Atom:最も基本的な状態の容器

Primitive Atom は、Jotai における最も基本的な atom の形態です。「primitive(原始的・基本的)」という名前が示すとおり、他の atom に依存せず、独立した値を保持する状態の容器として機能します。

Primitive Atom の基本構造

typescriptimport { atom } from 'jotai';

// 最もシンプルなPrimitive Atom
const countAtom = atom(0);
const nameAtom = atom('');
const isVisibleAtom = atom(true);

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

const userAtom = atom<User>({
  id: 0,
  name: '',
  email: '',
});

// 配列型のPrimitive Atom
const todosAtom = atom<Todo[]>([]);

// null許容型のPrimitive Atom
const selectedUserAtom = atom<User | null>(null);

Primitive Atom の特性と動作原理

Primitive Atom は以下の重要な特性を持ちます:

  1. 独立性: 他の atom に依存しない自立した状態
  2. 直接更新: useAtom の setter で直接値を更新可能
  3. 型推論: TypeScript が初期値から型を自動推論
  4. メモ化: 同じ値の場合は再レンダリングを回避
typescriptfunction PrimitiveAtomExample() {
  const [count, setCount] = useAtom(countAtom);
  const [user, setUser] = useAtom(userAtom);

  // 直接的な値の更新
  const increment = () => setCount(count + 1);
  const updateUser = (newUser: User) => setUser(newUser);

  // 関数型更新(推奨パターン)
  const incrementSafely = () =>
    setCount((prev) => prev + 1);
  const updateUserName = (newName: string) => {
    setUser((prev) => ({ ...prev, name: newName }));
  };

  return (
    <div>
      <p>カウント: {count}</p>
      <p>ユーザー: {user.name}</p>
      <button onClick={incrementSafely}>+1</button>
      <button onClick={() => updateUserName('新しい名前')}>
        名前変更
      </button>
    </div>
  );
}

Primitive Atom の設計原則

効果的な Primitive Atom を設計するための原則:

typescript// ✅ 良い例:単一責任の原則
const userIdAtom = atom(0);
const userNameAtom = atom('');
const userEmailAtom = atom('');

// ❌ 悪い例:複数の責任が混在
const userAndUIStateAtom = atom({
  user: { id: 0, name: '', email: '' },
  isModalOpen: false,
  currentPage: 'home',
});

// ✅ 良い例:適切な初期値
const themeAtom = atom<'light' | 'dark'>('light');
const languageAtom = atom<'ja' | 'en'>('ja');

// ❌ 悪い例:不適切な初期値
const themeAtom = atom(''); // 型が不明確
const dataAtom = atom(undefined); // undefinedは避ける

Derived Atom:計算された状態の魔法

Derived Atom は、他の atom の値を基に計算される「派生した状態」を表現します。これは関数型プログラミングの概念を状態管理に応用した、Jotai の最も強力で美しい機能の一つです。

読み取り専用 Derived Atom

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

// 派生atom:フルネーム
const fullNameAtom = atom((get) => {
  const firstName = get(firstNameAtom);
  const lastName = get(lastNameAtom);
  return `${lastName} ${firstName}`;
});

// 派生atom:年齢計算
const ageAtom = atom((get) => {
  const birthYear = get(birthYearAtom);
  const currentYear = new Date().getFullYear();
  return currentYear - birthYear;
});

// 派生atom:ユーザー情報サマリー
const userSummaryAtom = atom((get) => {
  const fullName = get(fullNameAtom);
  const age = get(ageAtom);
  return `${fullName}さん(${age}歳)`;
});

// 使用例
function UserDisplay() {
  const [fullName] = useAtom(fullNameAtom);
  const [age] = useAtom(ageAtom);
  const [summary] = useAtom(userSummaryAtom);

  return (
    <div>
      <h2>{fullName}</h2>
      <p>年齢: {age}歳</p>
      <p>{summary}</p>
    </div>
  );
}

書き込み可能 Derived Atom

より高度な Derived Atom では、setter 関数を定義することで書き込み操作も可能になります。

typescript// 読み書き可能なDerived Atom
const userAtom = atom(
  // getter: 複数のatomから統合オブジェクトを作成
  (get) => ({
    firstName: get(firstNameAtom),
    lastName: get(lastNameAtom),
    birthYear: get(birthYearAtom),
  }),
  // setter: 統合オブジェクトから各atomに値を設定
  (
    get,
    set,
    newUser: {
      firstName: string;
      lastName: string;
      birthYear: number;
    }
  ) => {
    set(firstNameAtom, newUser.firstName);
    set(lastNameAtom, newUser.lastName);
    set(birthYearAtom, newUser.birthYear);
  }
);

// 複雑な更新ロジックを持つDerived Atom
const userProfileAtom = atom(
  (get) => get(userAtom),
  (
    get,
    set,
    update: Partial<{
      firstName: string;
      lastName: string;
      birthYear: number;
    }>
  ) => {
    const current = get(userAtom);
    const updated = { ...current, ...update };

    // バリデーション
    if (updated.birthYear > new Date().getFullYear()) {
      throw new Error('生年は未来の年を指定できません');
    }

    set(userAtom, updated);
  }
);

Derived Atom の最適化特性

Derived Atom は自動的に最適化されます:

typescriptconst expensiveComputationAtom = atom((get) => {
  const data = get(dataAtom);
  console.log('重い計算を実行中...'); // 依存atomが変更された時のみ実行

  // 重い計算処理
  return data.map((item) => ({
    ...item,
    processed: heavyComputation(item),
  }));
});

// memoization(メモ化)の動作確認
function ExpensiveComponent() {
  const [result] = useAtom(expensiveComputationAtom);
  const [otherValue, setOtherValue] = useAtom(otherAtom);

  return (
    <div>
      <div>計算結果: {result.length}件</div>
      <button onClick={() => setOtherValue(Date.now())}>
        他の値を変更(重い計算は実行されない)
      </button>
    </div>
  );
}

Write-only Atom:副作用を扱う特殊な atom

Write-only Atom は、値を読み取ることはできませんが、書き込み(実行)によって副作用を引き起こす特殊な atom です。これにより、状態の更新だけでなく、API 呼び出し、ログ出力、複数の atom の一括更新などを管理できます。

基本的な Write-only Atom

typescript// APIへのデータ送信
const saveUserAtom = atom(
  null, // 読み取り値は null(実際には使われない)
  async (get, set, userData: User) => {
    // ローディング状態を開始
    set(isLoadingAtom, true);

    try {
      // API呼び出し
      const savedUser = await userAPI.save(userData);

      // 成功時の状態更新
      set(currentUserAtom, savedUser);
      set(errorAtom, null);

      // 成功通知
      set(notificationAtom, {
        type: 'success',
        message: 'ユーザー情報を保存しました',
      });
    } catch (error) {
      // エラー時の状態更新
      set(errorAtom, error.message);
      set(notificationAtom, {
        type: 'error',
        message: 'ユーザー情報の保存に失敗しました',
      });
    } finally {
      // ローディング状態を終了
      set(isLoadingAtom, false);
    }
  }
);

// 使用例
function UserForm() {
  const [user, setUser] = useAtom(currentUserAtom);
  const [, saveUser] = useAtom(saveUserAtom);
  const [isLoading] = useAtom(isLoadingAtom);

  const handleSubmit = async (formData: User) => {
    await saveUser(formData);
  };

  return (
    <form onSubmit={handleSubmit}>
      {/* フォーム要素 */}
      <button type='submit' disabled={isLoading}>
        {isLoading ? '保存中...' : '保存'}
      </button>
    </form>
  );
}

複雑な副作用管理

typescript// 複数のatomを協調させるWrite-only Atom
const initializeAppAtom = atom(null, async (get, set) => {
  // 初期化開始
  set(initializationStatusAtom, 'loading');

  try {
    // 並列で複数のデータを取得
    const [user, settings, notifications] =
      await Promise.all([
        userAPI.getCurrentUser(),
        settingsAPI.getUserSettings(),
        notificationAPI.getUnreadNotifications(),
      ]);

    // 各atomに値を設定
    set(currentUserAtom, user);
    set(userSettingsAtom, settings);
    set(notificationsAtom, notifications);

    // テーマを復元
    if (settings.theme) {
      set(themeAtom, settings.theme);
    }

    // 初期化完了
    set(initializationStatusAtom, 'success');
  } catch (error) {
    set(initializationStatusAtom, 'error');
    set(errorAtom, error.message);
  }
});

// ログアウト処理
const logoutAtom = atom(null, async (get, set) => {
  try {
    // サーバーサイドでのログアウト処理
    await authAPI.logout();

    // 全ての状態をリセット
    set(currentUserAtom, null);
    set(userSettingsAtom, null);
    set(notificationsAtom, []);
    set(themeAtom, 'light');

    // リダイレクト
    window.location.href = '/login';
  } catch (error) {
    set(errorAtom, 'ログアウト処理に失敗しました');
  }
});

Async Atom:非同期処理を透明化する仕組み

Async Atom は、非同期処理を状態管理に統合する革新的な機能です。Promise を返す atom により、React の Suspense や Error Boundary と自然に連携し、非同期処理を「透明化」します。

基本的な Async Atom

typescript// APIからデータを取得するAsync Atom
const userDataAtom = atom(async () => {
  const response = await fetch('/api/user');
  if (!response.ok) {
    throw new Error('ユーザーデータの取得に失敗しました');
  }
  return response.json();
});

// 依存関係のあるAsync Atom
const userIdAtom = atom(1);

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

  if (!response.ok) {
    throw new Error(
      `ユーザー${userId}の詳細情報取得に失敗`
    );
  }

  return response.json();
});

// 使用例(Suspenseとの連携)
function UserProfile() {
  const [userData] = useAtom(userDataAtom);
  const [userDetail] = useAtom(userDetailAtom);

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

function App() {
  return (
    <ErrorBoundary
      fallback={<div>エラーが発生しました</div>}
    >
      <Suspense fallback={<div>読み込み中...</div>}>
        <UserProfile />
      </Suspense>
    </ErrorBoundary>
  );
}

動的な Async Atom

typescript// 検索機能を持つAsync Atom
const searchQueryAtom = atom('');
const searchFiltersAtom = atom({
  category: 'all',
  sort: 'date',
});

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

  // 空のクエリの場合は検索しない
  if (!query.trim()) {
    return { results: [], total: 0 };
  }

  // デバウンス効果
  await new Promise((resolve) => setTimeout(resolve, 300));

  // クエリが変更されていたらキャンセル
  if (get(searchQueryAtom) !== query) {
    throw new Error('Search cancelled');
  }

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

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

// 検索コンポーネント
function SearchResults() {
  const [query, setQuery] = useAtom(searchQueryAtom);
  const [results] = useAtom(searchResultsAtom);

  return (
    <div>
      <input
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        placeholder='検索キーワード'
      />
      <div>
        {results.results.map((item) => (
          <div key={item.id}>{item.title}</div>
        ))}
      </div>
    </div>
  );
}

書き込み可能な Async Atom

typescript// データの更新と取得を組み合わせたAsync Atom
const userProfileAtom = atom(
  // getter: 現在のユーザープロフィールを取得
  async (get) => {
    const userId = get(currentUserIdAtom);
    const response = await fetch(
      `/api/users/${userId}/profile`
    );
    return response.json();
  },
  // setter: プロフィールを更新
  async (get, set, newProfile: UserProfile) => {
    const userId = get(currentUserIdAtom);

    // 楽観的更新
    const oldProfile = await get(userProfileAtom);
    set(userProfileAtom, Promise.resolve(newProfile));

    try {
      const response = await fetch(
        `/api/users/${userId}/profile`,
        {
          method: 'PUT',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify(newProfile),
        }
      );

      if (!response.ok) {
        throw new Error('プロフィール更新に失敗しました');
      }

      const updatedProfile = await response.json();
      set(userProfileAtom, Promise.resolve(updatedProfile));
    } catch (error) {
      // エラー時は元の値に戻す
      set(userProfileAtom, Promise.resolve(oldProfile));
      throw error;
    }
  }
);

これらの atom の種類を理解することで、Jotai を使った効果的な状態管理の設計が可能になります。次のセクションでは、これらの atom がどのように内部で動作するかを詳しく見ていきましょう。

atom の内部動作メカニズム

依存関係グラフの自動構築

Jotai の最も革新的な特徴の一つは、atom 間の依存関係を自動的に追跡・管理する仕組みです。この依存関係グラフ(Dependency Graph)により、効率的な更新と最適化が実現されています。

依存関係グラフの基本概念

typescript// 依存関係の例
const baseValueAtom = atom(10);
const multiplierAtom = atom(2);

// doubledAtom は baseValueAtom に依存
const doubledAtom = atom((get) => {
  return get(baseValueAtom) * 2;
});

// calculatedAtom は baseValueAtom と multiplierAtom の両方に依存
const calculatedAtom = atom((get) => {
  return get(baseValueAtom) * get(multiplierAtom);
});

// finalResultAtom は doubledAtom と calculatedAtom に依存
const finalResultAtom = atom((get) => {
  return get(doubledAtom) + get(calculatedAtom);
});

// 依存関係グラフ:
// baseValueAtom ──┐
//                ├── doubledAtom ──┐
//                └── calculatedAtom ──┤
// multiplierAtom ──┘                 └── finalResultAtom

この依存関係グラフにより、baseValueAtom が更新された場合、Jotai は自動的に以下の順序で再計算を実行します:

  1. baseValueAtom の値が変更
  2. doubledAtom が再計算(baseValueAtom に依存するため)
  3. calculatedAtom が再計算(baseValueAtommultiplierAtom に依存するため)
  4. finalResultAtom が再計算(doubledAtomcalculatedAtom に依存するため)

動的な依存関係の管理

typescriptconst conditionAtom = atom(true);
const dataAAtom = atom('データA');
const dataBAtom = atom('データB');

// 条件に応じて依存関係が変化するatom
const dynamicAtom = atom((get) => {
  const condition = get(conditionAtom);

  if (condition) {
    return get(dataAAtom); // conditionがtrueの時はdataAAtomに依存
  } else {
    return get(dataBAtom); // conditionがfalseの時はdataBAtomに依存
  }
});

// Jotaiは実行時に実際の依存関係を追跡
// condition=true時: dynamicAtom → conditionAtom, dataAAtom
// condition=false時: dynamicAtom → conditionAtom, dataBAtom

細粒度更新の実現方法

従来の状態管理では、状態の一部が変更されても、その状態を使用しているすべてのコンポーネントが再レンダリングされる問題がありました。Jotai は細粒度更新により、実際に変更された atom を使用するコンポーネントのみを再レンダリングします。

細粒度更新の仕組み

typescript// 独立したatomの定義
const userNameAtom = atom('田中太郎');
const userAgeAtom = atom(25);
const themeAtom = atom('light');

// 各コンポーネントは使用するatomのみを監視
function UserName() {
  const [name] = useAtom(userNameAtom);
  console.log('UserNameコンポーネントが再レンダリング');
  return <div>名前: {name}</div>;
}

function UserAge() {
  const [age] = useAtom(userAgeAtom);
  console.log('UserAgeコンポーネントが再レンダリング');
  return <div>年齢: {age}歳</div>;
}

function ThemeButton() {
  const [theme, setTheme] = useAtom(themeAtom);
  console.log('ThemeButtonコンポーネントが再レンダリング');

  return (
    <button
      onClick={() =>
        setTheme((t) => (t === 'light' ? 'dark' : 'light'))
      }
    >
      {theme}モード
    </button>
  );
}

// userNameAtomが変更された場合:
// → UserNameコンポーネントのみ再レンダリング
// → UserAge、ThemeButtonは再レンダリングされない

購読メカニズムの最適化

typescript// Jotaiの内部的な購読メカニズム(概念的な説明)
class AtomStore {
  private values = new Map();
  private subscribers = new Map();

  get(atom) {
    // 現在実行中のatomがあれば、依存関係を記録
    if (this.currentAtom) {
      this.addDependency(this.currentAtom, atom);
    }

    return this.values.get(atom);
  }

  set(atom, value) {
    const oldValue = this.values.get(atom);

    // 値が実際に変更された場合のみ更新
    if (Object.is(oldValue, value)) {
      return; // 同じ値なら何もしない
    }

    this.values.set(atom, value);

    // このatomを購読しているコンポーネントに通知
    this.notifySubscribers(atom);

    // 依存するatomも更新
    this.updateDependentAtoms(atom);
  }

  subscribe(atom, callback) {
    if (!this.subscribers.has(atom)) {
      this.subscribers.set(atom, new Set());
    }
    this.subscribers.get(atom).add(callback);

    return () => {
      this.subscribers.get(atom).delete(callback);
    };
  }
}

メモ化とキャッシュの仕組み

Jotai は積極的なメモ化により、不必要な再計算を避けてパフォーマンスを最適化しています。

自動メモ化の動作

typescriptconst expensiveDataAtom = atom([
  /* 大量のデータ */
]);

// 重い計算を行うDerived Atom
const processedDataAtom = atom((get) => {
  const data = get(expensiveDataAtom);
  console.log('重い計算を実行...'); // 実際に必要な時のみ実行される

  // 重い処理
  return data.map((item) => {
    return {
      ...item,
      processed: heavyComputation(item),
      cached: new Date().toISOString(),
    };
  });
});

// 使用例
function DataDisplay() {
  const [processedData] = useAtom(processedDataAtom);
  const [, setTheme] = useAtom(themeAtom);

  return (
    <div>
      <div>処理済みデータ: {processedData.length}件</div>
      <button onClick={() => setTheme('dark')}>
        テーマ変更(重い計算は実行されない)
      </button>
    </div>
  );
}

条件付きキャッシュ無効化

typescriptconst cacheKeyAtom = atom('key1');
const rawDataAtom = atom([1, 2, 3, 4, 5]);

// キャッシュキーが変更された時のみ再計算
const cachedProcessedAtom = atom((get) => {
  const cacheKey = get(cacheKeyAtom);
  const data = get(rawDataAtom);

  console.log(`キャッシュキー: ${cacheKey} で計算実行`);

  return {
    key: cacheKey,
    result: data.map((x) => x * 2),
    timestamp: Date.now(),
  };
});

ガベージコレクションとメモリ管理

Jotai は使用されなくなった atom の自動クリーンアップにより、メモリリークを防ぎます。

自動クリーンアップの仕組み

typescript// 動的に作成されるatom
const createUserAtom = (userId: number) => {
  return atom(async () => {
    const response = await fetch(`/api/users/${userId}`);
    return response.json();
  });
};

function UserList() {
  const [users] = useState([1, 2, 3, 4, 5]);

  return (
    <div>
      {users.map((userId) => (
        <UserCard key={userId} userId={userId} />
      ))}
    </div>
  );
}

function UserCard({ userId }: { userId: number }) {
  // 各UserCardが独自のatomを持つ
  const userAtom = useMemo(
    () => createUserAtom(userId),
    [userId]
  );
  const [user] = useAtom(userAtom);

  return <div>{user.name}</div>;
}

// UserCardがアンマウントされると、
// 対応するuserAtomも自動的にクリーンアップされる

実用的な atom 設計パターン

単一責任の原則に基づく atom 設計

効果的な atom 設計の基礎は、単一責任の原則(Single Responsibility Principle)です。各 atom は一つの明確な責任のみを持つべきです。

良い設計例

typescript// ✅ 各atomが単一の責任を持つ
const userIdAtom = atom<number | null>(null);
const userProfileAtom = atom<UserProfile | null>(null);
const userPreferencesAtom = atom<UserPreferences>({
  theme: 'light',
  language: 'ja',
  emailNotifications: true,
});

// ✅ UI状態も適切に分離
const sidebarOpenAtom = atom(false);
const modalOpenAtom = atom(false);
const currentPageAtom = atom<string>('home');

// ✅ ビジネスロジック関連の分離
const cartItemsAtom = atom<CartItem[]>([]);
const orderHistoryAtom = atom<Order[]>([]);
const paymentMethodAtom = atom<PaymentMethod | null>(null);

悪い設計例(アンチパターン)

typescript// ❌ 複数の責任が混在(アンチパターン)
const appStateAtom = atom({
  user: { id: null, profile: null, preferences: {} },
  ui: { sidebarOpen: false, currentPage: 'home' },
  cart: { items: [], total: 0 },
  theme: 'light',
});

// 問題点:
// 1. テーマを変更するだけで全体が再計算される
// 2. カート操作でユーザー関連コンポーネントも影響を受ける
// 3. テストが困難
// 4. 型安全性の確保が難しい

atom 間の適切な依存関係設計

atom 間の依存関係は、アプリケーションの複雑さを管理する重要な要素です。適切に設計された依存関係は、保守性と拡張性を向上させます。

階層的な依存関係設計

typescript// レイヤー1: 基本データ
const userIdAtom = atom<number | null>(null);
const rawUserDataAtom = atom<RawUserData | null>(null);

// レイヤー2: 変換されたデータ
const normalizedUserAtom = atom((get) => {
  const rawData = get(rawUserDataAtom);
  if (!rawData) return null;

  return {
    id: rawData.user_id,
    name: rawData.full_name,
    email: rawData.email_address,
    createdAt: new Date(rawData.created_at),
  };
});

// レイヤー3: 計算されたデータ
const userDisplayNameAtom = atom((get) => {
  const user = get(normalizedUserAtom);
  return user ? `${user.name} (${user.email})` : 'ゲスト';
});

const userAgeAtom = atom((get) => {
  const user = get(normalizedUserAtom);
  if (!user?.createdAt) return null;

  const now = new Date();
  const years =
    now.getFullYear() - user.createdAt.getFullYear();
  return years;
});

// レイヤー4: UI特化データ
const userSummaryAtom = atom((get) => {
  const displayName = get(userDisplayNameAtom);
  const age = get(userAgeAtom);

  return age
    ? `${displayName} - ${age}年前から利用`
    : displayName;
});

循環依存の回避

typescript// ❌ 循環依存(アンチパターン)
const userAtom = atom((get) => {
  const profile = get(profileAtom);
  return { ...profile, type: 'user' };
});

const profileAtom = atom((get) => {
  const user = get(userAtom); // 循環依存!
  return { name: user.name };
});

// ✅ 正しい設計:共通の基礎atomから派生
const baseUserDataAtom = atom({ name: '', email: '' });

const userAtom = atom((get) => {
  const base = get(baseUserDataAtom);
  return { ...base, type: 'user' };
});

const profileAtom = atom((get) => {
  const base = get(baseUserDataAtom);
  return {
    name: base.name,
    displayName: `${base.name}さん`,
  };
});

複雑なビジネスロジックの atom 分割

複雑なビジネスロジックを適切に atom に分割することで、保守性と再利用性を向上させることができます。

ショッピングカートの例

typescript// 基本的なデータatom
const cartItemsAtom = atom<CartItem[]>([]);
const discountCodesAtom = atom<DiscountCode[]>([]);
const shippingMethodAtom = atom<ShippingMethod | null>(
  null
);
const taxRateAtom = atom(0.1); // 10%の税率

// 計算ロジックのatom群
const subtotalAtom = atom((get) => {
  const items = get(cartItemsAtom);
  return items.reduce(
    (total, item) => total + item.price * item.quantity,
    0
  );
});

const discountAmountAtom = atom((get) => {
  const subtotal = get(subtotalAtom);
  const discountCodes = get(discountCodesAtom);

  return discountCodes.reduce((total, code) => {
    if (code.type === 'percentage') {
      return total + (subtotal * code.value) / 100;
    } else {
      return total + code.value;
    }
  }, 0);
});

const shippingCostAtom = atom((get) => {
  const subtotal = get(subtotalAtom);
  const shippingMethod = get(shippingMethodAtom);

  if (!shippingMethod) return 0;
  if (subtotal >= shippingMethod.freeThreshold) return 0;

  return shippingMethod.cost;
});

const taxAmountAtom = atom((get) => {
  const subtotal = get(subtotalAtom);
  const discount = get(discountAmountAtom);
  const taxRate = get(taxRateAtom);

  return (subtotal - discount) * taxRate;
});

const totalAtom = atom((get) => {
  const subtotal = get(subtotalAtom);
  const discount = get(discountAmountAtom);
  const shipping = get(shippingCostAtom);
  const tax = get(taxAmountAtom);

  return subtotal - discount + shipping + tax;
});

// ビジネスロジックのatom(Write-only)
const addToCartAtom = atom(
  null,
  (
    get,
    set,
    {
      product,
      quantity,
    }: { product: Product; quantity: number }
  ) => {
    const currentItems = get(cartItemsAtom);
    const existingItem = currentItems.find(
      (item) => item.productId === product.id
    );

    if (existingItem) {
      // 既存商品の数量を更新
      const updatedItems = currentItems.map((item) =>
        item.productId === product.id
          ? { ...item, quantity: item.quantity + quantity }
          : item
      );
      set(cartItemsAtom, updatedItems);
    } else {
      // 新しい商品を追加
      const newItem: CartItem = {
        productId: product.id,
        name: product.name,
        price: product.price,
        quantity: quantity,
      };
      set(cartItemsAtom, [...currentItems, newItem]);
    }
  }
);

const applyDiscountCodeAtom = atom(
  null,
  async (get, set, code: string) => {
    try {
      // APIでディスカウントコードを検証
      const discountCode = await validateDiscountCode(code);
      const currentCodes = get(discountCodesAtom);

      // 重複チェック
      if (currentCodes.some((c) => c.code === code)) {
        throw new Error('このコードは既に適用されています');
      }

      set(discountCodesAtom, [
        ...currentCodes,
        discountCode,
      ]);
    } catch (error) {
      throw new Error(
        `ディスカウントコードの適用に失敗: ${error.message}`
      );
    }
  }
);

パフォーマンスを考慮した atom 構成

高いパフォーマンスを維持するための atom 構成について考えてみましょう。

選択的更新のパターン

typescript// ❌ パフォーマンスが悪い例
const allUsersAtom = atom<User[]>([]);
const selectedUserIdAtom = atom<number | null>(null);

const selectedUserAtom = atom((get) => {
  const users = get(allUsersAtom); // 全ユーザーリストに依存
  const selectedId = get(selectedUserIdAtom);

  // usersが変更される度に再計算される
  return (
    users.find((user) => user.id === selectedId) || null
  );
});

// ✅ パフォーマンスが良い例
const usersMapAtom = atom<Map<number, User>>(new Map());
const selectedUserIdAtom = atom<number | null>(null);

const selectedUserAtom = atom((get) => {
  const usersMap = get(usersMapAtom);
  const selectedId = get(selectedUserIdAtom);

  // Map.getは高速で、全データの変更に影響されにくい
  return selectedId
    ? usersMap.get(selectedId) || null
    : null;
});

// ユーザー追加時のパフォーマンス最適化
const addUserAtom = atom(
  null,
  (get, set, newUser: User) => {
    const currentMap = get(usersMapAtom);
    const updatedMap = new Map(currentMap);
    updatedMap.set(newUser.id, newUser);
    set(usersMapAtom, updatedMap);
  }
);

遅延計算のパターン

typescript// 重い計算は実際に必要になるまで遅延
const rawDataAtom = atom<RawData[]>([]);
const processingConfigAtom = atom<ProcessingConfig>({
  mode: 'fast',
});

// 計算結果をキャッシュ
const processedDataAtom = atom((get) => {
  const rawData = get(rawDataAtom);
  const config = get(processingConfigAtom);

  console.log('重い処理を実行中...'); // 実際に使用される時のみ実行

  return rawData.map((item) =>
    heavyProcessing(item, config)
  );
});

// 条件付きで計算を実行
const conditionalProcessedAtom = atom((get) => {
  const isEnabled = get(processingEnabledAtom);

  if (!isEnabled) {
    return []; // 無効な場合は計算しない
  }

  return get(processedDataAtom);
});

高度な atom 活用テクニック

動的 atom の生成と管理

大規模なアプリケーションでは、実行時に動的に atom を生成する必要があることがあります。

Atom ファミリーパターン

typescriptimport { atomFamily } from 'jotai/utils';

// ユーザーIDに基づいて動的にatomを生成
const userAtomFamily = atomFamily((userId: number) => {
  return atom(async () => {
    const response = await fetch(`/api/users/${userId}`);
    return response.json();
  });
});

// 使用例
function UserProfile({ userId }: { userId: number }) {
  const userAtom = userAtomFamily(userId);
  const [user] = useAtom(userAtom);

  return <div>{user.name}</div>;
}

// チャットルーム用のatomファミリー
const chatRoomAtomFamily = atomFamily((roomId: string) => {
  return atom<Message[]>([]);
});

const addMessageAtomFamily = atomFamily(
  (roomId: string) => {
    return atom(null, (get, set, message: Message) => {
      const currentMessages = get(
        chatRoomAtomFamily(roomId)
      );
      set(chatRoomAtomFamily(roomId), [
        ...currentMessages,
        message,
      ]);
    });
  }
);

動的 atom 管理のユーティリティ

typescriptclass AtomManager {
  private atomMap = new Map<string, any>();

  getOrCreateAtom<T>(
    key: string,
    factory: () => PrimitiveAtom<T>
  ): PrimitiveAtom<T> {
    if (!this.atomMap.has(key)) {
      this.atomMap.set(key, factory());
    }
    return this.atomMap.get(key);
  }

  removeAtom(key: string) {
    this.atomMap.delete(key);
  }

  clearAll() {
    this.atomMap.clear();
  }
}

// 使用例
const atomManager = new AtomManager();

function DynamicComponent({
  dataKey,
}: {
  dataKey: string;
}) {
  const dataAtom = atomManager.getOrCreateAtom(
    dataKey,
    () => atom<string>('')
  );

  const [data, setData] = useAtom(dataAtom);

  return (
    <input
      value={data}
      onChange={(e) => setData(e.target.value)}
    />
  );
}

atom のコンポジション(合成)パターン

複数の atom を組み合わせて、より複雑な機能を実現するパターンです。

レデューサーパターンの実装

typescript// 状態とアクションの定義
interface TodoState {
  todos: Todo[];
  filter: 'all' | 'active' | 'completed';
}

type TodoAction =
  | { type: 'ADD_TODO'; text: string }
  | { type: 'TOGGLE_TODO'; id: number }
  | {
      type: 'SET_FILTER';
      filter: 'all' | 'active' | 'completed';
    }
  | { type: 'CLEAR_COMPLETED' };

// レデューサー関数
const todoReducer = (
  state: TodoState,
  action: TodoAction
): TodoState => {
  switch (action.type) {
    case 'ADD_TODO':
      return {
        ...state,
        todos: [
          ...state.todos,
          {
            id: Date.now(),
            text: action.text,
            completed: false,
          },
        ],
      };
    case 'TOGGLE_TODO':
      return {
        ...state,
        todos: state.todos.map((todo) =>
          todo.id === action.id
            ? { ...todo, completed: !todo.completed }
            : todo
        ),
      };
    case 'SET_FILTER':
      return { ...state, filter: action.filter };
    case 'CLEAR_COMPLETED':
      return {
        ...state,
        todos: state.todos.filter(
          (todo) => !todo.completed
        ),
      };
    default:
      return state;
  }
};

// atomによるレデューサーパターンの実装
const todoStateAtom = atom<TodoState>({
  todos: [],
  filter: 'all',
});

const todoDispatchAtom = atom(
  null,
  (get, set, action: TodoAction) => {
    const currentState = get(todoStateAtom);
    const newState = todoReducer(currentState, action);
    set(todoStateAtom, newState);
  }
);

// 派生atom
const filteredTodosAtom = atom((get) => {
  const { todos, filter } = get(todoStateAtom);

  switch (filter) {
    case 'active':
      return todos.filter((todo) => !todo.completed);
    case 'completed':
      return todos.filter((todo) => todo.completed);
    default:
      return todos;
  }
});

高階 atom パターン

typescript// atom創作のための高階関数
function createLoadableAtom<T>(
  fetchFn: () => Promise<T>,
  initialValue: T
) {
  const dataAtom = atom<T>(initialValue);
  const loadingAtom = atom(false);
  const errorAtom = atom<Error | null>(null);

  const loadAtom = atom(null, async (get, set) => {
    set(loadingAtom, true);
    set(errorAtom, null);

    try {
      const data = await fetchFn();
      set(dataAtom, data);
    } catch (error) {
      set(errorAtom, error as Error);
    } finally {
      set(loadingAtom, false);
    }
  });

  const stateAtom = atom((get) => ({
    data: get(dataAtom),
    loading: get(loadingAtom),
    error: get(errorAtom),
    reload: () => get(loadAtom),
  }));

  return stateAtom;
}

// 使用例
const userDataLoadableAtom = createLoadableAtom(
  () => fetch('/api/user').then((res) => res.json()),
  null
);

function UserComponent() {
  const [{ data, loading, error, reload }] = useAtom(
    userDataLoadableAtom
  );

  if (loading) return <div>読み込み中...</div>;
  if (error) return <div>エラー: {error.message}</div>;

  return (
    <div>
      <div>{data?.name}</div>
      <button onClick={reload}>再読み込み</button>
    </div>
  );
}

カスタム atom ファクトリの作成

再利用可能な atom パターンをファクトリ関数として抽象化します。

フォーム管理ファクトリ

typescriptinterface FormField<T> {
  value: T;
  error: string | null;
  touched: boolean;
}

interface FormConfig<T> {
  initialValues: T;
  validators: Partial<{
    [K in keyof T]: (value: T[K]) => string | null;
  }>;
}

function createFormAtoms<T extends Record<string, any>>(
  config: FormConfig<T>
) {
  // 各フィールドのatom
  const fieldAtoms = Object.keys(
    config.initialValues
  ).reduce((acc, key) => {
    const fieldKey = key as keyof T;
    acc[fieldKey] = atom<FormField<T[typeof fieldKey]>>({
      value: config.initialValues[fieldKey],
      error: null,
      touched: false,
    });
    return acc;
  }, {} as { [K in keyof T]: WritableAtom<FormField<T[K]>, FormField<T[K]>> });

  // フォーム全体の値
  const valuesAtom = atom((get) => {
    return Object.keys(fieldAtoms).reduce((acc, key) => {
      const fieldKey = key as keyof T;
      acc[fieldKey] = get(fieldAtoms[fieldKey]).value;
      return acc;
    }, {} as T);
  });

  // バリデーション実行
  const validateFieldAtom = atom(
    null,
    (get, set, fieldKey: keyof T) => {
      const field = get(fieldAtoms[fieldKey]);
      const validator = config.validators[fieldKey];

      if (validator) {
        const error = validator(field.value);
        set(fieldAtoms[fieldKey], {
          ...field,
          error,
          touched: true,
        });
      }
    }
  );

  // フォーム送信
  const submitAtom = atom(null, async (get, set) => {
    // 全フィールドのバリデーション
    const fields = Object.keys(fieldAtoms) as Array<
      keyof T
    >;
    for (const fieldKey of fields) {
      set(validateFieldAtom, fieldKey);
    }

    // エラーチェック
    const hasErrors = fields.some((fieldKey) => {
      return get(fieldAtoms[fieldKey]).error !== null;
    });

    if (!hasErrors) {
      const values = get(valuesAtom);
      return values;
    }

    throw new Error('フォームにエラーがあります');
  });

  return {
    fieldAtoms,
    valuesAtom,
    validateFieldAtom,
    submitAtom,
  };
}

// 使用例
const userFormAtoms = createFormAtoms({
  initialValues: {
    name: '',
    email: '',
    age: 0,
  },
  validators: {
    name: (value) =>
      value.length < 2
        ? '名前は2文字以上で入力してください'
        : null,
    email: (value) =>
      !value.includes('@')
        ? '有効なメールアドレスを入力してください'
        : null,
    age: (value) =>
      value < 0 ? '年齢は0以上で入力してください' : null,
  },
});

atom 開発・デバッグのベストプラクティス

atom の開発とデバッグを効率的に行うためのテクニックです。

開発ツールとの連携

typescript// デバッグ用のatom
const debugModeAtom = atom(
  process.env.NODE_ENV === 'development'
);

// ログ機能付きatom
function createLoggableAtom<T>(
  name: string,
  initialValue: T
) {
  const baseAtom = atom(initialValue);

  return atom(
    (get) => get(baseAtom),
    (get, set, newValue: T) => {
      const isDebug = get(debugModeAtom);
      if (isDebug) {
        console.log(
          `[${name}] ${get(baseAtom)}${newValue}`
        );
      }
      set(baseAtom, newValue);
    }
  );
}

// 使用例
const countAtom = createLoggableAtom('counter', 0);

// パフォーマンス測定付きatom
function createMeasurableAtom<T>(
  name: string,
  computeFn: (get: Getter) => T
) {
  return atom((get) => {
    const start = performance.now();
    const result = computeFn(get);
    const end = performance.now();

    if (get(debugModeAtom)) {
      console.log(`[${name}] 計算時間: ${end - start}ms`);
    }

    return result;
  });
}

テスト戦略

typescriptimport { getDefaultStore } from 'jotai';

// atomのテスト例
describe('ユーザー管理atom', () => {
  let store: ReturnType<typeof getDefaultStore>;

  beforeEach(() => {
    store = getDefaultStore();
  });

  test('ユーザー情報の更新', () => {
    const user = {
      id: 1,
      name: '田中太郎',
      email: 'tanaka@example.com',
    };

    store.set(userAtom, user);

    expect(store.get(userAtom)).toEqual(user);
    expect(store.get(userDisplayNameAtom)).toBe(
      '田中太郎 (tanaka@example.com)'
    );
  });

  test('非同期atomのテスト', async () => {
    // APIをモック
    const mockFetch = jest.fn().mockResolvedValue({
      json: () =>
        Promise.resolve({ id: 1, name: 'テストユーザー' }),
    });
    global.fetch = mockFetch;

    const result = await store.get(userDataAtom);

    expect(mockFetch).toHaveBeenCalledWith('/api/user');
    expect(result.name).toBe('テストユーザー');
  });
});

まとめ

本記事では、Jotai の atom という革命的な概念について、その理論的背景から実践的な活用法まで幅広く解説してまいりました。

atom は単なる技術的な機能ではなく、状態管理に対する根本的な発想の転換です。物理学の原子論から着想を得たこのアプローチは、複雑なアプリケーションの状態を「最小単位」から組み立てていくボトムアップ式の思考を可能にします。

従来のモノリシックな状態管理から脱却し、各関心事を独立した atom として定義することで、以下の重要な利益を得ることができます:

技術的利益

  • 細粒度更新による高いパフォーマンス
  • 自動的な依存関係管理による安全性
  • 型安全性の向上とバグの削減
  • テスタビリティの大幅な改善

開発体験の向上

  • 直感的で理解しやすい設計パターン
  • ボイラープレートコードの劇的な削減
  • チーム開発での競合の最小化
  • メンテナンス性の大幅な向上

アーキテクチャ上の利益

  • 関心の分離の完璧な実現
  • 再利用可能なコンポーネント設計
  • 拡張性に優れたアプリケーション構造
  • 複雑性を管理可能なレベルに抑制

atom は、React 開発者が長年抱えてきた状態管理の課題に対する、エレガントで実用的な解決策なのです。小さな atom から始まり、必要に応じて組み合わせていく。この自然なアプローチが、開発者にとっても、アプリケーションにとっても、最適な状態管理を実現します。

Jotai の atom を理解し活用することで、あなたの React 開発は新たな次元へと進化するでしょう。複雑性と戦うのではなく、複雑性を美しく管理する。それが atom という概念が私たちにもたらす、最も価値のある恩恵かもしれません。

関連リンク