T-CREATOR

Jotai エコシステム最前線:公式&コミュニティ拡張の地図と選び方

Jotai エコシステム最前線:公式&コミュニティ拡張の地図と選び方

React の状態管理ライブラリとして注目を集める Jotai ですが、その真価はコアライブラリだけではありません。公式から提供される多様な拡張パッケージや、活発なコミュニティによって開発されたツール群が、Jotai をより強力で柔軟な状態管理ソリューションへと進化させています。

本記事では、Jotai のエコシステム全体を俯瞰し、公式パッケージとコミュニティ製ツールの違いを理解し、プロジェクトのニーズに合わせて最適な拡張を選択するための実践的なガイドを提供します。

背景

Jotai のシンプルさと拡張性

Jotai は、React の状態管理を「atom(原子)」という小さな単位で管理するライブラリとして 2020 年に登場しました。Recoil にインスパイアされながらも、よりシンプルで軽量な API を提供することで、多くの開発者に支持されてきました。

Jotai のコア設計思想は「小さく始めて、必要に応じて拡張する」というものです。基本的な状態管理機能はコアライブラリに集約され、特定のユースケースに対応する機能は別パッケージとして提供されています。

エコシステムの成長過程

Jotai のエコシステムは、以下のような段階を経て成長してきました。

2020 年後半から 2021 年にかけて、コアライブラリの安定化が進み、基本的な atom 操作の API が確立されました。その後、2021 年後半からは公式チームによる拡張パッケージの開発が本格化し、現在では 10 種類以上の公式パッケージが提供されています。

同時に、コミュニティによる貢献も活発化しています。開発者ツール、テストユーティリティ、他ライブラリとの統合など、多様なニーズに応える拡張が生まれてきました。

以下の図は、Jotai エコシステムの全体像を示しています。

mermaidflowchart TB
  core["jotai<br/>(コアライブラリ)"]

  subgraph official["公式パッケージ群"]
    utils["jotai/utils<br/>(ユーティリティ)"]
    devtools["jotai-devtools<br/>(開発者ツール)"]
    query["jotai-tanstack-query<br/>(データフェッチ)"]
    xstate["jotai-xstate<br/>(ステートマシン)"]
    immer["jotai-immer<br/>(イミュータブル)"]
  end

  subgraph community["コミュニティ拡張"]
    testing["テストツール"]
    integrations["他ライブラリ統合"]
    helpers["ヘルパーライブラリ"]
  end

  core --> official
  core --> community
  official --> apps["React アプリケーション"]
  community --> apps

このように、Jotai は中核となるコアライブラリを軸に、公式パッケージとコミュニティ拡張が両輪となって、豊富なエコシステムを形成しています。

課題

拡張選択の難しさ

Jotai のエコシステムが豊かになる一方で、開発者は新たな課題に直面しています。それは「どの拡張を選ぶべきか」という選択の問題です。

多くの開発者が以下のような疑問を抱えています。

公式パッケージとコミュニティ製ツールの違いは何でしょうか。同じような機能を持つパッケージが複数ある場合、どれを選べば良いのでしょうか。また、プロジェクトの規模や要件に応じて、どのような拡張を導入すべきなのでしょうか。

パッケージ情報の分散

Jotai の拡張に関する情報は、公式ドキュメント、GitHub リポジトリ、コミュニティのブログ記事など、様々な場所に分散しています。

特に以下の点で情報収集が困難になっています。

#課題具体的な問題
1ドキュメントの更新頻度最新の機能や API の変更が反映されるまでにタイムラグがある
2ユースケースの不明確さ各拡張がどのような場面で威力を発揮するのか分かりにくい
3互換性情報の不足パッケージ同士の組み合わせ可否や、React バージョンとの互換性が不明瞭
4メンテナンス状況コミュニティ製ツールの保守状況や将来性が見えにくい

以下の図は、開発者が拡張選択時に直面する意思決定フローを表しています。

