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

React アプリケーションを開発していて、「同じような状態管理ロジックを何度も書いている」「ID ごとに異なる状態を管理するのが煩雑」と感じたことはありませんか?そんな悩みを抱えている開発者の皆様に、今回は Jotai の atomFamily
という素晴らしい機能をご紹介します。
この記事では、動的に atom を生成する atomFamily の魅力と実践的な使いどころを、具体的なコード例とともに詳しく解説していきます。きっと、あなたの状態管理への考え方が変わる発見があるはずです。
atomFamily とは何か
atomFamily は、Jotai ライブラリが提供する強力な機能の一つで、動的に atom を生成するためのファクトリー関数です。通常の atom が一つの状態を管理するのに対し、atomFamily はパラメータに基づいて複数の関連する atom を効率的に管理できます。
atomFamily の基本的な概念を表で整理してみましょう。
# | 項目 | atom | atomFamily |
---|---|---|---|
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 アプリケーションが一段階レベルアップすることを確信しています。素晴らしい開発体験をお楽しみください!
関連リンク
- article
動的な atom を生成するJotai の atomFamily の使いどころ - ID ごとの状態管理を効率化する
- article
巨大な atom は分割して統治せよ! splitAtom ユーティリティでリストのレンダリングを高速化する
- article
あなたの Jotai アプリ、遅くない?React DevTools と debugLabel でボトルネックを特定し、最適化する手順
- article
Jotai アプリのパフォーマンスを極限まで高める selectAtom 活用術 - 不要な再レンダリングを撲滅せよ
- article
ローディングとエラー状態をスマートに分離。Jotai の loadable ユーティリティ徹底活用ガイド
- article
Jotai vs React Query(TanStack Query) - データフェッチング状態管理の使い分け
- article
動的な atom を生成するJotai の atomFamily の使いどころ - ID ごとの状態管理を効率化する
- article
アニメーションで CV 率 UP?React を使った UI 改善実践録
- article
ゼロから作る!React でスムーズなローディングアニメーションを実装する方法
- article
ユーザー体験を劇的に変える!React で実装するマイクロインタラクション事例集
- article
CSS だけじゃ物足りない!React アプリを動かす JS アニメーション 10 選
- article
React × Three.js で実現するインタラクティブ 3D アニメーション最前線
- review
今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
- review
ついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
- review
愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
- review
週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
- review
新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
- review
科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来