T-CREATOR

Jotai 運用ガイド:命名規約・debugLabel・依存グラフ可視化の標準化

Jotai 運用ガイド:命名規約・debugLabel・依存グラフ可視化の標準化

Jotai を活用した状態管理は、そのシンプルさと柔軟性が魅力です。しかし、プロジェクトが大きくなるにつれて、atom の数が増え、依存関係が複雑化します。その結果、デバッグが困難になり、保守性が低下してしまうケースがあるでしょう。

本記事では、Jotai を実際のプロジェクトで運用する際に重要となる「命名規約」「debugLabel の活用」「依存グラフの可視化」という 3 つの標準化手法を詳しく解説します。これらのベストプラクティスを導入することで、チーム開発における保守性と開発効率を大幅に向上させることができますよ。

背景

Jotai における状態管理の特徴

Jotai は atom という単位で状態を管理する軽量なライブラリです。React の状態管理において、Redux や Recoil と比較して学習コストが低く、ボイラープレートも少ないという特徴があります。

atom は独立した状態の最小単位として機能し、他の atom に依存することで複雑な状態を構築できるんですね。この composable な設計により、状態のロジックを細かく分割し、再利用性の高い実装が可能になります。

以下の図は、Jotai の基本的な atom の構造と依存関係を示しています。

mermaidflowchart TD
  user["コンポーネント"] -->|useAtom で読み書き| primitiveAtom["Primitive Atom<br/>(基本状態)"]
  user -->|useAtomValue で読み取り| derivedAtom["Derived Atom<br/>(派生状態)"]
  derivedAtom -->|依存| primitiveAtom
  derivedAtom -->|依存| anotherAtom["別の Atom"]

図で理解できる要点

  • Primitive Atom は独立した状態を保持します
  • Derived Atom は他の atom に依存した計算値です
  • コンポーネントは hooks を通じて atom を利用します

プロジェクト規模拡大による課題

小規模なプロジェクトでは atom の数が少なく、依存関係もシンプルです。しかし、機能追加に伴い atom が増えると、以下のような状況が生まれます。

まず、atom 間の依存関係が複雑に絡み合い、どの atom がどの atom に依存しているのか把握しづらくなります。次に、デバッグ時に DevTools で表示される atom の識別が困難になり、問題の原因特定に時間がかかるでしょう。さらに、命名規則が統一されていないと、同じような役割の atom でも命名がバラバラになり、コードの可読性が低下してしまいます。

これらの課題は、チーム開発において特に顕著です。複数の開発者が独自の命名規則で atom を作成すると、コードレビューや引き継ぎの際に混乱を招く原因となるんですね。

課題

命名の一貫性欠如

Jotai では atom の命名に厳格なルールがありません。そのため、開発者ごとに異なる命名スタイルが混在しやすいという問題があります。

例えば、ユーザー情報を保持する atom を作成する際、以下のように様々な命名パターンが考えられます。

typescript// パターン1: プレフィックスなし
const user = atom(null);
const currentUser = atom(null);

// パターン2: Atom サフィックス
const userAtom = atom(null);
const currentUserAtom = atom(null);

// パターン3: 役割による命名
const userState = atom(null);
const userData = atom(null);

このような命名の不統一は、コードベース全体の可読性を下げます。新しいメンバーがプロジェクトに参加した際、どの命名規則を採用すべきか判断できず、さらに混乱が広がる可能性があるでしょう。

#問題点影響
1命名規則の不統一コード検索が困難、レビューの負担増加
2atom の役割が不明確用途の理解に時間がかかる
3新規参加者の混乱オンボーディングコストの増加

デバッグ時の識別困難性

Jotai の DevTools や React DevTools で状態をデバッグする際、atom に debugLabel が設定されていないと、atom(...) のような汎用的な表示になります。これでは多数の atom の中から目的のものを特定することが非常に困難です。

特に derived atom が複数ネストしている場合、どの計算結果がどの atom に対応するのか判別できず、デバッグに多大な時間を費やすことになるでしょう。

typescript// debugLabel なし
const countAtom = atom(0);
const doubledAtom = atom((get) => get(countAtom) * 2);

// DevTools での表示: atom(...), atom(...) - 区別不可能

このような状況では、console.log を多用したり、ソースコードを何度も往復したりする必要があり、開発効率が大幅に低下してしまいます。

