T-CREATOR

Jotai クイックリファレンス:atom/read/write/derived の書き方早見表

Jotai クイックリファレンス:atom/read/write/derived の書き方早見表

React の状態管理ライブラリ Jotai を使うとき、「atom の書き方がよくわからない」「derived atom ってどう使うの?」といった疑問をお持ちではありませんか?

この記事では、Jotai の基本的な atom の作成から、read・write 操作、derived atom の活用まで、実際のコードを交えながら体系的にご紹介します。コピペで使える実用的なパターンも多数掲載していますので、開発の現場でお役立てください。

基本的な atom の書き方

Jotai における atom は、アプリケーションの状態を表現する最小単位です。まずは基本的な atom の作成方法から見ていきましょう。

プリミティブ atom

プリミティブ値(文字列、数値、真偽値)を管理する atom の作成方法をご紹介します。

typescriptimport { atom } from 'jotai';

// 文字列 atom
const nameAtom = atom('田中太郎');

// 数値 atom
const countAtom = atom(0);

// 真偽値 atom
const isLoadingAtom = atom(false);

TypeScript を使用する場合は、型を明示的に指定することも可能です。

typescriptimport { atom } from 'jotai';

// 型注釈付きの atom
const userIdAtom = atom<string | null>(null);
const scoreAtom = atom<number>(0);
const isActiveAtom = atom<boolean>(true);

プリミティブ atom は最もシンプルな形式で、単一の値を保持します。初期値として渡した値の型が自動的に推論されるため、通常は型注釈は不要ですが、null を含む可能性がある場合など、明示的に型を指定したいケースでは便利です。

オブジェクト atom

複数の関連するデータをまとめて管理したい場合は、オブジェクト型の atom を作成します。

typescriptimport { atom } from 'jotai';

// ユーザー情報を管理する atom
const userAtom = atom({
  id: 1,
  name: '田中太郎',
  email: 'tanaka@example.com',
  age: 30,
});

// 設定情報を管理する atom
const settingsAtom = atom({
  theme: 'light',
  language: 'ja',
  notifications: true,
});

TypeScript でインターフェースを使用したより厳密な型定義も可能です。

typescriptinterface User {
  id: number;
  name: string;
  email: string;
  age: number;
}

const userAtom = atom<User>({
  id: 1,
  name: '田中太郎',
  email: 'tanaka@example.com',
  age: 30,
});

オブジェクト atom を使用することで、関連するデータを一つの atom で管理でき、コンポーネント間での状態共有が効率的に行えます。

配列 atom

リスト形式のデータを管理する場合は、配列型の atom を作成しましょう。

typescriptimport { atom } from 'jotai';

// 商品リストを管理する atom
const productsAtom = atom([
  { id: 1, name: 'ノートパソコン', price: 89800 },
  { id: 2, name: 'マウス', price: 2980 },
  { id: 3, name: 'キーボード', price: 5980 },
]);

// 単純な文字列配列の atom
const tagsAtom = atom(['React', 'TypeScript', 'Jotai']);

// 空配列から始める atom
const todosAtom = atom<string[]>([]);

配列 atom では、要素の追加・削除・更新といった操作を後述する write 系操作で行います。TypeScript を使用する場合、空配列から始める際は型注釈を付けることで、後の操作で型安全性を保てます。

read 系操作

atom から値を読み取る操作について詳しく見ていきます。read 系操作では get 関数を使用して、他の atom の値を参照できます。

get を使った読み取り

get 関数は atom の現在の値を取得するための基本的な関数です。

typescriptimport { atom } from 'jotai';

const firstNameAtom = atom('太郎');
const lastNameAtom = atom('田中');

// 他の atom を参照する derived atom
const fullNameAtom = atom((get) => {
  const firstName = get(firstNameAtom);
  const lastName = get(lastNameAtom);
  return `${lastName} ${firstName}`;
});

get 関数を使用することで、他の atom の値をリアルタイムで参照できます。参照元の atom が更新されると、自動的に derived atom も再計算されます。

条件分岐を含む読み取り操作も可能です。

typescriptconst isLoggedInAtom = atom(false);
const userAtom = atom({ name: '田中太郎', role: 'admin' });

