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ではシグナルグラフを理解する必要があります。これは、データフローが直接的で効率的である一方、デバッグ時には追跡が複雑になることを意味しているのです。
項目 | React | SolidJS |
---|---|---|
エラー発生場所 | コンポーネントツリー内 | シグナルグラフ内 |
デバッグ対象 | レンダリングサイクル | リアクティブ更新チェーン |
情報の可視化 | React DevTools | SolidJS 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での効果的なデバッグには、以下のワークフローをおすすめします:
- エラーの分類: シグナル関連、コンポーネント関連、非同期処理関連に分けて考える
- ログ出力の活用: 戦略的にログを配置し、問題の所在を特定する
- 段階的な修正: 一つずつエラーを解決し、回帰テストを実施する
- 予防的措置: TypeScriptの型チェックとエラーハンドリングを徹底する
これらの手順を習慣化することで、開発効率が大幅に向上するでしょう。
エラー予防のベストプラクティス
最後に、エラーを未然に防ぐためのベストプラクティスをご紹介します:
分類 | 対策 | 具体的な方法 |
---|---|---|
型安全性 | TypeScriptの活用 | 厳格な型定義、null安全性の確保 |
エラーハンドリング | 包括的な例外処理 | try-catch文、ErrorBoundaryの設置 |
テスト | 自動テストの実装 | ユニットテスト、統合テストの充実 |
監視 | ログとモニタリング | エラー追跡システムの導入 |
文書化 | コードドキュメント | エラーパターンの文書化 |
SolidJSでの開発を成功させるために、これらの知識を実際のプロジェクトでぜひ活用してください。継続的な学習と実践により、より堅牢で保守しやすいアプリケーションが構築できるようになりますね。
関連リンク
- review
今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
- review
ついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
- review
愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
- review
週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
- review
新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
- review
科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来