依存関係の把握困難性

atom が増えると、どの atom がどの atom に依存しているかを把握するのが難しくなります。特に以下のようなケースでは、依存グラフを理解することが重要です。

まず、パフォーマンス最適化を行う際、どの atom の更新が連鎖的に他の atom を再計算させるのか知る必要があります。次に、atom を削除または変更する際、影響範囲を正確に把握しなければなりません。さらに、新しい機能を追加する際、既存の atom を再利用できるか判断するためには、依存関係の全体像を理解しておく必要があるんですね。

しかし、コードを読むだけでは複雑な依存関係を視覚的に理解することは困難です。特に循環依存や深いネストがある場合、バグの温床となる可能性があるでしょう。

以下の図は、複雑化した atom の依存関係の例を示しています。

mermaidflowchart TD
  compA["Component A"] -->|useAtom| atomA["userAtom"]
  compB["Component B"] -->|useAtomValue| atomB["userNameAtom"]
  compC["Component C"] -->|useAtomValue| atomC["userAgeAtom"]

  atomB -->|依存| atomA
  atomC -->|依存| atomA

  compD["Component D"] -->|useAtomValue| atomD["userProfileAtom"]
  atomD -->|依存| atomB
  atomD -->|依存| atomC
  atomD -->|依存| atomA

図で理解できる要点

  • 複数のコンポーネントが異なる atom を参照します
  • 派生 atom は複数の atom に依存する場合があります
  • 依存関係が深くなると、変更の影響範囲が拡大します

解決策

命名規約の標準化

一貫した命名規約を定めることで、チーム全体でのコード可読性が向上します。以下の規約を推奨いたします。

基本方針

すべての atom 名には Atom サフィックスを付けることで、通常の変数や関数と明確に区別できます。これにより、コードを読む際に atom であることが一目で分かるんですね。

typescript// 推奨: Atom サフィックス
const userAtom = atom<User | null>(null);
const countAtom = atom(0);
const isLoadingAtom = atom(false);
typescript// 非推奨: サフィックスなし
const user = atom<User | null>(null);
const count = atom(0);
const isLoading = atom(false);

Primitive Atom の命名

Primitive atom は基本的な状態を保持するため、その内容を明確に表す名詞を使用します。

typescript// ユーザー関連
const currentUserAtom = atom<User | null>(null);
const userListAtom = atom<User[]>([]);

// UI 状態
const sidebarOpenAtom = atom(false);
const themeAtom = atom<'light' | 'dark'>('light');

// フォーム状態
const emailInputAtom = atom('');
const passwordInputAtom = atom('');

boolean 型の atom には is, has, should などのプレフィックスを使用すると、真偽値であることが明確になります。

typescriptconst isAuthenticatedAtom = atom(false);
const hasNotificationAtom = atom(false);
const shouldAutoSaveAtom = atom(true);

Derived Atom の命名

Derived atom は他の atom から計算された値を返すため、計算内容や用途を示す名前にします。

typescriptimport { atom } from 'jotai';

// 基本 atom
const firstNameAtom = atom('Taro');
const lastNameAtom = atom('Yamada');
typescript// 派生 atom: 名前を結合
const fullNameAtom = atom((get) => {
  const firstName = get(firstNameAtom);
  const lastName = get(lastNameAtom);
  return `${lastName} ${firstName}`;
});
typescript// 派生 atom: 計算結果
const userCountAtom = atom((get) => {
  const users = get(userListAtom);
  return users.length;
});

複雑な計算を行う場合、その内容を命名に反映させることで、用途が明確になるでしょう。

typescriptconst filteredActiveUsersAtom = atom((get) => {
  const users = get(userListAtom);
  return users.filter((user) => user.isActive);
});

const sortedUsersByNameAtom = atom((get) => {
  const users = get(filteredActiveUsersAtom);
  return [...users].sort((a, b) =>
    a.name.localeCompare(b.name)
  );
});
#Atom の種類命名規則
1Primitive (値)名詞 + AtomuserAtom, countAtom
2Primitive (真偽値)is/has/should + 形容詞 + AtomisLoadingAtom
3Derived (計算)計算内容 + AtomfullNameAtom, userCountAtom
4Derived (フィルタ)filtered + 条件 + AtomfilteredActiveUsersAtom

