T-CREATOR

動的な atom を生成するJotai の atomFamily の使いどころ - ID ごとの状態管理を効率化する

動的な atom を生成するJotai の atomFamily の使いどころ - ID ごとの状態管理を効率化する

React アプリケーションを開発していて、「同じような状態管理ロジックを何度も書いている」「ID ごとに異なる状態を管理するのが煩雑」と感じたことはありませんか?そんな悩みを抱えている開発者の皆様に、今回は Jotai の atomFamily という素晴らしい機能をご紹介します。

この記事では、動的に atom を生成する atomFamily の魅力と実践的な使いどころを、具体的なコード例とともに詳しく解説していきます。きっと、あなたの状態管理への考え方が変わる発見があるはずです。

atomFamily とは何か

atomFamily は、Jotai ライブラリが提供する強力な機能の一つで、動的に atom を生成するためのファクトリー関数です。通常の atom が一つの状態を管理するのに対し、atomFamily はパラメータに基づいて複数の関連する atom を効率的に管理できます。

atomFamily の基本的な概念を表で整理してみましょう。

#項目atomatomFamily
1状態の数単一複数(動的)
2生成タイミング静的動的
3パラメータなしあり
4用途グローバル状態ID 別・カテゴリ別状態

この表を見ると、atomFamily がいかに柔軟な状態管理を可能にするかがお分かりいただけるでしょう。

javascriptimport { atomFamily } from 'jotai/utils';

// 基本的な atomFamily の定義
const countFamily = atomFamily((id) => 0);

// 使用例
const count1 = countFamily('user1'); // user1 用のカウンター
const count2 = countFamily('user2'); // user2 用のカウンター

上記のコードでは、countFamily という関数を定義し、異なる ID に対してそれぞれ独立したカウンター状態を作成しています。まるで状態管理の魔法のようですね!

従来の状態管理における課題

多くの開発者が直面する状態管理の課題を見てみましょう。例えば、複数のユーザー情報を管理する場合を考えてみてください。

javascript// 従来のアプローチ(問題のあるパターン)
const user1Atom = atom({ name: '', email: '' });
const user2Atom = atom({ name: '', email: '' });
const user3Atom = atom({ name: '', email: '' });
// ... ユーザーが増えるたびに新しい atom を手動で定義

// または、すべてを一つの atom で管理
const usersAtom = atom({
  user1: { name: '', email: '' },
  user2: { name: '', email: '' },
  user3: { name: '', email: '' },
});

このアプローチでは、以下のような深刻な問題が発生します:

1. スケーラビリティの問題

ユーザー数が動的に変化するアプリケーションでは、事前にすべての atom を定義することは不可能です。SNS アプリやチャットアプリなど、リアルタイムでユーザーが追加される環境では、この制約が致命的になります。

2. メモリ効率の悪化

すべての状態を一つの大きな atom で管理すると、一部の変更でも全体の再レンダリングが発生し、パフォーマンスが大幅に低下してしまいます。

3. コードの重複と保守性の低下

同じような状態管理ロジックを何度も書くことになり、バグの温床となってしまいます。

javascript// よくある失敗例:エラーが発生しやすいパターン
const handleUserUpdate = (userId, field, value) => {
  setUsers((prev) => ({
    ...prev,
    [userId]: {
      ...prev[userId], // prev[userId] が undefined の場合エラー
      [field]: value,
    },
  }));
};

上記のコードは、prev[userId] が存在しない場合に以下のエラーが発生します:

javascriptTypeError: Cannot read properties of undefined (reading 'name')

こうした問題に遭遇したとき、「もっと良い方法があるはず」と感じるのは、開発者として正しい直感なのです。

atomFamily が解決する問題

atomFamily は、これらの課題を根本的に解決してくれます。その解決アプローチを詳しく見ていきましょう。

1. 動的な状態生成

atomFamily を使えば、必要な時に必要な分だけ atom を生成できます。