const displayNameAtom = atom((get) => {
  const isLoggedIn = get(isLoggedInAtom);
  if (!isLoggedIn) {
    return 'ゲスト';
  }

  const user = get(userAtom);
  return user.name;
});

複数 atom の組み合わせ

複数の atom を組み合わせて、より複雑な計算を行う derived atom を作成できます。

typescriptconst priceAtom = atom(1000);
const taxRateAtom = atom(0.1);
const discountAtom = atom(0.05);

// 複数の atom を組み合わせた計算
const finalPriceAtom = atom((get) => {
  const price = get(priceAtom);
  const taxRate = get(taxRateAtom);
  const discount = get(discountAtom);

  const discountedPrice = price * (1 - discount);
  const finalPrice = discountedPrice * (1 + taxRate);

  return Math.round(finalPrice);
});

配列 atom を使った集計処理の例もご紹介します。

typescriptconst itemsAtom = atom([
  { name: '商品A', price: 1000, quantity: 2 },
  { name: '商品B', price: 1500, quantity: 1 },
  { name: '商品C', price: 800, quantity: 3 },
]);

// 合計金額を計算する derived atom
const totalAmountAtom = atom((get) => {
  const items = get(itemsAtom);
  return items.reduce((total, item) => {
    return total + item.price * item.quantity;
  }, 0);
});

// 商品数を計算する derived atom
const itemCountAtom = atom((get) => {
  const items = get(itemsAtom);
  return items.length;
});

複数の atom を組み合わせることで、単一の atom では表現できない複雑なビジネスロジックを効率的に実装できます。

非同期読み取り

Jotai では Promise を返す非同期 atom も簡単に作成できます。

typescript// 非同期でユーザー情報を取得する atom
const userIdAtom = atom(1);

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

複数の非同期操作を組み合わせることも可能です。

typescriptconst userIdAtom = atom(1);

// ユーザー基本情報を取得
const userInfoAtom = atom(async (get) => {
  const userId = get(userIdAtom);
  const response = await fetch(`/api/users/${userId}`);
  return response.json();
});

// ユーザーの投稿を取得
const userPostsAtom = atom(async (get) => {
  const userId = get(userIdAtom);
  const response = await fetch(
    `/api/users/${userId}/posts`
  );
  return response.json();
});

// ユーザー情報と投稿を組み合わせる
const userProfileAtom = atom(async (get) => {
  const [userInfo, userPosts] = await Promise.all([
    get(userInfoAtom),
    get(userPostsAtom),
  ]);

  return {
    ...userInfo,
    posts: userPosts,
    postCount: userPosts.length,
  };
});

非同期 atom を使用することで、API からのデータ取得や重い計算処理を効率的に管理できます。エラーハンドリングも組み込むことで、より堅牢なアプリケーションを作成できます。

write 系操作

atom の値を更新する write 系操作について詳しく解説します。write 系操作では set 関数を使用して atom の値を変更できます。

set を使った更新

基本的な set 関数の使用方法から見ていきましょう。

typescriptimport { atom } from 'jotai';

const countAtom = atom(0);

// カウンターを増加させる derived atom
const incrementAtom = atom(
  null, // read 関数は null
  (get, set) => {
    const currentCount = get(countAtom);
    set(countAtom, currentCount + 1);
  }
);

// カウンターを減少させる derived atom
const decrementAtom = atom(null, (get, set) => {
  const currentCount = get(countAtom);
  set(countAtom, currentCount - 1);
});

write-only の atom を作成する場合、第一引数(read 関数)に null を指定します。第二引数の write 関数で、get を使って現在の値を取得し、set で新しい値を設定します。

文字列や真偽値の更新例もご紹介します。

typescriptconst messageAtom = atom('初期メッセージ');
const isVisibleAtom = atom(true);

// メッセージを更新する atom
const updateMessageAtom = atom(
  null,
  (get, set, newMessage: string) => {
    set(messageAtom, newMessage);
  }
);

// 表示/非表示を切り替える atom
const toggleVisibilityAtom = atom(null, (get, set) => {
  const currentVisibility = get(isVisibleAtom);
  set(isVisibleAtom, !currentVisibility);
});

