T-CREATOR

SolidJS のエラー処理とデバッグのコツ

SolidJS のエラー処理とデバッグのコツ

SolidJS開発において、エラー処理とデバッグは避けて通れない重要なスキルです。この記事では、SolidJS特有のエラーパターンから効果的なデバッグ手法まで、実用的なテクニックを詳しく解説いたします。

初心者の方でも理解しやすいよう、具体的なコード例とともに段階的にお話しを進めていきますね。

背景

SolidJSのリアクティブシステムとエラーの関係

SolidJSのリアクティブシステムは、シグナルベースの仕組みを採用しています。これは従来のVirtual DOMアプローチとは根本的に異なるため、エラーが発生する箇所や伝播の仕方も独特です。

シグナルの更新チェーンでエラーが発生した場合、その影響範囲を特定するのが困難になることがあります。特に、複数のシグナルが相互依存している場合は注意が必要でしょう。

typescriptimport { createSignal, createEffect } from 'solid-js';

const [count, setCount] = createSignal(0);
const [doubled, setDoubled] = createSignal(0);
typescript// エラーが発生しやすいパターン
createEffect(() => {
  // countが負数の場合にエラーが発生
  if (count() < 0) {
    throw new Error('Count cannot be negative');
  }
  setDoubled(count() * 2);
});

Reactとの違いから生まれるデバッグの難しさ

Reactに慣れた開発者がSolidJSに移行する際、最も戸惑うのがデバッグ方法の違いです。

Reactでは、コンポーネントツリーを辿ってエラーの発生源を特定できましたが、SolidJSではシグナルグラフを理解する必要があります。これは、データフローが直接的で効率的である一方、デバッグ時には追跡が複雑になることを意味しているのです。

項目ReactSolidJS
エラー発生場所コンポーネントツリー内シグナルグラフ内
デバッグ対象レンダリングサイクルリアクティブ更新チェーン
情報の可視化React DevToolsSolidJS DevTools(限定的)

課題

一般的なSolidJSエラーパターン

SolidJS開発でよく遭遇するエラーパターンをご紹介します。これらを理解することで、エラー発生時の対応速度が格段に向上するでしょう。

シグナル更新エラー

typescriptimport { createSignal } from 'solid-js';

const [data, setData] = createSignal(null);

// エラーパターン1: null参照エラー
const processData = () => {
  // data()がnullの場合にエラー発生
  return data().map(item => item.name); // TypeError: Cannot read property 'map' of null
};

このエラーは、シグナルの初期値設定や条件分岐の不備が原因で発生します。

無限ループエラー

typescriptimport { createSignal, createEffect } from 'solid-js';

const [valueA, setValueA] = createSignal(0);
const [valueB, setValueB] = createSignal(0);

// エラーパターン2: 相互依存による無限ループ
createEffect(() => {
  setValueB(valueA() + 1);
});

createEffect(() => {
  setValueA(valueB() + 1); // 無限ループが発生
});

コンポーネントライフサイクルエラー

typescriptimport { onMount, onCleanup } from 'solid-js';

const MyComponent = () => {
  let timer;

  onMount(() => {
    timer = setInterval(() => {
      console.log('Timer running');
    }, 1000);
  });

  // エラーパターン3: クリーンアップ不備
  // onCleanupを忘れるとメモリリークが発生
  onCleanup(() => {
    if (timer) {
      clearInterval(timer);
    }
  });

  return <div>Timer Component</div>;
};

デバッグツールの不足

SolidJSのデバッグツールは、ReactやVueと比較すると発展途上の状態です。特に以下の課題があります:

  • ブラウザ拡張機能の機能制限: SolidJS DevToolsは存在しますが、React DevToolsほど充実していません
  • IDE統合の不足: VSCodeなどでの統合デバッグ機能が限定的です
  • ログ出力の複雑さ: シグナルの変更を追跡するための標準的な方法が確立されていません

エラー情報の読み取りにくさ

SolidJSのエラーメッセージは、時として原因の特定が困難な場合があります。

typescript// よく見るエラーメッセージの例
Error: Computation created outside a `createRoot` or `render` call. 
Wrap the relevant code in a `createRoot` or `render` call.

Error: Setting a signal in a computation is not allowed

これらのエラーメッセージは、SolidJSの内部仕組みを理解していないと解釈が困難です。初心者の方には特に理解しにくい内容となっています。

解決策

エラーバウンダリの実装

SolidJSでは、エラーバウンダリを使用してコンポーネントレベルでのエラーハンドリングが可能です。

typescriptimport { ErrorBoundary } from 'solid-js';

const App = () => {
  return (
    <ErrorBoundary fallback={ErrorFallback}>
      <MainContent />
    </ErrorBoundary>
  );
};
typescript// エラー時に表示するフォールバックコンポーネント
const ErrorFallback = (err, reset) => {
  console.error('Application error:', err);
  
  return (
    <div class="error-container">
      <h2>エラーが発生しました</h2>
      <p>Error: {err.message}</p>
      <button onClick={reset}>アプリケーションをリセット</button>
    </div>
  );
};