debugLabel の活用

debugLabel を設定することで、DevTools でのデバッグが飛躍的に効率化されます。すべての atom に適切な debugLabel を付与することを強く推奨いたします。

基本的な設定方法

atom の第二引数として debugLabel を指定します。

typescriptimport { atom } from 'jotai';

const userAtom = atom<User | null>(null);
userAtom.debugLabel = 'userAtom';
typescriptconst isLoadingAtom = atom(false);
isLoadingAtom.debugLabel = 'isLoadingAtom';

この設定により、DevTools で atom を確認した際に atom(...) ではなく userAtom のように識別可能な名前で表示されます。

一括設定パターン

すべての atom に個別に debugLabel を設定するのは手間がかかります。そこで、atom 作成と同時に debugLabel を設定するヘルパー関数を作成すると効率的です。

typescript// ヘルパー関数の定義
import { atom, WritableAtom, Atom } from 'jotai';

export function atomWithDebug<T>(
  initialValue: T,
  debugLabel: string
): WritableAtom<T, [T], void> {
  const a = atom(initialValue);
  a.debugLabel = debugLabel;
  return a;
}
typescript// 派生 atom 用のヘルパー
export function derivedAtomWithDebug<T>(
  read: (get: Getter) => T,
  debugLabel: string
): Atom<T> {
  const a = atom(read);
  a.debugLabel = debugLabel;
  return a;
}
typescript// 使用例
const userAtom = atomWithDebug<User | null>(
  null,
  'userAtom'
);
const countAtom = atomWithDebug(0, 'countAtom');

const doubledCountAtom = derivedAtomWithDebug(
  (get) => get(countAtom) * 2,
  'doubledCountAtom'
);

このアプローチにより、atom の作成と debugLabel の設定を一箇所で管理でき、設定漏れを防げます。

debugLabel のベストプラクティス

debugLabel の命名は、変数名と一致させることで、コードと DevTools の表示を対応付けやすくなります。

typescript// 推奨: 変数名と debugLabel を一致
const userProfileAtom = atomWithDebug<Profile | null>(
  null,
  'userProfileAtom'
);

// 非推奨: 異なる名前
const userProfileAtom = atomWithDebug<Profile | null>(
  null,
  'profile'
);

複雑な派生 atom の場合、計算内容を debugLabel に含めると、デバッグ時に理解しやすくなります。

typescriptconst filteredActiveUsersAtom = derivedAtomWithDebug(
  (get) => {
    const users = get(userListAtom);
    return users.filter((u) => u.isActive);
  },
  'filteredActiveUsersAtom (active users only)'
);

依存グラフの可視化

atom の依存関係を視覚化することで、コードの構造を直感的に理解できます。ここでは、手動での図作成と自動生成の両方のアプローチを紹介いたします。

手動での依存グラフ作成

プロジェクトのドキュメントに、主要な atom の依存関係を Mermaid 図として記載しておくと、新規メンバーの理解が早まります。

以下は、ユーザー認証機能の atom 依存グラフの例です。

mermaidflowchart TD
  authTokenAtom["authTokenAtom<br/>(認証トークン)"]
  userIdAtom["userIdAtom<br/>(ユーザーID)"]

  userAtom["userAtom<br/>(ユーザー情報)"] -->|依存| authTokenAtom
  userAtom -->|依存| userIdAtom

  isAuthenticatedAtom["isAuthenticatedAtom<br/>(認証状態)"] -->|依存| authTokenAtom

  userNameAtom["userNameAtom<br/>(ユーザー名)"] -->|依存| userAtom
  userEmailAtom["userEmailAtom<br/>(メールアドレス)"] -->|依存| userAtom

図で理解できる要点

  • authTokenAtomuserIdAtom は基本的な状態です
  • userAtom は複数の基本 atom に依存します
  • 派生 atom はユーザー情報から特定の値を抽出します

このような図をプロジェクトの README や設計ドキュメントに含めることで、チーム全体での理解が深まるでしょう。

自動生成ツールの活用

手動での図作成は初期構築には有効ですが、atom が増えると更新が追いつかなくなります。そこで、コードから自動的に依存グラフを生成するツールを導入することが推奨されます。