update 関数での更新

より複雑な更新処理には、関数を使った更新パターンが便利です。

typescriptconst userAtom = atom({
  id: 1,
  name: '田中太郎',
  email: 'tanaka@example.com',
  profile: {
    age: 30,
    department: '開発部',
  },
});

// ユーザー名を更新する atom
const updateUserNameAtom = atom(
  null,
  (get, set, newName: string) => {
    const currentUser = get(userAtom);
    set(userAtom, {
      ...currentUser,
      name: newName,
    });
  }
);

// プロフィール情報を更新する atom
const updateProfileAtom = atom(
  null,
  (
    get,
    set,
    updates: Partial<{ age: number; department: string }>
  ) => {
    const currentUser = get(userAtom);
    set(userAtom, {
      ...currentUser,
      profile: {
        ...currentUser.profile,
        ...updates,
      },
    });
  }
);

配列の更新操作についても見てみましょう。

typescriptconst todosAtom = atom([
  { id: 1, text: 'Jotai を学ぶ', completed: false },
  { id: 2, text: '記事を書く', completed: false },
]);

// 新しい TODO を追加する atom
const addTodoAtom = atom(
  null,
  (
    get,
    set,
    newTodo: {
      id: number;
      text: string;
      completed: boolean;
    }
  ) => {
    const currentTodos = get(todosAtom);
    set(todosAtom, [...currentTodos, newTodo]);
  }
);

// TODO を削除する atom
const removeTodoAtom = atom(
  null,
  (get, set, todoId: number) => {
    const currentTodos = get(todosAtom);
    set(
      todosAtom,
      currentTodos.filter((todo) => todo.id !== todoId)
    );
  }
);

// TODO の完了状態を切り替える atom
const toggleTodoAtom = atom(
  null,
  (get, set, todoId: number) => {
    const currentTodos = get(todosAtom);
    set(
      todosAtom,
      currentTodos.map((todo) =>
        todo.id === todoId
          ? { ...todo, completed: !todo.completed }
          : todo
      )
    );
  }
);

これらの更新パターンを使用することで、イミュータブルな状態更新を簡潔に記述できます。

非同期書き込み

API への送信など、非同期処理を伴う書き込み操作も実装できます。

typescriptconst userAtom = atom({
  id: 1,
  name: '田中太郎',
  email: 'tanaka@example.com',
});

const isLoadingAtom = atom(false);

// ユーザー情報をサーバーに保存する非同期 atom
const saveUserAtom = atom(
  null,
  async (get, set, userData: Partial<typeof userAtom>) => {
    set(isLoadingAtom, true);

    try {
      const currentUser = get(userAtom);
      const updatedUser = { ...currentUser, ...userData };

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

      if (!response.ok) {
        throw new Error('保存に失敗しました');
      }

      const savedUser = await response.json();
      set(userAtom, savedUser);
    } catch (error) {
      console.error('ユーザー保存エラー:', error);
      // エラー処理...
    } finally {
      set(isLoadingAtom, false);
    }
  }
);

複数の atom を同時に更新する複雑な非同期処理の例もご紹介します。

typescriptconst postsAtom = atom([]);
const currentPageAtom = atom(1);
const totalPagesAtom = atom(0);
const isLoadingAtom = atom(false);

// ページネーション付きデータ取得の atom
const loadPostsAtom = atom(
  null,
  async (get, set, page: number) => {
    set(isLoadingAtom, true);

    try {
      const response = await fetch(
        `/api/posts?page=${page}&limit=10`
      );
      const data = await response.json();

      // 複数の atom を同時に更新
      set(postsAtom, data.posts);
      set(currentPageAtom, data.currentPage);
      set(totalPagesAtom, data.totalPages);
    } catch (error) {
      console.error('投稿取得エラー:', error);
      set(postsAtom, []);
    } finally {
      set(isLoadingAtom, false);
    }
  }
);

非同期書き込み操作を使用することで、サーバーとの通信を含む複雑な状態管理を効率的に実装できます。

derived atom の活用

derived atom は既存の atom から新しい値を計算して生成する、Jotai の強力な機能です。計算、フィルタリング、変換といった様々な用途で活用できます。

計算 atom