javascriptimport { atomFamily } from 'jotai/utils';

// ユーザー情報を管理する atomFamily
const userAtomFamily = atomFamily((userId) => ({
  name: '',
  email: '',
  isActive: true,
}));

// 動的にユーザー状態を取得・作成
const getUserAtom = (userId) => userAtomFamily(userId);

この方法なら、新しいユーザーが登録された瞬間に、そのユーザー専用の状態管理が自動的に準備されます。まるで、必要な道具が必要な時に現れてくれるような魔法のような体験ですね。

2. 効率的なメモリ使用

各ユーザーの状態が独立しているため、一人のユーザー情報が更新されても、他のユーザーに関連するコンポーネントは再レンダリングされません。

javascript// 効率的な更新処理
const UserProfile = ({ userId }) => {
  const [user, setUser] = useAtom(userAtomFamily(userId));

  const updateName = (newName) => {
    // この更新は他のユーザーのコンポーネントに影響しない
    setUser((prev) => ({ ...prev, name: newName }));
  };

  return (
    <div>
      <input
        value={user.name}
        onChange={(e) => updateName(e.target.value)}
      />
    </div>
  );
};

3. 型安全性の向上

TypeScript との組み合わせで、より安全な開発が可能になります。

typescriptinterface User {
  name: string;
  email: string;
  isActive: boolean;
}

// 型安全な atomFamily の定義
const userAtomFamily = atomFamily<User, string>(
  (userId: string) => ({
    name: '',
    email: '',
    isActive: true,
  })
);

atomFamily の基本的な使い方

それでは、atomFamily の基本的な使い方を段階的に学んでいきましょう。

インストールと初期設定

まず、必要なパッケージをインストールします。

bashyarn add jotai

基本的な定義方法

atomFamily の基本的な定義は以下のようになります。

javascriptimport { atomFamily } from 'jotai/utils';

// 基本形:初期値を返す関数
const basicFamily = atomFamily((param) => initialValue);

// より実践的な例
const todoFamily = atomFamily((todoId) => ({
  id: todoId,
  text: '',
  completed: false,
  createdAt: new Date(),
}));

パラメータの活用

atomFamily のパラメータは、文字列や数値だけでなく、オブジェクトも使用できます。

javascript// 複合パラメータの例
const cacheFamily = atomFamily(({ url, method }) => ({
  data: null,
  loading: false,
  error: null,
}));

// 使用例
const userDataAtom = cacheFamily({
  url: '/api/users/123',
  method: 'GET',
});
const postsDataAtom = cacheFamily({
  url: '/api/posts',
  method: 'GET',
});

計算された atom との組み合わせ

atomFamily は、計算された atom との組み合わせでより強力になります。

javascript// 基本データ
const userFamily = atomFamily((userId) => ({
  name: '',
  posts: [],
}));

// 計算された atom
const userPostCountFamily = atomFamily((userId) =>
  atom((get) => {
    const user = get(userFamily(userId));
    return user.posts.length;
  })
);

この計算された atom により、ユーザーの投稿数を効率的に取得できるようになります。データが更新されるたびに自動的に再計算されるのは、まさに React の醍醐味ですね。

実践例:ID ごとの Todo 管理システム

実際のアプリケーションで atomFamily がどのように活用できるかを、Todo 管理システムを例に詳しく見ていきましょう。

Todo データ構造の設計

まず、Todo アイテムの基本構造を定義します。

typescriptinterface Todo {
  id: string;
  text: string;
  completed: boolean;
  priority: 'low' | 'medium' | 'high';
  createdAt: Date;
  updatedAt: Date;
}

// Todo 管理のための atomFamily
const todoFamily = atomFamily<Todo, string>(
  (todoId: string) => ({
    id: todoId,
    text: '',
    completed: false,
    priority: 'medium',
    createdAt: new Date(),
    updatedAt: new Date(),
  })
);

Todo リスト管理