Jotai には公式の DevTools があり、atom の依存関係をリアルタイムで可視化できます。

typescript// jotai-devtools のインストール
// yarn add jotai-devtools
typescript// アプリケーションのルートに DevTools を追加
import { DevTools } from 'jotai-devtools';
import 'jotai-devtools/styles.css';

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

DevTools を組み込むと、ブラウザ上で以下の情報を確認できます。

まず、すべての atom の現在値がリアルタイムで表示されます。次に、atom 間の依存関係がグラフィカルに表示され、どの atom がどの atom を参照しているか一目で分かります。さらに、atom の値の変更履歴を追跡でき、いつどのように状態が変わったのか確認できるんですね。

カスタムスクリプトでの依存関係抽出

より詳細な分析や、ドキュメント生成を自動化したい場合、TypeScript の AST を解析して atom の依存関係を抽出するスクリプトを作成することも可能です。

typescript// scripts/analyze-atoms.ts
import * as ts from 'typescript';
import * as fs from 'fs';

interface AtomInfo {
  name: string;
  dependencies: string[];
  filePath: string;
}
typescript// AST から atom の依存関係を抽出する関数
function extractAtomDependencies(
  sourceFile: ts.SourceFile
): AtomInfo[] {
  const atoms: AtomInfo[] = [];

  function visit(node: ts.Node) {
    // atom 呼び出しを検出
    if (ts.isCallExpression(node)) {
      const expression = node.expression.getText();
      if (expression === 'atom') {
        // atom 名と依存関係を抽出
        // (実装の詳細は省略)
      }
    }
    ts.forEachChild(node, visit);
  }

  visit(sourceFile);
  return atoms;
}
typescript// 実行スクリプト
const program = ts.createProgram(['src/**/*.ts'], {});
const sourceFiles = program.getSourceFiles();

const allAtoms: AtomInfo[] = [];
sourceFiles.forEach((file) => {
  const atoms = extractAtomDependencies(file);
  allAtoms.push(...atoms);
});

// 依存グラフを Mermaid 形式で出力
console.log('flowchart TD');
allAtoms.forEach((atom) => {
  atom.dependencies.forEach((dep) => {
    console.log(`  ${atom.name} -->|依存| ${dep}`);
  });
});

このようなスクリプトを CI/CD パイプラインに組み込むことで、常に最新の依存グラフをドキュメントとして生成できます。

#可視化手法利点使用シーン
1手動 Mermaid 図シンプル、ドキュメント化が容易小規模プロジェクト、初期設計
2jotai-devtoolsリアルタイム、インタラクティブ開発・デバッグ時
3カスタムスクリプト自動化、CI/CD 統合可能大規模プロジェクト、継続的な監視

具体例

実践例:ショッピングカート機能

ここでは、EC サイトのショッピングカート機能を例に、命名規約・debugLabel・依存グラフ可視化を実践した実装を示します。

Atom の設計と命名

まず、基本的な状態を管理する primitive atom を定義します。

typescript// src/atoms/cart.ts
import { atom } from 'jotai';

// 型定義
export interface CartItem {
  productId: string;
  name: string;
  price: number;
  quantity: number;
}
typescript// Primitive atom: カート内商品リスト
export const cartItemsAtom = atom<CartItem[]>([]);
cartItemsAtom.debugLabel = 'cartItemsAtom';
typescript// Primitive atom: 送料無料の基準額
export const freeShippingThresholdAtom = atom(5000);
freeShippingThresholdAtom.debugLabel =
  'freeShippingThresholdAtom';
typescript// Primitive atom: 税率
export const taxRateAtom = atom(0.1);
taxRateAtom.debugLabel = 'taxRateAtom';

次に、これらの primitive atom から派生する derived atom を作成します。