mermaidflowchart TD
  start["拡張の必要性を認識"] --> purpose["目的を明確化"]
  purpose --> search["候補パッケージを検索"]
  search --> evaluate["評価基準で比較"]

  evaluate --> official{"公式パッケージ<br/>が存在?"}
  official -->|Yes| check_official["公式パッケージを検証"]
  official -->|No| check_community["コミュニティ製を探索"]

  check_official --> decide_official{"要件を<br/>満たす?"}
  decide_official -->|Yes| adopt_official["公式パッケージを採用"]
  decide_official -->|No| check_community

  check_community --> decide_community{"適切なものが<br/>存在?"}
  decide_community -->|Yes| adopt_community["コミュニティ製を採用"]
  decide_community -->|No| custom["自作を検討"]

  adopt_official --> done["導入完了"]
  adopt_community --> done
  custom --> done

この意思決定プロセスには多くの判断ポイントがあり、適切な情報がなければ最適な選択をすることが難しくなっています。

バージョン管理とアップデートの複雑さ

Jotai のコアライブラリと各拡張パッケージは、それぞれ独立したリリースサイクルを持っています。

これにより、以下のような管理上の課題が生じます。

コアライブラリがメジャーアップデートされた際、全ての拡張が同時に対応するとは限りません。特定の拡張だけが古いバージョンに依存している場合、依存関係の解決が複雑になります。

また、コミュニティ製ツールの中には、メンテナンスが滞っているものもあります。そのようなパッケージを採用してしまうと、後々のアップデート時に問題が発生する可能性があります。

解決策

公式パッケージの全体像把握

Jotai の公式チームが提供するパッケージは、以下のカテゴリに分類できます。

ユーティリティ系パッケージ

jotai​/​utils は、Jotai を使う上で頻繁に必要となる便利なユーティリティ関数を提供します。

typescriptimport {
  atomWithStorage,
  atomWithDefault,
} from 'jotai/utils';

// localStorageと同期するatom
const darkModeAtom = atomWithStorage('darkMode', false);

// 非同期のデフォルト値を持つatom
const userAtom = atomWithDefault(async () => {
  const response = await fetch('/api/user');
  return response.json();
});

上記のコードは、ブラウザの localStorage と自動的に同期する状態や、非同期でデフォルト値を取得する状態を簡単に作成できることを示しています。

jotai​/​utils には他にも、atom 同士の結合や、状態のリセット、家族(atom family)の管理など、実践的な機能が含まれています。

データフェッチ系パッケージ

jotai-tanstack-query は、人気のデータフェッチライブラリ TanStack Query(旧 React Query)と Jotai を統合します。

typescriptimport { atomWithQuery } from 'jotai-tanstack-query';

// TanStack Queryの機能を持つatom
const userQueryAtom = atomWithQuery(() => ({
  queryKey: ['user', userId],
  queryFn: async () => {
    const response = await fetch(`/api/users/${userId}`);
    return response.json();
  },
}));

このパッケージを使うことで、TanStack Query の強力なキャッシング機能や自動リフェッチ機能を、Jotai の状態管理と組み合わせて利用できます。

データフェッチのステータス(loading、error、success)も自動的に管理され、UI コンポーネントでの利用がシンプルになります。

状態変更支援パッケージ

イミュータブルな状態更新をサポートする jotai-immer は、複雑なオブジェクトの更新を直感的に記述できるようにします。

typescriptimport { atomWithImmer } from 'jotai-immer';

// Immerを使ったatom
const todosAtom = atomWithImmer([
  { id: 1, text: 'Learn Jotai', completed: false },
  { id: 2, text: 'Build app', completed: false },
]);

Immer を使った状態更新は、ミュータブルな記述で書けながら、内部的にはイミュータブルに処理されます。

typescriptimport { useAtom } from 'jotai'

function TodoList() {
  const [todos, setTodos] = useAtom(todosAtom)

  // ドラフト状態で直接変更できる
  const toggleTodo = (id: number) => {
    setTodos(draft => {
      const todo = draft.find(t => t.id === id)
      if (todo) {
        todo.completed = !todo.completed
      }
    })
  }

  return (/* JSX */)
}

上記のように、配列やオブジェクトの深いネストでも、直感的に状態を更新できるのが Immer の利点です。

開発者体験向上パッケージ

jotai-devtools は、開発中に Jotai の状態を可視化し、デバッグを支援します。

typescriptimport { DevTools } from 'jotai-devtools';
import 'jotai-devtools/styles.css';

function App() {
  return (
    <>
      <DevTools />
      <MainContent />
    </>
  );
}