既存の atom の値を使って計算を行う derived atom の作成方法をご紹介します。

typescriptimport { atom } from 'jotai';

// 基本的な数値 atom
const widthAtom = atom(100);
const heightAtom = atom(50);

// 面積を計算する derived atom
const areaAtom = atom((get) => {
  const width = get(widthAtom);
  const height = get(heightAtom);
  return width * height;
});

// 周囲の長さを計算する derived atom
const perimeterAtom = atom((get) => {
  const width = get(widthAtom);
  const height = get(heightAtom);
  return 2 * (width + height);
});

より複雑な計算処理の例として、ショッピングカートの合計金額計算を見てみましょう。

typescriptconst cartItemsAtom = atom([
  {
    id: 1,
    name: '商品A',
    price: 1000,
    quantity: 2,
    taxRate: 0.1,
  },
  {
    id: 2,
    name: '商品B',
    price: 1500,
    quantity: 1,
    taxRate: 0.1,
  },
  {
    id: 3,
    name: '商品C',
    price: 800,
    quantity: 3,
    taxRate: 0.08,
  },
]);

// 小計(税抜き)を計算
const subtotalAtom = atom((get) => {
  const items = get(cartItemsAtom);
  return items.reduce((total, item) => {
    return total + item.price * item.quantity;
  }, 0);
});

// 消費税合計を計算
const totalTaxAtom = atom((get) => {
  const items = get(cartItemsAtom);
  return items.reduce((totalTax, item) => {
    const itemSubtotal = item.price * item.quantity;
    const itemTax = itemSubtotal * item.taxRate;
    return totalTax + itemTax;
  }, 0);
});

// 総計を計算
const grandTotalAtom = atom((get) => {
  const subtotal = get(subtotalAtom);
  const totalTax = get(totalTaxAtom);
  return subtotal + totalTax;
});

計算 atom を使用することで、複雑なビジネスロジックを分かりやすく分割して管理できます。

フィルタリング atom

配列 atom から特定の条件に合致する要素のみを抽出する derived atom の作成方法です。

typescriptconst usersAtom = atom([
  {
    id: 1,
    name: '田中太郎',
    department: '開発部',
    isActive: true,
  },
  {
    id: 2,
    name: '佐藤花子',
    department: '営業部',
    isActive: true,
  },
  {
    id: 3,
    name: '鈴木一郎',
    department: '開発部',
    isActive: false,
  },
  {
    id: 4,
    name: '高橋美咲',
    department: '人事部',
    isActive: true,
  },
]);

// アクティブなユーザーのみを取得
const activeUsersAtom = atom((get) => {
  const users = get(usersAtom);
  return users.filter((user) => user.isActive);
});

// 開発部のユーザーのみを取得
const developmentUsersAtom = atom((get) => {
  const users = get(usersAtom);
  return users.filter(
    (user) => user.department === '開発部'
  );
});

検索機能と組み合わせたフィルタリングの例もご紹介します。

typescriptconst searchTermAtom = atom('');
const selectedDepartmentAtom = atom('全て');

// 検索とフィルタを組み合わせた derived atom
const filteredUsersAtom = atom((get) => {
  const users = get(usersAtom);
  const searchTerm = get(searchTermAtom);
  const selectedDepartment = get(selectedDepartmentAtom);

  return users.filter((user) => {
    const matchesSearch = user.name.includes(searchTerm);
    const matchesDepartment =
      selectedDepartment === '全て' ||
      user.department === selectedDepartment;
    const isActive = user.isActive;

    return matchesSearch && matchesDepartment && isActive;
  });
});

複数の条件を組み合わせることで、柔軟な検索・フィルタリング機能を実装できます。

変換 atom

既存の atom の値を異なる形式に変換する derived atom の作成方法です。

typescriptconst rawDataAtom = atom([
  {
    id: 1,
    firstName: '太郎',
    lastName: '田中',
    birthDate: '1990-05-15',
  },
  {
    id: 2,
    firstName: '花子',
    lastName: '佐藤',
    birthDate: '1988-12-03',
  },
  {
    id: 3,
    firstName: '一郎',
    lastName: '鈴木',
    birthDate: '1992-08-20',
  },
]);