typescript// Derived atom: カート内商品の合計金額
export const cartSubtotalAtom = atom((get) => {
  const items = get(cartItemsAtom);
  return items.reduce(
    (sum, item) => sum + item.price * item.quantity,
    0
  );
});
cartSubtotalAtom.debugLabel = 'cartSubtotalAtom';
typescript// Derived atom: 税額
export const cartTaxAtom = atom((get) => {
  const subtotal = get(cartSubtotalAtom);
  const taxRate = get(taxRateAtom);
  return Math.floor(subtotal * taxRate);
});
cartTaxAtom.debugLabel = 'cartTaxAtom';
typescript// Derived atom: 送料
export const shippingFeeAtom = atom((get) => {
  const subtotal = get(cartSubtotalAtom);
  const threshold = get(freeShippingThresholdAtom);
  return subtotal >= threshold ? 0 : 500;
});
shippingFeeAtom.debugLabel = 'shippingFeeAtom';
typescript// Derived atom: 最終合計金額
export const cartTotalAtom = atom((get) => {
  const subtotal = get(cartSubtotalAtom);
  const tax = get(cartTaxAtom);
  const shipping = get(shippingFeeAtom);
  return subtotal + tax + shipping;
});
cartTotalAtom.debugLabel = 'cartTotalAtom';
typescript// Derived atom: 送料無料まであといくらか
export const remainingForFreeShippingAtom = atom((get) => {
  const subtotal = get(cartSubtotalAtom);
  const threshold = get(freeShippingThresholdAtom);
  const remaining = threshold - subtotal;
  return remaining > 0 ? remaining : 0;
});
remainingForFreeShippingAtom.debugLabel =
  'remainingForFreeShippingAtom';

依存グラフの可視化

上記の実装における atom の依存関係を図示すると、以下のようになります。

mermaidflowchart TD
  cartItemsAtom["cartItemsAtom<br/>(カート商品リスト)"]
  freeShippingThresholdAtom["freeShippingThresholdAtom<br/>(送料無料基準額)"]
  taxRateAtom["taxRateAtom<br/>(税率)"]

  cartSubtotalAtom["cartSubtotalAtom<br/>(小計)"] -->|依存| cartItemsAtom

  cartTaxAtom["cartTaxAtom<br/>(税額)"] -->|依存| cartSubtotalAtom
  cartTaxAtom -->|依存| taxRateAtom

  shippingFeeAtom["shippingFeeAtom<br/>(送料)"] -->|依存| cartSubtotalAtom
  shippingFeeAtom -->|依存| freeShippingThresholdAtom

  remainingForFreeShippingAtom["remainingForFreeShippingAtom<br/>(送料無料まで)"] -->|依存| cartSubtotalAtom
  remainingForFreeShippingAtom -->|依存| freeShippingThresholdAtom

  cartTotalAtom["cartTotalAtom<br/>(合計金額)"] -->|依存| cartSubtotalAtom
  cartTotalAtom -->|依存| cartTaxAtom
  cartTotalAtom -->|依存| shippingFeeAtom

図で理解できる要点

  • 3 つの primitive atom が基礎となります
  • cartSubtotalAtom が中心的な役割を果たし、多くの派生 atom がこれに依存します
  • cartTotalAtom は複数の派生 atom を組み合わせた最終計算値です

この依存グラフから、cartItemsAtom が更新されると cartSubtotalAtom が再計算され、それに依存するすべての atom が連鎖的に更新されることが分かります。

コンポーネントでの利用

実装した atom をコンポーネントで使用する例です。

typescript// src/components/Cart.tsx
import { useAtom, useAtomValue } from 'jotai';
import {
  cartItemsAtom,
  cartSubtotalAtom,
  cartTaxAtom,
  shippingFeeAtom,
  cartTotalAtom,
  remainingForFreeShippingAtom,
} from '@/atoms/cart';
typescriptexport function Cart() {
  const [items, setItems] = useAtom(cartItemsAtom);
  const subtotal = useAtomValue(cartSubtotalAtom);
  const tax = useAtomValue(cartTaxAtom);
  const shipping = useAtomValue(shippingFeeAtom);
  const total = useAtomValue(cartTotalAtom);
  const remaining = useAtomValue(remainingForFreeShippingAtom);

  // 商品削除ハンドラー
  const removeItem = (productId: string) => {
    setItems(items.filter(item => item.productId !== productId));
  };
typescript  return (
    <div>
      <h2>ショッピングカート</h2>

      {/* 送料無料まであと... */}
      {remaining > 0 && (
        <div className="alert">
          あと {remaining.toLocaleString()}円で送料無料!
        </div>
      )}

      {/* 商品リスト */}
      <ul>
        {items.map(item => (
          <li key={item.productId}>
            {item.name} - {item.price}円 × {item.quantity}
            <button onClick={() => removeItem(item.productId)}>削除</button>
          </li>
        ))}
      </ul>
typescript      {/* 金額詳細 */}
      <div className="summary">
        <div>小計: {subtotal.toLocaleString()}円</div>
        <div>消費税: {tax.toLocaleString()}円</div>
        <div>送料: {shipping.toLocaleString()}円</div>
        <div className="total">合計: {total.toLocaleString()}円</div>
      </div>
    </div>
  );
}