このツールをアプリケーションに組み込むと、以下のような機能が利用できます。

現在の atom 一覧の表示、各 atom の値のリアルタイム監視、atom 間の依存関係グラフの可視化、状態変更の履歴追跡、タイムトラベルデバッグなどが可能になります。

以下の図は、公式パッケージの分類と主な用途を示しています。

mermaidflowchart LR
  subgraph utility["ユーティリティ系"]
    utils["jotai/utils"]
  end

  subgraph data["データフェッチ系"]
    query["jotai-tanstack-query"]
    urql["jotai-urql"]
  end

  subgraph state["状態変更支援系"]
    immer["jotai-immer"]
    optics["jotai-optics"]
  end

  subgraph integration["統合系"]
    xstate["jotai-xstate"]
    valtio["jotai-valtio"]
    zustand["jotai-zustand"]
  end

  subgraph dev["開発支援系"]
    devtools["jotai-devtools"]
  end

  utility --> use1["基本的な状態操作の拡張"]
  data --> use2["APIやGraphQLとの連携"]
  state --> use3["複雑な状態更新の簡略化"]
  integration --> use4["他ライブラリとの共存"]
  dev --> use5["デバッグと可視化"]

コミュニティ拡張の選び方

コミュニティによって開発された拡張を選ぶ際は、以下の観点で評価することが重要です。

メンテナンス状況の確認

GitHub リポジトリで以下の指標をチェックしましょう。

#確認項目良好な状態の目安
1最終コミット日3 ヶ月以内
2Issue 対応率未解決 Issue が全体の 30%以下
3Pull Request 対応1 ヶ月以内にマージまたはコメント
4リリース頻度半年に 1 回以上
5スター数100 以上(人気度の参考)

最終コミット日が古すぎる場合、そのパッケージは放棄されている可能性があります。一方で、頻繁すぎるコミットは、API が安定していない兆候かもしれません。

依存関係の確認

package.json を確認して、不要な依存関係が含まれていないかチェックします。

json{
  "dependencies": {
    "jotai": "^2.0.0"
  },
  "peerDependencies": {
    "react": ">=18.0.0"
  }
}

Jotai のバージョン指定が適切か、React 等の peerDependencies が明示されているかを確認しましょう。

重すぎる依存関係を持つパッケージは、バンドルサイズの増加やセキュリティリスクの原因になる可能性があります。

ドキュメントとサンプルコードの質

README やドキュメントサイトで、以下の情報が提供されているかを確認します。

基本的な使い方の説明、実用的なサンプルコード、API リファレンス、よくある問題とその解決方法、他パッケージとの互換性情報などが記載されていることが望ましいです。

ドキュメントが充実しているパッケージは、メンテナが真剣に開発に取り組んでいる証拠でもあります。

プロジェクト規模別の推奨構成

プロジェクトの規模や要件に応じて、導入すべき拡張は変わってきます。

小規模プロジェクト(個人開発・プロトタイプ)

最小構成で始めて、必要に応じて拡張します。

typescript// 必要最小限の構成
import { atom, useAtom } from 'jotai';
import { atomWithStorage } from 'jotai/utils';

小規模プロジェクトでは、コアライブラリと jotai​/​utils だけで十分なケースが多いでしょう。

開発中は jotai-devtools を追加すると、状態の把握が容易になります。

中規模プロジェクト(チーム開発・業務アプリ)

データフェッチやフォーム管理など、実践的な機能が必要になります。

typescript// 推奨パッケージ構成
import { atom, useAtom } from 'jotai';
import { atomWithStorage, atomFamily } from 'jotai/utils';
import { atomWithQuery } from 'jotai-tanstack-query';
import { DevTools } from 'jotai-devtools';

中規模プロジェクトでは、API 通信が頻繁に発生するため、jotai-tanstack-query の導入が効果的です。

また、複雑な状態更新が必要な場合は jotai-immer も検討しましょう。

大規模プロジェクト(エンタープライズ)

既存システムとの統合や、高度な状態管理パターンが求められます。

typescript// 包括的な構成例
import { atom, useAtom } from 'jotai';
import {
  atomWithStorage,
  atomFamily,
  selectAtom,
  splitAtom,
} from 'jotai/utils';
import {
  atomWithQuery,
  atomWithMutation,
} from 'jotai-tanstack-query';
import { atomWithImmer } from 'jotai-immer';
import { atomWithMachine } from 'jotai-xstate';
import { DevTools } from 'jotai-devtools';