// 表示用にデータを変換する derived atom
const displayDataAtom = atom((get) => {
  const rawData = get(rawDataAtom);

  return rawData.map((person) => {
    const fullName = `${person.lastName} ${person.firstName}`;
    const birthYear = new Date(
      person.birthDate
    ).getFullYear();
    const age = new Date().getFullYear() - birthYear;

    return {
      id: person.id,
      fullName,
      age,
      ageGroup:
        age < 30
          ? '20代以下'
          : age < 40
          ? '30代'
          : '40代以上',
    };
  });
});

API レスポンスを UI で使いやすい形式に変換する例です。

typescriptconst apiResponseAtom = atom({
  user_info: {
    user_id: 1,
    display_name: '田中太郎',
    email_address: 'tanaka@example.com',
  },
  settings: {
    theme_preference: 'dark',
    notification_enabled: true,
    language_code: 'ja',
  },
});

// キャメルケースに変換し、構造を整理する derived atom
const normalizedUserAtom = atom((get) => {
  const response = get(apiResponseAtom);

  return {
    user: {
      id: response.user_info.user_id,
      name: response.user_info.display_name,
      email: response.user_info.email_address,
    },
    preferences: {
      theme: response.settings.theme_preference,
      notifications: response.settings.notification_enabled,
      language: response.settings.language_code,
    },
  };
});

変換 atom を使用することで、外部からのデータを内部の一貫した形式に変換し、コンポーネントでの利用を簡単にできます。

実践的な書き方パターン

実際の開発でよく使われる Jotai のパターンと、パフォーマンス最適化のテクニックをご紹介します。

よく使う組み合わせ

フォーム管理でよく使われるパターンから見ていきましょう。

typescript// フォームデータを管理する atom
const formDataAtom = atom({
  name: '',
  email: '',
  age: 0,
  department: '',
});

// バリデーションエラーを管理する atom
const validationErrorsAtom = atom((get) => {
  const data = get(formDataAtom);
  const errors: string[] = [];

  if (!data.name.trim()) {
    errors.push('名前は必須です');
  }

  if (!data.email.includes('@')) {
    errors.push('有効なメールアドレスを入力してください');
  }

  if (data.age < 18) {
    errors.push('18歳以上である必要があります');
  }

  return errors;
});

// フォームが有効かどうかを判定する atom
const isFormValidAtom = atom((get) => {
  const errors = get(validationErrorsAtom);
  return errors.length === 0;
});

// 個別フィールドを更新する atom たち
const updateNameAtom = atom(
  null,
  (get, set, newName: string) => {
    const current = get(formDataAtom);
    set(formDataAtom, { ...current, name: newName });
  }
);

const updateEmailAtom = atom(
  null,
  (get, set, newEmail: string) => {
    const current = get(formDataAtom);
    set(formDataAtom, { ...current, email: newEmail });
  }
);

モーダル管理のパターンも頻繁に使用されます。

typescript// モーダルの状態を管理
const modalStateAtom = atom({
  isOpen: false,
  type: null as 'confirm' | 'alert' | 'form' | null,
  title: '',
  content: '',
  data: null as any,
});

// モーダルを開く atom
const openModalAtom = atom(
  null,
  (
    get,
    set,
    modalConfig: {
      type: 'confirm' | 'alert' | 'form';
      title: string;
      content: string;
      data?: any;
    }
  ) => {
    set(modalStateAtom, {
      isOpen: true,
      ...modalConfig,
      data: modalConfig.data || null,
    });
  }
);

// モーダルを閉じる atom
const closeModalAtom = atom(null, (get, set) => {
  set(modalStateAtom, {
    isOpen: false,
    type: null,
    title: '',
    content: '',
    data: null,
  });
});

パフォーマンス最適化

大きなリストを扱う際のパフォーマンス最適化パターンをご紹介します。

typescript// 大きなデータセット
const allItemsAtom = atom<Item[]>([]);

// ページング用の設定
const pageSizeAtom = atom(20);
const currentPageAtom = atom(1);