すべての Todo ID を管理するための atom も必要です。

javascript// Todo ID のリストを管理
const todoIdsAtom = atom([]);

// 新しい Todo を追加する関数
const addTodoAtom = atom(null, (get, set, text) => {
  const newId = `todo-${Date.now()}-${Math.random()}`;
  const todoIds = get(todoIdsAtom);

  // ID リストを更新
  set(todoIdsAtom, [...todoIds, newId]);

  // 新しい Todo を初期化
  set(todoFamily(newId), {
    id: newId,
    text,
    completed: false,
    priority: 'medium',
    createdAt: new Date(),
    updatedAt: new Date(),
  });

  return newId;
});

Todo コンポーネントの実装

個別の Todo アイテムを表示・編集するコンポーネントを作成します。

jsximport { useAtom } from 'jotai';

const TodoItem = ({ todoId }) => {
  const [todo, setTodo] = useAtom(todoFamily(todoId));

  const toggleCompleted = () => {
    setTodo((prev) => ({
      ...prev,
      completed: !prev.completed,
      updatedAt: new Date(),
    }));
  };

  const updateText = (newText) => {
    setTodo((prev) => ({
      ...prev,
      text: newText,
      updatedAt: new Date(),
    }));
  };

  return (
    <div
      className={`todo-item ${
        todo.completed ? 'completed' : ''
      }`}
    >
      <input
        type='checkbox'
        checked={todo.completed}
        onChange={toggleCompleted}
      />
      <input
        type='text'
        value={todo.text}
        onChange={(e) => updateText(e.target.value)}
        placeholder='Todo を入力してください'
      />
      <span className='priority'>{todo.priority}</span>
    </div>
  );
};

Todo リスト全体の表示

すべての Todo を表示するメインコンポーネントです。

jsxconst TodoList = () => {
  const [todoIds] = useAtom(todoIdsAtom);
  const [, addTodo] = useAtom(addTodoAtom);
  const [newTodoText, setNewTodoText] = useState('');

  const handleAddTodo = () => {
    if (newTodoText.trim()) {
      addTodo(newTodoText.trim());
      setNewTodoText('');
    }
  };

  return (
    <div className='todo-list'>
      <div className='add-todo'>
        <input
          type='text'
          value={newTodoText}
          onChange={(e) => setNewTodoText(e.target.value)}
          placeholder='新しい Todo を追加'
          onKeyPress={(e) =>
            e.key === 'Enter' && handleAddTodo()
          }
        />
        <button onClick={handleAddTodo}>追加</button>
      </div>

      <div className='todos'>
        {todoIds.map((todoId) => (
          <TodoItem key={todoId} todoId={todoId} />
        ))}
      </div>
    </div>
  );
};

この実装の素晴らしい点は、各 Todo アイテムが完全に独立していることです。一つの Todo を編集しても、他の Todo コンポーネントは全く影響を受けません。

エラーハンドリングの実装

実際のアプリケーションでは、エラーハンドリングも重要です。

javascript// エラー処理を含む Todo 更新
const updateTodoAtom = atom(
  null,
  async (get, set, { todoId, updates }) => {
    try {
      const currentTodo = get(todoFamily(todoId));

      // API に更新を送信
      const response = await fetch(`/api/todos/${todoId}`, {
        method: 'PUT',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          ...currentTodo,
          ...updates,
        }),
      });

      if (!response.ok) {
        throw new Error(
          `HTTP error! status: ${response.status}`
        );
      }

      // 成功時のみ状態を更新
      set(todoFamily(todoId), (prev) => ({
        ...prev,
        ...updates,
        updatedAt: new Date(),
      }));
    } catch (error) {
      console.error('Todo update failed:', error);
      // エラー処理(ユーザーに通知など)
      throw error;
    }
  }
);

よくあるエラーとして、存在しない Todo ID を参照した場合の処理も考慮しましょう。