大規模プロジェクトでは、複雑なビジネスロジックを管理するために jotai-xstate でステートマシンを導入することも有効です。

また、既存の Redux や Zustand との共存が必要な場合は、jotai-reduxjotai-zustand を使って段階的な移行が可能です。

具体例

ケーススタディ 1:E コマースアプリケーション

オンラインショッピングサイトを構築する場合の、Jotai エコシステム活用例を見ていきましょう。

要件分析

E コマースアプリでは、以下のような状態管理が必要です。

商品カタログの表示、ショッピングカートの管理、ユーザー認証情報の保持、注文履歴の取得、お気に入り商品のローカル保存などが挙げられます。

パッケージ選定

上記の要件に対して、以下のパッケージを組み合わせます。

typescript// パッケージインポート
import { atom, useAtom, useSetAtom } from 'jotai';
import { atomWithStorage } from 'jotai/utils';
import { atomWithQuery } from 'jotai-tanstack-query';
import { atomWithImmer } from 'jotai-immer';

各パッケージの役割は以下のとおりです。

jotai​/​utils でお気に入り商品を localStorage に永続化し、jotai-tanstack-query で商品データや注文履歴を API から取得します。また、jotai-immer でカート内の複雑な状態更新を簡潔に記述します。

商品カタログの状態管理

商品一覧は、TanStack Query の機能を活用してキャッシングします。

typescript// 商品カタログのatom定義
const productsQueryAtom = atomWithQuery(() => ({
  queryKey: ['products'],
  queryFn: async () => {
    const response = await fetch('/api/products');
    if (!response.ok)
      throw new Error('Failed to fetch products');
    return response.json();
  },
  staleTime: 5 * 60 * 1000, // 5分間はキャッシュを使用
}));

この atom をコンポーネントで使用すると、ローディング状態やエラーハンドリングが自動的に行われます。

typescript// 商品一覧コンポーネント
import { useAtomValue } from 'jotai';

function ProductList() {
  const { data, isLoading, error } = useAtomValue(
    productsQueryAtom
  );

  if (isLoading) return <div>読み込み中...</div>;
  if (error) return <div>エラーが発生しました</div>;

  return (
    <div>
      {data.products.map((product) => (
        <ProductCard key={product.id} product={product} />
      ))}
    </div>
  );
}

上記のコードでは、データフェッチの状態が自動的に管理され、コンポーネントのコードが非常にシンプルになっています。

ショッピングカートの状態管理

カート内の商品管理には、Immer を使って直感的な更新処理を実現します。

typescript// カート商品の型定義
interface CartItem {
  productId: number;
  name: string;
  price: number;
  quantity: number;
}

// カートのatom定義
const cartAtom = atomWithImmer<CartItem[]>([]);

商品の追加や数量変更の処理は、以下のように記述できます。

typescript// カート操作用のカスタムフック
function useCart() {
  const [cart, setCart] = useAtom(cartAtom);

  const addToCart = (product: Product) => {
    setCart((draft) => {
      const existingItem = draft.find(
        (item) => item.productId === product.id
      );

      if (existingItem) {
        // 既存商品の数量を増やす
        existingItem.quantity += 1;
      } else {
        // 新規商品を追加
        draft.push({
          productId: product.id,
          name: product.name,
          price: product.price,
          quantity: 1,
        });
      }
    });
  };

  const updateQuantity = (
    productId: number,
    quantity: number
  ) => {
    setCart((draft) => {
      const item = draft.find(
        (item) => item.productId === productId
      );
      if (item) {
        item.quantity = quantity;
      }
    });
  };

  const removeFromCart = (productId: number) => {
    setCart((draft) => {
      const index = draft.findIndex(
        (item) => item.productId === productId
      );
      if (index !== -1) {
        draft.splice(index, 1);
      }
    });
  };

  return {
    cart,
    addToCart,
    updateQuantity,
    removeFromCart,
  };
}

Immer のおかげで、配列の検索や更新が直感的に記述でき、コードの可読性が向上しています。

お気に入り商品の永続化