カスタムエラーバウンダリの実装

より詳細なエラー処理が必要な場合は、カスタムエラーバウンダリを作成します。

typescriptimport { createSignal, ErrorBoundary } from 'solid-js';

const CustomErrorBoundary = (props) => {
  const [errorLog, setErrorLog] = createSignal([]);

  const handleError = (error, reset) => {
    // エラーログを記録
    setErrorLog(prev => [...prev, {
      timestamp: new Date(),
      error: error.message,
      stack: error.stack
    }]);

    // 外部サービスにエラーレポートを送信(オプション)
    reportError(error);

    return props.fallback(error, reset);
  };

  return (
    <ErrorBoundary fallback={handleError}>
      {props.children}
    </ErrorBoundary>
  );
};

開発者ツールの活用方法

SolidJS DevToolsの導入

bashyarn add -D @solid-devtools/extension
typescript// main.tsxでDevToolsを初期化
import { attachDevtoolsOverlay } from '@solid-devtools/overlay';

if (import.meta.env.DEV) {
  attachDevtoolsOverlay();
}

ブラウザ開発者ツールの効果的な使用

ブラウザの開発者ツールを最大限活用するための設定です。

typescript// デバッグ用のヘルパー関数を作成
const debugSignal = (name, signal) => {
  if (import.meta.env.DEV) {
    createEffect(() => {
      console.log(`[DEBUG] ${name}:`, signal());
    });
  }
  return signal;
};

// 使用例
const [count, setCount] = debugSignal('count', createSignal(0));

ログ出力の戦略

効果的なログ出力により、問題の特定を素早く行えます。

typescript// ログレベルを定義
enum LogLevel {
  DEBUG = 0,
  INFO = 1,
  WARN = 2,
  ERROR = 3
}

class Logger {
  private level: LogLevel;

  constructor(level: LogLevel = LogLevel.INFO) {
    this.level = level;
  }

  debug(message: string, data?: any) {
    if (this.level <= LogLevel.DEBUG) {
      console.log(`[DEBUG] ${message}`, data);
    }
  }

  info(message: string, data?: any) {
    if (this.level <= LogLevel.INFO) {
      console.info(`[INFO] ${message}`, data);
    }
  }

  warn(message: string, data?: any) {
    if (this.level <= LogLevel.WARN) {
      console.warn(`[WARN] ${message}`, data);
    }
  }

  error(message: string, error?: Error) {
    if (this.level <= LogLevel.ERROR) {
      console.error(`[ERROR] ${message}`, error);
    }
  }
}

const logger = new Logger(LogLevel.DEBUG);
typescript// シグナル変更のトレースログ
const createTrackedSignal = <T>(name: string, initialValue: T) => {
  const [signal, setSignal] = createSignal(initialValue);
  
  const wrappedSetter = (value: T) => {
    logger.debug(`Signal "${name}" updating from ${signal()} to ${value}`);
    setSignal(value);
  };

  return [signal, wrappedSetter] as const;
};

具体例

よくあるエラーと解決方法

ケース1: シグナルのnull参照エラー

typescript// 問題のあるコード
const [user, setUser] = createSignal(null);

const UserProfile = () => {
  return (
    <div>
      <h1>{user().name}</h1> {/* Error: Cannot read property 'name' of null */}
    </div>
  );
};
typescript// 解決策1: 条件分岐による安全な参照
const UserProfile = () => {
  return (
    <div>
      {user() ? (
        <h1>{user().name}</h1>
      ) : (
        <p>ユーザー情報を読み込み中...</p>
      )}
    </div>
  );
};
typescript// 解決策2: オプショナルチェーニングの活用
const UserProfile = () => {
  return (
    <div>
      <h1>{user()?.name || 'ゲストユーザー'}</h1>
    </div>
  );
};

ケース2: 非同期処理でのエラーハンドリング

typescriptimport { createResource } from 'solid-js';

// APIからデータを取得する関数
const fetchUserData = async (userId: string) => {
  try {
    const response = await fetch(`/api/users/${userId}`);
    if (!response.ok) {
      throw new Error(`HTTP Error ${response.status}: ${response.statusText}`);
    }
    return await response.json();
  } catch (error) {
    console.error('Failed to fetch user data:', error);
    throw error;
  }
};
typescript// createResourceを使用した安全な実装
const UserData = () => {
  const [userId] = createSignal('123');
  const [userData] = createResource(userId, fetchUserData);

  return (
    <div>
      {userData.loading && <p>読み込み中...</p>}
      {userData.error && (
        <div class="error">
          <p>エラーが発生しました: {userData.error.message}</p>
          <button onClick={() => userData.refetch()}>
            再試行
          </button>
        </div>
      )}
      {userData() && (
        <div>
          <h2>{userData().name}</h2>
          <p>{userData().email}</p>
        </div>
      )}
    </div>
  );
};

デバッグ手順の実演

実際のデバッグプロセスを段階的に示します。

ステップ1: エラーの特定