javascript// 安全な Todo 取得
const safeTodoFamily = atomFamily((todoId) => {
  if (!todoId || typeof todoId !== 'string') {
    throw new Error(`Invalid todo ID: ${todoId}`);
  }

  return {
    id: todoId,
    text: '',
    completed: false,
    priority: 'medium',
    createdAt: new Date(),
    updatedAt: new Date(),
  };
});

このエラーハンドリングにより、以下のような一般的なエラーを防ぐことができます:

javascriptError: Invalid todo ID: undefined
TypeError: Cannot read properties of null (reading 'id')

実践例:ユーザー情報の動的管理

次に、より複雑なユーザー情報管理システムを atomFamily で実装してみましょう。リアルタイムでユーザーが追加・削除される環境を想定します。

ユーザーデータ構造の定義

typescriptinterface UserProfile {
  id: string;
  name: string;
  email: string;
  avatar: string;
  isOnline: boolean;
  lastSeen: Date;
  preferences: {
    theme: 'light' | 'dark';
    notifications: boolean;
    language: string;
  };
}

// ユーザー情報管理の atomFamily
const userProfileFamily = atomFamily<UserProfile, string>(
  (userId: string) => ({
    id: userId,
    name: '',
    email: '',
    avatar: '',
    isOnline: false,
    lastSeen: new Date(),
    preferences: {
      theme: 'light',
      notifications: true,
      language: 'ja',
    },
  })
);

オンラインユーザー管理

チャットアプリなどでよく使われる、オンラインユーザーの管理機能を実装します。

javascript// オンラインユーザーの ID リスト
const onlineUsersAtom = atom([]);

// ユーザーのオンライン状態を更新する atom
const updateUserOnlineStatusAtom = atom(
  null,
  (get, set, { userId, isOnline }) => {
    // ユーザープロファイルを更新
    set(userProfileFamily(userId), (prev) => ({
      ...prev,
      isOnline,
      lastSeen: new Date(),
    }));

    // オンラインユーザーリストを更新
    const onlineUsers = get(onlineUsersAtom);
    if (isOnline && !onlineUsers.includes(userId)) {
      set(onlineUsersAtom, [...onlineUsers, userId]);
    } else if (!isOnline) {
      set(
        onlineUsersAtom,
        onlineUsers.filter((id) => id !== userId)
      );
    }
  }
);

ユーザー検索機能

大量のユーザーが存在する場合の検索機能も実装してみましょう。

javascript// 検索結果をキャッシュする atomFamily
const searchResultsFamily = atomFamily((query) =>
  atom(async (get) => {
    if (!query || query.length < 2) return [];

    try {
      const response = await fetch(
        `/api/users/search?q=${encodeURIComponent(query)}`
      );
      if (!response.ok) {
        throw new Error(
          `Search failed: ${response.status}`
        );
      }

      const users = await response.json();

      // 検索結果のユーザー情報をキャッシュ
      users.forEach((user) => {
        set(userProfileFamily(user.id), user);
      });

      return users.map((user) => user.id);
    } catch (error) {
      console.error('User search error:', error);
      return [];
    }
  })
);

ユーザー一覧コンポーネント

検索機能付きのユーザー一覧を表示するコンポーネントです。

jsxconst UserList = () => {
  const [searchQuery, setSearchQuery] = useState('');
  const [searchResults] = useAtom(
    searchResultsFamily(searchQuery)
  );
  const [onlineUsers] = useAtom(onlineUsersAtom);

  // デバウンス処理(検索の実行を遅延)
  const debouncedQuery = useDebounce(searchQuery, 300);

  useEffect(() => {
    // 検索クエリが変更された時の処理
  }, [debouncedQuery]);

  return (
    <div className='user-list'>
      <div className='search-section'>
        <input
          type='text'
          placeholder='ユーザーを検索...'
          value={searchQuery}
          onChange={(e) => setSearchQuery(e.target.value)}
        />
      </div>

      <div className='online-users'>
        <h3>オンラインユーザー ({onlineUsers.length}名)</h3>
        {onlineUsers.map((userId) => (
          <UserCard key={userId} userId={userId} />
        ))}
      </div>

      {searchResults.length > 0 && (
        <div className='search-results'>
          <h3>検索結果</h3>
          {searchResults.map((userId) => (
            <UserCard key={userId} userId={userId} />
          ))}
        </div>
      )}
    </div>
  );
};