ユーザーのお気に入り商品は、ブラウザを閉じても保持されるように localStorage と同期します。

typescript// お気に入り商品のatom(localStorageと同期)
const favoriteProductIdsAtom = atomWithStorage<number[]>(
  'favoriteProducts',
  []
);

この状態は、自動的に localStorage に保存され、ページをリロードしても復元されます。

typescript// お気に入り操作のフック
function useFavorites() {
  const [favoriteIds, setFavoriteIds] = useAtom(
    favoriteProductIdsAtom
  );

  const toggleFavorite = (productId: number) => {
    setFavoriteIds((prev) => {
      if (prev.includes(productId)) {
        return prev.filter((id) => id !== productId);
      } else {
        return [...prev, productId];
      }
    });
  };

  const isFavorite = (productId: number) => {
    return favoriteIds.includes(productId);
  };

  return { favoriteIds, toggleFavorite, isFavorite };
}

以下の図は、E コマースアプリにおける状態管理の全体像を示しています。

mermaidflowchart TB
  subgraph server["サーバー側"]
    api["REST API"]
    db[("データベース")]
  end

  subgraph client["クライアント側(Jotai)"]
    query["atomWithQuery<br/>(商品・注文データ)"]
    cart["atomWithImmer<br/>(カート状態)"]
    storage["atomWithStorage<br/>(お気に入り)"]
  end

  subgraph ui["UIコンポーネント"]
    products["商品一覧"]
    cartui["カート画面"]
    favorites["お気に入り"]
  end

  db --> api
  api -->|fetch| query
  query --> products

  cart --> cartui
  cartui -->|チェックアウト| api

  storage <-->|同期| local["localStorage"]
  storage --> favorites

このアーキテクチャにより、サーバーデータとクライアント状態が明確に分離され、それぞれに最適な Jotai パッケージが使われています。

ケーススタディ 2:複雑なフォーム管理

多段階ウィザード形式のフォームを実装する場合の例を見ていきましょう。

フォームの要件

会員登録フォームで、以下のような複数ステップがあるとします。

ステップ 1 で基本情報(氏名、メールアドレス)を入力し、ステップ 2 で住所情報を入力します。ステップ 3 で興味のあるカテゴリを選択し、最後のステップ 4 で入力内容を確認して送信します。

各ステップで入力検証を行い、エラーがあれば次に進めないようにします。また、ブラウザをリフレッシュしても入力内容が保持されるようにします。

パッケージ構成

この要件に対して、以下のパッケージを組み合わせます。

typescriptimport { atom } from 'jotai';
import {
  atomWithStorage,
  atomWithDefault,
} from 'jotai/utils';
import { atomWithImmer } from 'jotai-immer';
import { atomWithMachine } from 'jotai-xstate';
import { createMachine } from 'xstate';

フォームの状態遷移管理に XState を使い、入力データの管理に Immer を使用します。さらに、入力途中のデータを localStorage に保存します。

ステートマシンの定義

フォームのステップ遷移は、XState のステートマシンで厳密に管理します。

typescript// フォームのステートマシン定義
const formMachine = createMachine({
  id: 'registrationForm',
  initial: 'basicInfo',
  states: {
    basicInfo: {
      on: {
        NEXT: {
          target: 'address',
          cond: 'isBasicInfoValid',
        },
      },
    },
    address: {
      on: {
        NEXT: {
          target: 'interests',
          cond: 'isAddressValid',
        },
        BACK: 'basicInfo',
      },
    },
    interests: {
      on: {
        NEXT: 'confirmation',
        BACK: 'address',
      },
    },
    confirmation: {
      on: {
        SUBMIT: 'submitting',
        BACK: 'interests',
      },
    },
    submitting: {
      on: {
        SUCCESS: 'success',
        ERROR: 'error',
      },
    },
    success: {
      type: 'final',
    },
    error: {
      on: {
        RETRY: 'confirmation',
      },
    },
  },
});

このステートマシンにより、不正な状態遷移が防止され、フォームの動作が予測可能になります。

typescript// atomとしてステートマシンを使用
const formMachineAtom = atomWithMachine(() => formMachine);

フォームデータの管理

入力データは、Immer と localStorage の組み合わせで管理します。