typescript// 問題のあるコンポーネント
const ProblemComponent = () => {
  const [items, setItems] = createSignal([]);
  const [filter, setFilter] = createSignal('');

  // このコードでエラーが発生
  const filteredItems = () => {
    return items().filter(item => 
      item.name.toLowerCase().includes(filter().toLowerCase())
    );
  };

  return (
    <div>
      <input 
        value={filter()} 
        onInput={(e) => setFilter(e.target.value)}
        placeholder="フィルター"
      />
      <ul>
        {filteredItems().map(item => (
          <li key={item.id}>{item.name}</li>
        ))}
      </ul>
    </div>
  );
};

ステップ2: ログ出力によるデバッグ

typescriptconst ProblemComponent = () => {
  const [items, setItems] = createSignal([]);
  const [filter, setFilter] = createSignal('');

  const filteredItems = () => {
    console.log('items:', items()); // デバッグログ
    console.log('filter:', filter()); // デバッグログ
    
    return items().filter(item => {
      console.log('Processing item:', item); // 各アイテムをログ出力
      return item.name.toLowerCase().includes(filter().toLowerCase());
    });
  };

  // ... 省略
};

ステップ3: エラーハンドリングの追加

typescriptconst ProblemComponent = () => {
  const [items, setItems] = createSignal([]);
  const [filter, setFilter] = createSignal('');
  const [error, setError] = createSignal(null);

  const filteredItems = () => {
    try {
      setError(null); // エラーをクリア
      
      const itemList = items();
      if (!Array.isArray(itemList)) {
        throw new Error('Items must be an array');
      }

      return itemList.filter(item => {
        if (!item || typeof item.name !== 'string') {
          console.warn('Invalid item found:', item);
          return false;
        }
        return item.name.toLowerCase().includes(filter().toLowerCase());
      });
    } catch (err) {
      console.error('Error in filteredItems:', err);
      setError(err.message);
      return [];
    }
  };

  return (
    <div>
      {error() && (
        <div class="error-message">
          エラー: {error()}
        </div>
      )}
      <input 
        value={filter()} 
        onInput={(e) => setFilter(e.target.value)}
        placeholder="フィルター"
      />
      <ul>
        {filteredItems().map(item => (
          <li key={item.id}>{item.name}</li>
        ))}
      </ul>
    </div>
  );
};

実用的なコード例

デバッグ用ヘルパーコンポーネント

typescript// デバッグ情報を表示するコンポーネント
const DebugPanel = () => {
  const [isVisible, setIsVisible] = createSignal(false);
  
  return (
    <>
      {import.meta.env.DEV && (
        <div class="debug-panel">
          <button onClick={() => setIsVisible(!isVisible())}>
            {isVisible() ? 'デバッグパネルを閉じる' : 'デバッグパネルを開く'}
          </button>
          {isVisible() && (
            <div class="debug-content">
              <h3>デバッグ情報</h3>
              <pre>{JSON.stringify(debugInfo(), null, 2)}</pre>
            </div>
          )}
        </div>
      )}
    </>
  );
};

パフォーマンス測定ツール

typescript// パフォーマンス測定のためのヘルパー
const measurePerformance = (name: string, fn: () => any) => {
  if (import.meta.env.DEV) {
    const start = performance.now();
    const result = fn();
    const end = performance.now();
    console.log(`[PERF] ${name}: ${(end - start).toFixed(2)}ms`);
    return result;
  }
  return fn();
};

// 使用例
const ExpensiveComponent = () => {
  const [data, setData] = createSignal([]);

  const processedData = createMemo(() => 
    measurePerformance('data processing', () => {
      return data().map(item => ({
        ...item,
        computed: expensiveCalculation(item)
      }));
    })
  );

  return (
    <div>
      {processedData().map(item => (
        <div key={item.id}>{item.name}</div>
      ))}
    </div>
  );
};

まとめ

効率的なデバッグフローの確立

SolidJSでの効果的なデバッグには、以下のワークフローをおすすめします:

  1. エラーの分類: シグナル関連、コンポーネント関連、非同期処理関連に分けて考える
  2. ログ出力の活用: 戦略的にログを配置し、問題の所在を特定する
  3. 段階的な修正: 一つずつエラーを解決し、回帰テストを実施する
  4. 予防的措置: TypeScriptの型チェックとエラーハンドリングを徹底する

これらの手順を習慣化することで、開発効率が大幅に向上するでしょう。

エラー予防のベストプラクティス

最後に、エラーを未然に防ぐためのベストプラクティスをご紹介します:

分類対策具体的な方法
型安全性TypeScriptの活用厳格な型定義、null安全性の確保
エラーハンドリング包括的な例外処理try-catch文、ErrorBoundaryの設置
テスト自動テストの実装ユニットテスト、統合テストの充実
監視ログとモニタリングエラー追跡システムの導入
文書化コードドキュメントエラーパターンの文書化

SolidJSでの開発を成功させるために、これらの知識を実際のプロジェクトでぜひ活用してください。継続的な学習と実践により、より堅牢で保守しやすいアプリケーションが構築できるようになりますね。

関連リンク