// 現在のページのアイテムのみを計算
const currentPageItemsAtom = atom((get) => {
  const allItems = get(allItemsAtom);
  const pageSize = get(pageSizeAtom);
  const currentPage = get(currentPageAtom);

  const startIndex = (currentPage - 1) * pageSize;
  const endIndex = startIndex + pageSize;

  return allItems.slice(startIndex, endIndex);
});

// 検索結果をメモ化する atom
const searchResultsAtom = atom((get) => {
  const allItems = get(allItemsAtom);
  const searchTerm = get(searchTermAtom);

  // 検索語が短い場合は早期リターン
  if (searchTerm.length < 2) {
    return allItems;
  }

  return allItems.filter(
    (item) =>
      item.name
        .toLowerCase()
        .includes(searchTerm.toLowerCase()) ||
      item.description
        .toLowerCase()
        .includes(searchTerm.toLowerCase())
  );
});

ローカルストレージとの連携パターンも実用的です。

typescript// ローカルストレージから初期値を読み込む関数
const loadFromStorage = (
  key: string,
  defaultValue: any
) => {
  try {
    const stored = localStorage.getItem(key);
    return stored ? JSON.parse(stored) : defaultValue;
  } catch {
    return defaultValue;
  }
};

// ローカルストレージに保存する関数
const saveToStorage = (key: string, value: any) => {
  try {
    localStorage.setItem(key, JSON.stringify(value));
  } catch (error) {
    console.error(
      'ローカルストレージへの保存に失敗:',
      error
    );
  }
};

// ローカルストレージ連携 atom
const userPreferencesAtom = atom(
  loadFromStorage('userPreferences', {
    theme: 'light',
    language: 'ja',
    fontSize: 14,
  })
);

// 設定を更新し、ローカルストレージにも保存する atom
const updatePreferencesAtom = atom(
  null,
  (get, set, updates: Partial<UserPreferences>) => {
    const current = get(userPreferencesAtom);
    const newPreferences = { ...current, ...updates };

    set(userPreferencesAtom, newPreferences);
    saveToStorage('userPreferences', newPreferences);
  }
);

以下は非同期処理でのエラーハンドリングとローディング状態管理のパターンです。

typescript// API の状態を管理する atom
const apiStateAtom = atom({
  isLoading: false,
  error: null as string | null,
  data: null as any,
});

// 安全な API 呼び出しを行う atom
const fetchDataAtom = atom(
  null,
  async (get, set, url: string) => {
    set(apiStateAtom, {
      isLoading: true,
      error: null,
      data: null,
    });

    try {
      const response = await fetch(url);

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

      const data = await response.json();
      set(apiStateAtom, {
        isLoading: false,
        error: null,
        data,
      });
    } catch (error) {
      const errorMessage =
        error instanceof Error
          ? error.message
          : '不明なエラー';
      set(apiStateAtom, {
        isLoading: false,
        error: errorMessage,
        data: null,
      });
    }
  }
);

これらのパターンを組み合わせることで、効率的で保守性の高い状態管理を実現できます。

まとめ

この記事では、Jotai における atom の基本的な書き方から、read・write 操作、derived atom の活用まで、実践的なパターンを幅広くご紹介しました。

基本的な atom では、プリミティブ値、オブジェクト、配列の管理方法を学びました。これらの基礎をしっかりと理解することで、より複雑な状態管理にも対応できます。

read 系操作 では、get 関数を使った値の読み取りや、複数の atom を組み合わせた計算、非同期処理について解説しました。特に derived atom の概念は、Jotai の強力な機能の一つです。

write 系操作 では、set 関数を使った更新処理や、非同期書き込み操作について詳しく見てきました。イミュータブルな更新パターンを理解することで、予測可能な状態管理が可能になります。

derived atom の活用 では、計算、フィルタリング、変換といった実用的なパターンをご紹介しました。これらを使いこなすことで、複雑なビジネスロジックを分かりやすく分割できます。

実践的なパターン では、フォーム管理やモーダル制御、パフォーマンス最適化など、実際の開発でよく遭遇する課題への対処法をお示ししました。

Jotai の atom 設計では、小さく分割された atom を組み合わせることで、保守性と再利用性の高いコードを書けます。今回ご紹介したパターンを参考に、ぜひあなたのプロジェクトでも Jotai を活用してみてください。

関連リンク