typescript// フォームデータの型定義
interface RegistrationFormData {
  basicInfo: {
    firstName: string;
    lastName: string;
    email: string;
  };
  address: {
    postalCode: string;
    prefecture: string;
    city: string;
    street: string;
  };
  interests: string[];
}

// フォームデータのatom(localStorage同期)
const formDataAtom = atomWithStorage<RegistrationFormData>(
  'registrationFormData',
  {
    basicInfo: { firstName: '', lastName: '', email: '' },
    address: {
      postalCode: '',
      prefecture: '',
      city: '',
      street: '',
    },
    interests: [],
  }
);

Immer を使ったバージョンで、部分更新を簡単にします。

typescript// Immerでラップしたatom
const formDataWithImmerAtom =
  atomWithImmer<RegistrationFormData>(
    (get) => get(formDataAtom),
    (get, set, update) => {
      set(formDataAtom, update);
    }
  );

これにより、ネストしたオブジェクトの更新が容易になります。

typescript// フォーム入力ハンドラの例
function useFormInput() {
  const [formData, setFormData] = useAtom(
    formDataWithImmerAtom
  );

  const updateBasicInfo = (
    field: string,
    value: string
  ) => {
    setFormData((draft) => {
      draft.basicInfo[field] = value;
    });
  };

  const updateAddress = (field: string, value: string) => {
    setFormData((draft) => {
      draft.address[field] = value;
    });
  };

  const toggleInterest = (interest: string) => {
    setFormData((draft) => {
      const index = draft.interests.indexOf(interest);
      if (index >= 0) {
        draft.interests.splice(index, 1);
      } else {
        draft.interests.push(interest);
      }
    });
  };

  return {
    formData,
    updateBasicInfo,
    updateAddress,
    toggleInterest,
  };
}

バリデーション

各ステップのバリデーションロジックは、派生 atom として定義します。

typescript// 基本情報の検証atom
const isBasicInfoValidAtom = atom((get) => {
  const { basicInfo } = get(formDataAtom);

  if (!basicInfo.firstName.trim()) return false;
  if (!basicInfo.lastName.trim()) return false;
  if (!basicInfo.email.match(/^[^\s@]+@[^\s@]+\.[^\s@]+$/))
    return false;

  return true;
});

// 住所情報の検証atom
const isAddressValidAtom = atom((get) => {
  const { address } = get(formDataAtom);

  if (!address.postalCode.match(/^\d{3}-?\d{4}$/))
    return false;
  if (!address.prefecture) return false;
  if (!address.city.trim()) return false;
  if (!address.street.trim()) return false;

  return true;
});

これらのバリデーション atom は、XState のガード条件として使用できます。

以下の図は、フォームの状態遷移とデータフローを表しています。

mermaidstateDiagram-v2
  [*] --> BasicInfo: フォーム開始

  BasicInfo --> Address: NEXT<br/>(検証OK)
  Address --> BasicInfo: BACK
  Address --> Interests: NEXT<br/>(検証OK)
  Interests --> Address: BACK
  Interests --> Confirmation: NEXT
  Confirmation --> Interests: BACK
  Confirmation --> Submitting: SUBMIT
  Submitting --> Success: API成功
  Submitting --> Error: API失敗
  Error --> Confirmation: RETRY
  Success --> [*]: 完了

  note right of BasicInfo
    localStorage自動保存
  end note

  note right of Submitting
    APIへPOST
  end note

このように、Jotai エコシステムの複数のパッケージを組み合わせることで、複雑なフォーム管理も整然と実装できます。

ケーススタディ 3:リアルタイムダッシュボード

WebSocket を使ったリアルタイムデータ表示の例を見ていきましょう。

要件

管理画面で、以下のようなリアルタイム情報を表示します。

現在のアクティブユーザー数、直近 1 時間のトランザクション数、システムリソース使用率(CPU、メモリ)、エラーログのストリームなどです。

データは WebSocket で受信し、過去のデータは REST API で取得します。また、グラフ表示のためのデータ整形が必要です。

パッケージ選定

この要件には、以下のパッケージを使用します。

typescriptimport { atom, useAtom, useSetAtom } from 'jotai';
import { atomWithQuery } from 'jotai-tanstack-query';
import { atomFamily } from 'jotai/utils';