個別ユーザーカードコンポーネント

各ユーザーの情報を表示するカードコンポーネントです。

jsxconst UserCard = ({ userId }) => {
  const [user] = useAtom(userProfileFamily(userId));

  const formatLastSeen = (date) => {
    const now = new Date();
    const diff = now - date;
    const minutes = Math.floor(diff / 60000);

    if (minutes < 1) return '今';
    if (minutes < 60) return `${minutes}分前`;

    const hours = Math.floor(minutes / 60);
    if (hours < 24) return `${hours}時間前`;

    const days = Math.floor(hours / 24);
    return `${days}日前`;
  };

  return (
    <div
      className={`user-card ${
        user.isOnline ? 'online' : 'offline'
      }`}
    >
      <img
        src={user.avatar || '/default-avatar.png'}
        alt={user.name}
        className='avatar'
        onError={(e) => {
          e.target.src = '/default-avatar.png';
        }}
      />

      <div className='user-info'>
        <h4>{user.name || 'Unknown User'}</h4>
        <p className='email'>{user.email}</p>

        <div className='status'>
          <span
            className={`status-indicator ${
              user.isOnline ? 'online' : 'offline'
            }`}
          />
          {user.isOnline
            ? 'オンライン'
            : `最終ログイン: ${formatLastSeen(
                user.lastSeen
              )}`}
        </div>
      </div>
    </div>
  );
};

WebSocket を使ったリアルタイム更新

実際のアプリケーションでは、WebSocket を使ってリアルタイムでユーザー状態を更新することが多いでしょう。

javascript// WebSocket 管理用の atom
const websocketAtom = atom(null);

// WebSocket 接続とイベント処理
const initializeWebSocketAtom = atom(null, (get, set) => {
  const ws = new WebSocket(
    'wss://your-websocket-server.com'
  );

  ws.onopen = () => {
    console.log('WebSocket connected');
    set(websocketAtom, ws);
  };

  ws.onmessage = (event) => {
    try {
      const data = JSON.parse(event.data);

      switch (data.type) {
        case 'USER_ONLINE':
          set(updateUserOnlineStatusAtom, {
            userId: data.userId,
            isOnline: true,
          });
          break;

        case 'USER_OFFLINE':
          set(updateUserOnlineStatusAtom, {
            userId: data.userId,
            isOnline: false,
          });
          break;

        case 'USER_PROFILE_UPDATE':
          set(userProfileFamily(data.userId), (prev) => ({
            ...prev,
            ...data.updates,
          }));
          break;

        default:
          console.warn('Unknown message type:', data.type);
      }
    } catch (error) {
      console.error(
        'WebSocket message parsing error:',
        error
      );
    }
  };

  ws.onerror = (error) => {
    console.error('WebSocket error:', error);
  };

  ws.onclose = () => {
    console.log('WebSocket disconnected');
    set(websocketAtom, null);
    // 再接続ロジックをここに実装
  };
});

この実装により、ユーザーの状態変更がリアルタイムでアプリケーション全体に反映されます。各ユーザーの状態が独立して管理されているため、パフォーマンスの心配もありません。

パフォーマンス最適化のポイント

atomFamily を使用する際の重要なパフォーマンス最適化のポイントをご紹介します。これらのテクニックを知っているかどうかで、アプリケーションの品質が大きく変わります。

1. メモリリークの防止

atomFamily は使用されなくなった atom を自動的にガベージコレクトしませんので、明示的な cleanup が必要です。