このように、各 atom が明確な命名と debugLabel を持つことで、コンポーネント内でも用途が一目で理解できます。また、DevTools で各 atom の値をリアルタイムで確認できるため、デバッグが容易になるんですね。

テストでの活用

命名規約と debugLabel が整っていると、テストコードも書きやすくなります。

typescript// src/atoms/cart.test.ts
import { describe, it, expect } from 'vitest';
import { createStore } from 'jotai';
import {
  cartItemsAtom,
  cartSubtotalAtom,
  cartTaxAtom,
  shippingFeeAtom,
  cartTotalAtom,
  remainingForFreeShippingAtom,
} from './cart';
typescriptdescribe('Cart Atoms', () => {
  it('小計が正しく計算される', () => {
    const store = createStore();

    // カートに商品を追加
    store.set(cartItemsAtom, [
      { productId: '1', name: '商品A', price: 1000, quantity: 2 },
      { productId: '2', name: '商品B', price: 1500, quantity: 1 }
    ]);

    // 小計を確認
    const subtotal = store.get(cartSubtotalAtom);
    expect(subtotal).toBe(3500); // 1000*2 + 1500*1
  });
typescriptit('送料無料の基準を超えると送料が0円になる', () => {
  const store = createStore();

  // 送料無料基準額以上の商品を追加
  store.set(cartItemsAtom, [
    {
      productId: '1',
      name: '高額商品',
      price: 6000,
      quantity: 1,
    },
  ]);

  const shipping = store.get(shippingFeeAtom);
  expect(shipping).toBe(0);
});
typescript  it('送料無料まであといくらか正しく計算される', () => {
    const store = createStore();

    // 3000円分の商品を追加
    store.set(cartItemsAtom, [
      { productId: '1', name: '商品', price: 3000, quantity: 1 }
    ]);

    const remaining = store.get(remainingForFreeShippingAtom);
    expect(remaining).toBe(2000); // 5000 - 3000
  });
});

テストにおいても、atom の名前が明確であるため、何をテストしているのかが分かりやすくなります。debugLabel があることで、テスト失敗時の原因特定も迅速に行えるでしょう。

チーム運用でのルール化

これらの実践をチーム全体で標準化するために、以下のようなルールをプロジェクトのドキュメントに明記することを推奨します。

#ルール詳細
1命名規約の統一すべての atom に Atom サフィックスを付ける
2debugLabel の必須化すべての atom に debugLabel を設定する
3ヘルパー関数の使用atomWithDebug 関数を使って atom を作成する
4依存グラフの更新主要機能の atom 追加時は依存グラフを更新する
5コードレビューatom 作成時は命名と debugLabel を必ずレビューする

これらのルールを ESLint のカスタムルールや、プルリクエストテンプレートに含めることで、運用の徹底が図れます。

まとめ

Jotai を活用したプロジェクトでは、命名規約・debugLabel・依存グラフ可視化の 3 つを標準化することで、保守性と開発効率が大幅に向上します。

命名規約を統一することで、コードベース全体の一貫性が保たれ、新規メンバーのオンボーディングがスムーズになります。debugLabel を設定することで、デバッグ時の atom 識別が容易になり、問題解決のスピードが上がるでしょう。さらに、依存グラフを可視化することで、atom 間の関係性を直感的に理解でき、パフォーマンス最適化や影響範囲の把握が正確に行えます。

これらのベストプラクティスは、小規模プロジェクトでは過剰に感じるかもしれません。しかし、プロジェクトが成長するにつれて、その価値は確実に実感できるはずです。ぜひ、プロジェクトの初期段階からこれらの標準化を導入し、長期的な保守性を確保していきましょう。

Jotai のシンプルさを活かしつつ、チーム開発における規律を保つことで、スケーラブルで持続可能な状態管理が実現できますよ。

関連リンク