リアルタイムデータはカスタム atom で管理し、履歴データは TanStack Query でキャッシングします。

WebSocket データの管理

WebSocket から受信したデータを保持する atom を作成します。

typescript// リアルタイムメトリクスの型定義
interface Metrics {
  activeUsers: number;
  transactions: number;
  cpuUsage: number;
  memoryUsage: number;
  timestamp: number;
}

// リアルタイムデータのatom
const realtimeMetricsAtom = atom<Metrics | null>(null);

// WebSocket接続のatom
const wsConnectionAtom = atom(
  (get) => get(realtimeMetricsAtom),
  (get, set) => {
    const ws = new WebSocket(
      'wss://api.example.com/metrics'
    );

    ws.onmessage = (event) => {
      const data = JSON.parse(event.data);
      set(realtimeMetricsAtom, data);
    };

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

    return () => ws.close();
  }
);

WebSocket 接続は、コンポーネントのマウント時に確立されます。

typescript// ダッシュボードコンポーネント
function Dashboard() {
  const [metrics] = useAtom(realtimeMetricsAtom);
  const initWebSocket = useSetAtom(wsConnectionAtom);

  useEffect(() => {
    const cleanup = initWebSocket();
    return cleanup;
  }, [initWebSocket]);

  if (!metrics) return <div>接続中...</div>;

  return (
    <div>
      <MetricCard
        label='アクティブユーザー'
        value={metrics.activeUsers}
      />
      <MetricCard
        label='トランザクション数'
        value={metrics.transactions}
      />
      <MetricCard
        label='CPU使用率'
        value={`${metrics.cpuUsage}%`}
      />
      <MetricCard
        label='メモリ使用率'
        value={`${metrics.memoryUsage}%`}
      />
    </div>
  );
}

履歴データの取得

過去のメトリクスデータは、TanStack Query で取得します。

typescript// 履歴データ取得用のatom family
const historicalMetricsAtom = atomFamily(
  (timeRange: string) =>
    atomWithQuery(() => ({
      queryKey: ['metrics', 'history', timeRange],
      queryFn: async () => {
        const response = await fetch(
          `/api/metrics/history?range=${timeRange}`
        );
        return response.json();
      },
      refetchInterval: 60000, // 1分ごとに更新
    }))
);

atom family を使うことで、時間範囲ごとに個別のキャッシュを持つことができます。

typescript// グラフ表示コンポーネント
function MetricsGraph({
  timeRange,
}: {
  timeRange: string;
}) {
  const { data, isLoading } = useAtomValue(
    historicalMetricsAtom(timeRange)
  );

  if (isLoading) return <div>データ読み込み中...</div>;

  return <LineChart data={data.metrics} />;
}

timeRange が変わると、自動的に対応する atom が選択され、データが取得されます。

このように、Jotai エコシステムを活用することで、リアルタイムデータとキャッシュされた履歴データを効率的に管理できます。

まとめ

Jotai のエコシステムは、コアライブラリのシンプルさを保ちながら、実践的な開発ニーズに応える豊富な拡張パッケージを提供しています。

公式パッケージは、データフェッチ、状態変更支援、他ライブラリとの統合、開発者ツールなど、幅広い領域をカバーしており、安定性とメンテナンスの継続性が保証されています。一方、コミュニティ製の拡張は、ニッチな用途や実験的な機能を提供し、エコシステムに多様性をもたらしています。

拡張を選択する際は、公式パッケージの有無、メンテナンス状況、依存関係、ドキュメントの質を総合的に評価することが重要です。プロジェクトの規模や要件に応じて、必要最小限の構成から始め、段階的に拡張していくアプローチが推奨されます。

本記事で紹介したケーススタディのように、複数のパッケージを組み合わせることで、E コマース、複雑なフォーム、リアルタイムダッシュボードなど、様々なユースケースに対応できます。それぞれのパッケージが特定の役割に特化しているため、責務が明確に分離され、保守性の高いコードを実現できるでしょう。

Jotai エコシステムは今後も進化を続けます。公式チームによる新しいパッケージのリリースや、コミュニティによる革新的なツールの登場に注目しながら、最新の情報をキャッチアップし続けることが大切です。

あなたのプロジェクトに最適な Jotai 拡張を見つけ、快適な開発体験を手に入れてください。

関連リンク