javascript// メモリリーク対策の実装
const cleanupAtom = atom(null, (get, set, userIds) => {
  // 不要なユーザー atom をクリーンアップ
  userIds.forEach((userId) => {
    // atom を削除する前に、使用されていないことを確認
    const isStillInUse = checkIfUserIsStillInUse(userId);
    if (!isStillInUse) {
      // Jotai のガベージコレクションをトリガー
      set(userProfileFamily.remove(userId));
    }
  });
});

// 定期的なクリーンアップの実行
useEffect(() => {
  const interval = setInterval(() => {
    // 5分ごとに未使用の atom をクリーンアップ
    const inactiveUserIds = getInactiveUserIds();
    setCleanup(inactiveUserIds);
  }, 5 * 60 * 1000);

  return () => clearInterval(interval);
}, []);

2. 懒読み込み(Lazy Loading)

必要になった時にのみデータを読み込む戦略を実装しましょう。

javascript// 懒読み込み対応の atomFamily
const lazyUserDataFamily = atomFamily((userId) =>
  atom(async (get) => {
    // キャッシュから確認
    const cached = get(userCacheFamily(userId));
    if (cached && isValidCache(cached)) {
      return cached.data;
    }

    try {
      // API からデータを取得
      const response = await fetch(`/api/users/${userId}`);
      if (!response.ok) {
        throw new Error(
          `Failed to fetch user: ${response.status}`
        );
      }

      const userData = await response.json();

      // キャッシュに保存
      set(userCacheFamily(userId), {
        data: userData,
        timestamp: Date.now(),
        ttl: 5 * 60 * 1000, // 5分間有効
      });

      return userData;
    } catch (error) {
      console.error(`Error loading user ${userId}:`, error);
      throw error;
    }
  })
);

3. バッチ更新の実装

複数の状態を一度に更新する場合は、バッチ処理を活用しましょう。

javascript// バッチ更新用の atom
const batchUpdateUsersAtom = atom(
  null,
  (get, set, updates) => {
    // React の unstable_batchedUpdates を使用
    unstable_batchedUpdates(() => {
      updates.forEach(({ userId, data }) => {
        set(userProfileFamily(userId), (prev) => ({
          ...prev,
          ...data,
          updatedAt: new Date(),
        }));
      });
    });
  }
);

// 使用例
const handleBulkUserUpdate = (userUpdates) => {
  setBatchUpdateUsers(userUpdates);
};

4. 選択的な再レンダリング最適化

必要な部分のみを再レンダリングするためのセレクターを活用します。

javascript// ユーザー名のみを監視するセレクター
const userNameFamily = atomFamily((userId) =>
  atom((get) => {
    const user = get(userProfileFamily(userId));
    return user.name;
  })
);

// オンライン状態のみを監視するセレクター
const userOnlineStatusFamily = atomFamily((userId) =>
  atom((get) => {
    const user = get(userProfileFamily(userId));
    return user.isOnline;
  })
);

// コンポーネントでの使用
const UserNameDisplay = ({ userId }) => {
  const [userName] = useAtom(userNameFamily(userId));
  // ユーザー名が変更された時のみ再レンダリング
  return <span>{userName}</span>;
};

5. パフォーマンス監視の実装

実際のパフォーマンスを監視するためのツールも実装しておきましょう。

javascript// パフォーマンス監視用の atom
const performanceMetricsAtom = atom({
  atomCount: 0,
  memoryUsage: 0,
  renderCount: 0,
});

// atom の使用状況を監視
const atomUsageTrackerAtom = atom(null, (get, set) => {
  const metrics = get(performanceMetricsAtom);

  // 現在のメモリ使用量を取得(概算)
  const memoryUsage = performance.memory
    ? performance.memory.usedJSHeapSize
    : 0;

  set(performanceMetricsAtom, {
    ...metrics,
    memoryUsage,
    renderCount: metrics.renderCount + 1,
  });
});

注意点とベストプラクティス

atomFamily を効果的に使用するための重要な注意点とベストプラクティスをまとめました。これらを理解することで、より堅牢なアプリケーションを構築できます。

1. パラメータの正規化

atomFamily のパラメータは、一意性を保つために正規化する必要があります。

javascript// 悪い例:パラメータが正規化されていない
const badFamily = atomFamily((param) => {
  // { id: 1 } と { id: '1' } が異なる atom を生成してしまう
  return initialValue;
});

// 良い例:パラメータを正規化
const goodFamily = atomFamily((param) => {
  const normalizedParam =
    typeof param === 'object'
      ? JSON.stringify(param, Object.keys(param).sort()) // キーをソート
      : String(param);

  return initialValue;
});

// より安全な実装
const safeUserFamily = atomFamily((userId) => {
  if (!userId) {
    throw new Error('User ID is required');
  }

  // 文字列に正規化
  const normalizedId = String(userId).trim();
  if (!normalizedId) {
    throw new Error('Invalid user ID');
  }

  return {
    id: normalizedId,
    name: '',
    email: '',
  };
});

2. TypeScript での型安全性

TypeScript を使用する場合は、適切な型定義が重要です。

typescript// 型安全な atomFamily の定義
interface UserState {
  id: string;
  name: string;
  email: string;
  isActive: boolean;
}

// パラメータと戻り値の型を明示的に指定
const typedUserFamily = atomFamily<UserState, string>(
  (userId: string): UserState => ({
    id: userId,
    name: '',
    email: '',
    isActive: true,
  })
);

// ジェネリクスを使用したより柔軟な実装
interface AtomFamilyConfig<T, P> {
  initialValue: (param: P) => T;
  validator?: (param: P) => boolean;
}

const createTypedAtomFamily = <T, P>(
  config: AtomFamilyConfig<T, P>
) => {
  return atomFamily<T, P>((param: P) => {
    if (config.validator && !config.validator(param)) {
      throw new Error(`Invalid parameter: ${param}`);
    }
    return config.initialValue(param);
  });
};

3. エラーハンドリングの戦略

適切なエラーハンドリングを実装することで、アプリケーションの安定性が向上します。

javascript// エラー状態を含む atomFamily
const userWithErrorFamily = atomFamily((userId) =>
  atom({
    data: null,
    loading: false,
    error: null,
  })
);

// エラーハンドリング付きの非同期 atom
const asyncUserFamily = atomFamily((userId) =>
  atom(async (get) => {
    const userState = get(userWithErrorFamily(userId));

    // ローディング状態を設定
    set(userWithErrorFamily(userId), {
      ...userState,
      loading: true,
      error: null,
    });

    try {
      const response = await fetch(`/api/users/${userId}`);

      if (!response.ok) {
        if (response.status === 404) {
          throw new Error(`User not found: ${userId}`);
        } else if (response.status === 403) {
          throw new Error(
            `Access denied for user: ${userId}`
          );
        } else {
          throw new Error(
            `HTTP ${response.status}: ${response.statusText}`
          );
        }
      }

      const userData = await response.json();

      // 成功時の状態更新
      set(userWithErrorFamily(userId), {
        data: userData,
        loading: false,
        error: null,
      });

      return userData;
    } catch (error) {
      // エラー時の状態更新
      set(userWithErrorFamily(userId), {
        data: null,
        loading: false,
        error: error.message,
      });

      throw error;
    }
  })
);

4. テストの書き方

atomFamily のテストは、通常の atom とは少し異なるアプローチが必要です。

javascriptimport { createStore } from 'jotai';
import { userProfileFamily } from './atoms';

describe('userProfileFamily', () => {
  let store;

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

  test('should create independent atoms for different user IDs', () => {
    const user1Atom = userProfileFamily('user1');
    const user2Atom = userProfileFamily('user2');

    // 初期値の確認
    expect(store.get(user1Atom).id).toBe('user1');
    expect(store.get(user2Atom).id).toBe('user2');

    // 独立性の確認
    store.set(user1Atom, {
      ...store.get(user1Atom),
      name: 'Alice',
    });
    expect(store.get(user1Atom).name).toBe('Alice');
    expect(store.get(user2Atom).name).toBe(''); // 影響されない
  });

  test('should handle invalid user ID', () => {
    expect(() => {
      userProfileFamily('');
    }).toThrow('Invalid user ID');
  });

  test('should maintain state consistency', async () => {
    const userAtom = userProfileFamily('test-user');

    // 非同期更新のテスト
    const updatePromise = store.set(
      asyncUserFamily('test-user')
    );

    // ローディング状態の確認
    expect(
      store.get(userWithErrorFamily('test-user')).loading
    ).toBe(true);

    await updatePromise;

    // 完了状態の確認
    expect(
      store.get(userWithErrorFamily('test-user')).loading
    ).toBe(false);
  });
});

5. デバッグとログ

開発時のデバッグを効率化するためのログ機能を実装しましょう。

javascript// デバッグ用のミドルウェア
const debugAtomFamily = atomFamily((id) => {
  const baseAtom = atom(initialValue);

  if (process.env.NODE_ENV === 'development') {
    return atom(
      (get) => {
        const value = get(baseAtom);
        console.log(`[AtomFamily] Get ${id}:`, value);
        return value;
      },
      (get, set, newValue) => {
        console.log(`[AtomFamily] Set ${id}:`, newValue);
        set(baseAtom, newValue);
      }
    );
  }

  return baseAtom;
});

// パフォーマンス監視付きの atomFamily
const performanceAwareFamily = atomFamily((id) => {
  const startTime = performance.now();

  return atom((get) => {
    const value = get(baseAtom);
    const endTime = performance.now();

    if (endTime - startTime > 100) {
      console.warn(
        `[Performance] Slow atom access for ${id}: ${
          endTime - startTime
        }ms`
      );
    }

    return value;
  });
});

これらのベストプラクティスを守ることで、maintainable で高性能な atomFamily 実装が可能になります。特に大規模なアプリケーションでは、これらの配慮が品質の差となって現れるでしょう。

まとめ

atomFamily は、React アプリケーションの状態管理を革新的に改善してくれる素晴らしいツールです。この記事を通じて、以下の重要なポイントをご理解いただけたと思います。

atomFamily の価値

#ポイント従来の方法atomFamily
1動的状態管理事前定義が必要必要時に自動生成
2メモリ効率全体再レンダリング個別最適化
3保守性コード重複DRY 原則の実現
4スケーラビリティ限定的無制限に近い

開発者にとっての意味

atomFamily を習得することで、あなたの開発スタイルは大きく変わるでしょう。「この状態管理は複雑すぎる」と感じていた問題も、atomFamily を使えばシンプルかつエレガントに解決できるようになります。

特に以下のような場面で、その真価を実感していただけるはずです:

  • 大規模なリスト管理:数千のアイテムを持つリストでも、パフォーマンスを損なうことなく管理できます
  • リアルタイムアプリケーション:チャットアプリやコラボレーションツールでの状態同期が劇的に簡単になります
  • 動的な UI 生成:ユーザーの操作に応じて動的にコンポーネントが生成される場面でも、状態管理の心配がなくなります

次のステップ

この記事で学んだ知識を実際のプロジェクトで活用してみてください。最初は小さな機能から始めて、徐々に複雑な状態管理に適用していくことをお勧めします。

atomFamily は単なるツールではありません。それは、より良いユーザー体験を提供するための、あなたの強力なパートナーです。効率的で美しい状態管理により、ユーザーの皆様により良いアプリケーションを届けることができるでしょう。

今日から、あなたの React アプリケーションが一段階レベルアップすることを確信しています。素晴らしい開発体験をお楽しみください!

関連リンク