SolidJS で無限ループが止まらない!createEffect/onCleanup の正しい書き方
SolidJS でリアクティブなアプリケーションを開発していると、createEffect を使った際に無限ループに遭遇することがあります。コンソールが止まらずにエラーが大量に出力されたり、ブラウザがフリーズしたりする経験をされた方も多いのではないでしょうか。
この記事では、SolidJS の createEffect と onCleanup の正しい使い方を、無限ループが発生する原因から解決策まで、実例を交えて詳しく解説します。初心者の方でも安心して理解できるよう、段階的に説明していきますので、ぜひ最後までお読みください。
背景
SolidJS のリアクティビティシステム
SolidJS は、細かい粒度のリアクティビティ(Fine-grained Reactivity)を採用している JavaScript フレームワークです。React や Vue とは異なり、仮想 DOM を使わずに、シグナルベースのリアクティブシステムで UI を更新します。
このリアクティビティの中核を担うのが、以下の 3 つの要素です。
| # | 要素 | 役割 | 例 |
|---|---|---|---|
| 1 | Signal | 状態を保持し変更を追跡 | createSignal() |
| 2 | Effect | Signal の変更に反応して副作用を実行 | createEffect() |
| 3 | Memo | Signal から派生した計算値をキャッシュ | createMemo() |
以下の図は、SolidJS のリアクティブシステムの基本的な流れを示したものです。
mermaidflowchart LR
signal["Signal<br/>(状態)"] -->|変更通知| effect["Effect<br/>(副作用)"]
signal -->|変更通知| memo["Memo<br/>(計算値)"]
memo -->|変更通知| effect
effect -->|状態更新| signal
style signal fill:#e1f5ff
style effect fill:#fff4e1
style memo fill:#f0e1ff
Signal が変更されると、それを監視している Effect や Memo に自動的に通知が届き、必要な処理が実行されます。
createEffect の基本的な役割
createEffect は、Signal の値が変更されたときに自動的に実行される副作用(side effect)を定義するための関数です。データの取得、DOM の操作、ログ出力など、状態変更に応じて何かを実行したい場合に使います。
typescriptimport { createSignal, createEffect } from 'solid-js';
typescriptfunction Counter() {
// Signal を作成(初期値: 0)
const [count, setCount] = createSignal(0);
typescript// Effect を作成(count が変わるたびに実行される)
createEffect(() => {
console.log('現在のカウント:', count());
});
typescript return (
<button onClick={() => setCount(count() + 1)}>
カウント: {count()}
</button>
);
}
上記のコードでは、ボタンをクリックして count の値が変わるたびに、createEffect 内の処理が自動的に実行されます。このように、createEffect は Signal の変更を監視し、それに応じた処理を行うための仕組みです。
onCleanup の基本的な役割
onCleanup は、Effect が再実行される前や、コンポーネントがアンマウントされるときに、クリーンアップ処理を実行するための関数です。タイマーの解除、イベントリスナーの削除、API リクエストのキャンセルなど、リソースの適切な解放に使われます。
typescriptimport { createEffect, onCleanup } from 'solid-js';
typescriptcreateEffect(() => {
// タイマーを設定
const timerId = setInterval(() => {
console.log("1秒経過");
}, 1000);
typescript // Effect が再実行される前にタイマーを解除
onCleanup(() => {
clearInterval(timerId);
console.log("タイマーを解除しました");
});
});
onCleanup を使うことで、メモリリークや予期しない動作を防ぎ、クリーンなコードを書くことができます。
図で理解できる要点:
- Signal が変更されると、Effect と Memo に自動的に通知される
- Effect は Signal を監視し、変更時に副作用を実行する
- onCleanup は Effect の再実行前やアンマウント時にリソースを解放する
課題
無限ループが発生する典型的なパターン
SolidJS で createEffect を使う際、最もよく遭遇する問題が無限ループです。以下のようなコードを書いてしまうと、ブラウザがフリーズしてしまいます。
typescriptimport { createSignal, createEffect } from 'solid-js';
typescriptfunction BadExample() {
const [count, setCount] = createSignal(0);
typescript// ❌ 無限ループが発生する
createEffect(() => {
console.log('カウント:', count());
// Effect 内で Signal を更新している
setCount(count() + 1);
});
typescript return <div>カウント: {count()}</div>;
}
このコードを実行すると、以下のようなエラーがコンソールに大量に出力されます。
plaintextError: Exceeded maximum number of effect iterations (500)
at runComputation (core.ts:245)
at createEffect (reactive.ts:78)
エラーコード: Error: Exceeded maximum number of effect iterations (500)
発生条件: Effect 内で監視している Signal を更新すると、以下の無限ループが発生します。
以下の図は、無限ループが発生するメカニズムを示しています。
mermaidflowchart LR
effect["Effect 実行"] -->|count を読み取り| signal["Signal: count"]
signal -->|変更通知| effect
effect -->|setCount 呼び出し| update["count を更新"]
update --> signal
style effect fill:#ffe1e1
style signal fill:#fff4e1
style update fill:#ffe1e1
- Effect が実行され、
count()を読み取る - Effect 内で
setCount()を呼び出し、countを更新 countの変更が Effect に通知される- 再び Effect が実行される(1 に戻る)
このサイクルが永遠に続くため、無限ループとなります。
よくある間違いの具体例
パターン 1: Effect 内での Signal 更新
最も典型的な間違いは、監視している Signal を Effect 内で直接更新してしまうことです。
typescript// ❌ 間違った例
createEffect(() => {
const currentValue = count();
// 条件分岐しても無限ループになる
if (currentValue < 100) {
setCount(currentValue + 1);
}
});
条件を付けても、条件が満たされる間は無限にループし続けます。
パターン 2: 複数の Signal を連鎖的に更新
複数の Signal を連鎖的に更新する場合も、無限ループに陥りやすいです。
typescriptfunction ChainUpdate() {
const [valueA, setValueA] = createSignal(0);
const [valueB, setValueB] = createSignal(0);
typescript// ❌ valueA が変わると valueB を更新
createEffect(() => {
setValueB(valueA() * 2);
});
typescript// ❌ valueB が変わると valueA を更新
createEffect(() => {
setValueA(valueB() / 2);
});
typescript return <div>A: {valueA()}, B: {valueB()}</div>;
}
この場合、valueA と valueB が互いに更新し合い、無限ループが発生します。
パターン 3: 依存関係の誤認識
Effect が意図しない Signal を監視してしまい、無限ループになるケースもあります。
typescriptfunction DependencyIssue() {
const [data, setData] = createSignal({ count: 0 });
typescript// ❌ data オブジェクト全体を更新してしまう
createEffect(() => {
const current = data();
// オブジェクトを新しく作成して設定
setData({ count: current.count + 1 });
});
typescript return <div>カウント: {data().count}</div>;
}
オブジェクト全体を新しく作成すると、参照が変わるため、Effect が再実行されます。
エラーが発生したときの症状
無限ループが発生すると、以下のような症状が現れます。
| # | 症状 | 説明 |
|---|---|---|
| 1 | コンソールの大量出力 | 同じログやエラーが連続して出力される |
| 2 | ブラウザのフリーズ | CPU 使用率が 100% になり、操作不能になる |
| 3 | メモリリーク | メモリ使用量が急激に増加し続ける |
| 4 | エラーメッセージ | Exceeded maximum number of effect iterations |
これらの症状が出た場合は、Effect 内で Signal を更新していないか、依存関係が循環していないかを確認する必要があります。
図で理解できる要点:
- Effect 内で監視している Signal を更新すると無限ループが発生
- Signal 同士が互いに更新し合う連鎖も無限ループの原因
- オブジェクトの参照を変えると意図しない再実行が起きる
解決策
createEffect を使う際の基本原則
無限ループを防ぐためには、以下の基本原則を守ることが重要です。
| # | 原則 | 説明 |
|---|---|---|
| 1 | 読み取り専用の原則 | Effect 内では Signal を読み取るだけで、更新しない |
| 2 | イベント駆動の原則 | Signal の更新はユーザーイベントや外部入力で行う |
| 3 | 単方向データフローの原則 | データの流れを一方向に保つ |
| 4 | createMemo の活用 | 派生値の計算には Effect ではなく Memo を使う |
以下の図は、正しいデータフローの概念を示しています。
mermaidflowchart TD
event["ユーザーイベント<br/>(クリック・入力)"] -->|トリガー| update["Signal 更新<br/>(setState)"]
update --> signal["Signal<br/>(状態)"]
signal -->|読み取り専用| effect["Effect<br/>(副作用)"]
signal -->|読み取り専用| memo["Memo<br/>(計算値)"]
effect -->|外部 API 呼び出し| external["外部リソース<br/>(API・タイマー)"]
memo -->|表示| ui["UI レンダリング"]
style event fill:#e1ffe1
style update fill:#ffe1e1
style signal fill:#e1f5ff
style effect fill:#fff4e1
style memo fill:#f0e1ff
Effect は Signal を読み取るだけで、更新はユーザーイベントや外部入力から行います。これにより、データフローが一方向になり、無限ループを防げます。
正しい createEffect の書き方
パターン 1: 副作用のみを実行する
Effect は、Signal の変更に応じて外部リソースを操作するために使います。Signal 自体は更新しません。
typescriptimport { createSignal, createEffect } from 'solid-js';
typescriptfunction CorrectExample() {
const [searchTerm, setSearchTerm] = createSignal("");
typescript// ✅ 正しい:Signal を読み取り、副作用のみを実行
createEffect(() => {
const term = searchTerm();
// 外部 API を呼び出す(Signal は更新しない)
if (term.length > 0) {
console.log(`検索中: ${term}`);
// fetch(`/api/search?q=${term}`)
// .then(res => res.json())
// .then(data => console.log(data));
}
});
typescript return (
<input
type="text"
placeholder="検索..."
onInput={(e) => setSearchTerm(e.currentTarget.value)}
/>
);
}
この例では、searchTerm が変わると Effect が実行されますが、Effect 内では searchTerm を更新していません。API 呼び出しという副作用のみを行っています。
パターン 2: createMemo で派生値を計算
Signal から派生した値が必要な場合は、Effect ではなく createMemo を使います。
typescriptimport { createSignal, createMemo } from 'solid-js';
typescriptfunction MemoExample() {
const [count, setCount] = createSignal(0);
typescript// ✅ 正しい:派生値の計算には createMemo を使う
const doubledCount = createMemo(() => {
return count() * 2;
});
typescript return (
<div>
<button onClick={() => setCount(count() + 1)}>
カウント: {count()}
</button>
<p>2倍: {doubledCount()}</p>
</div>
);
}
createMemo は計算結果をキャッシュし、依存する Signal が変わったときだけ再計算します。Effect と違い、値を返すことができます。
パターン 3: untrack で依存関係を制御
特定の Signal を監視対象から除外したい場合は、untrack を使います。
typescriptimport {
createSignal,
createEffect,
untrack,
} from 'solid-js';
typescriptfunction UntrackExample() {
const [count, setCount] = createSignal(0);
const [trigger, setTrigger] = createSignal(0);
typescript// ✅ trigger の変更のみを監視し、count は監視しない
createEffect(() => {
// trigger を読み取る(監視対象)
trigger();
// untrack で count を監視対象から除外
const currentCount = untrack(() => count());
console.log(`トリガー時のカウント: ${currentCount}`);
});
typescript return (
<div>
<button onClick={() => setCount(count() + 1)}>
カウント: {count()}
</button>
<button onClick={() => setTrigger(trigger() + 1)}>
トリガー実行
</button>
</div>
);
}
untrack を使うと、count が変わっても Effect は実行されず、trigger が変わったときだけ実行されます。
正しい onCleanup の書き方
onCleanup は、Effect が再実行される前や、コンポーネントがアンマウントされる前に、リソースを解放するために使います。
パターン 1: タイマーの解除
タイマーを設定した場合は、必ず onCleanup で解除します。
typescriptimport {
createSignal,
createEffect,
onCleanup,
} from 'solid-js';
typescriptfunction TimerExample() {
const [seconds, setSeconds] = createSignal(0);
typescript createEffect(() => {
// タイマーを設定
const interval = setInterval(() => {
setSeconds((s) => s + 1);
}, 1000);
typescript // ✅ コンポーネントがアンマウントされる前にタイマーを解除
onCleanup(() => {
clearInterval(interval);
console.log("タイマーを解除しました");
});
});
typescript return <div>経過時間: {seconds()}秒</div>;
}
onCleanup を使わないと、コンポーネントがアンマウントされた後もタイマーが動き続け、メモリリークが発生します。
パターン 2: イベントリスナーの削除
DOM にイベントリスナーを追加した場合も、必ず onCleanup で削除します。
typescriptimport { createEffect, onCleanup } from 'solid-js';
typescriptfunction EventListenerExample() {
createEffect(() => {
// イベントリスナーを追加
const handleResize = () => {
console.log("ウィンドウサイズ:", window.innerWidth);
};
window.addEventListener("resize", handleResize);
typescript // ✅ コンポーネントがアンマウントされる前にリスナーを削除
onCleanup(() => {
window.removeEventListener("resize", handleResize);
console.log("イベントリスナーを削除しました");
});
});
typescript return <div>ウィンドウサイズを監視中</div>;
}
イベントリスナーを削除しないと、コンポーネントがアンマウントされた後も、イベントが発火し続けます。
パターン 3: API リクエストのキャンセル
非同期処理を行う場合は、AbortController を使ってリクエストをキャンセルできるようにします。
typescriptimport {
createSignal,
createEffect,
onCleanup,
} from 'solid-js';
typescriptfunction FetchExample() {
const [query, setQuery] = createSignal("");
const [results, setResults] = createSignal([]);
typescript createEffect(() => {
const searchQuery = query();
// 空文字の場合は何もしない
if (!searchQuery) {
setResults([]);
return;
}
typescript// AbortController を作成
const controller = new AbortController();
typescript// API リクエストを実行
fetch(`/api/search?q=${searchQuery}`, {
signal: controller.signal,
})
.then((res) => res.json())
.then((data) => setResults(data))
.catch((err) => {
if (err.name !== 'AbortError') {
console.error('検索エラー:', err);
}
});
typescript // ✅ Effect が再実行される前にリクエストをキャンセル
onCleanup(() => {
controller.abort();
console.log("リクエストをキャンセルしました");
});
});
typescript return (
<div>
<input
type="text"
placeholder="検索..."
onInput={(e) => setQuery(e.currentTarget.value)}
/>
<ul>
{results().map((item) => (
<li>{item}</li>
))}
</ul>
</div>
);
}
ユーザーが素早く入力を変更した場合、古いリクエストをキャンセルすることで、不要な通信を減らせます。
図で理解できる要点:
- Effect は Signal を読み取るだけで、更新はしない
- 派生値の計算には createMemo を使う
- onCleanup でリソースを必ず解放し、メモリリークを防ぐ
具体例
実践例 1: リアルタイム検索機能
ユーザーの入力に応じて、リアルタイムで検索結果を表示する機能を実装してみましょう。
typescriptimport {
createSignal,
createEffect,
onCleanup,
} from 'solid-js';
typescript// 検索結果の型定義
type SearchResult = {
id: number;
title: string;
description: string;
};
typescriptfunction RealtimeSearch() {
// 検索クエリを管理する Signal
const [query, setQuery] = createSignal("");
// 検索結果を管理する Signal
const [results, setResults] = createSignal<SearchResult[]>([]);
// ローディング状態を管理する Signal
const [loading, setLoading] = createSignal(false);
typescript // query が変わるたびに検索を実行
createEffect(() => {
const searchQuery = query();
typescript// 空文字の場合は結果をクリア
if (!searchQuery) {
setResults([]);
setLoading(false);
return;
}
typescript// ローディング開始
setLoading(true);
typescript // デバウンス用のタイマーを設定(500ms 待機)
const timerId = setTimeout(() => {
// AbortController を作成
const controller = new AbortController();
typescript// API リクエストを実行
fetch(`/api/search?q=${encodeURIComponent(searchQuery)}`, {
signal: controller.signal,
})
.then((res) => res.json())
.then((data: SearchResult[]) => {
setResults(data);
setLoading(false);
})
.catch((err) => {
if (err.name !== 'AbortError') {
console.error('検索エラー:', err);
setLoading(false);
}
});
typescript // ✅ Effect が再実行される前にリクエストをキャンセル
onCleanup(() => {
controller.abort();
});
}, 500);
typescript // ✅ Effect が再実行される前にタイマーをクリア
onCleanup(() => {
clearTimeout(timerId);
});
});
typescript return (
<div>
<h2>リアルタイム検索</h2>
<input
type="text"
placeholder="検索キーワードを入力..."
value={query()}
onInput={(e) => setQuery(e.currentTarget.value)}
/>
typescript{
loading() && <p>検索中...</p>;
}
typescript <ul>
{results().map((result) => (
<li key={result.id}>
<h3>{result.title}</h3>
<p>{result.description}</p>
</li>
))}
</ul>
</div>
);
}
このコードのポイントは以下の通りです。
queryが変わるたびに Effect が実行される- 500ms のデバウンスを設定し、連続入力時の無駄なリクエストを防ぐ
AbortControllerでリクエストをキャンセル可能にするonCleanupでタイマーとリクエストの両方をクリーンアップする- Signal の更新は Effect 内ではなく、API のレスポンス時に行う
以下の図は、リアルタイム検索のフローを示しています。
mermaidsequenceDiagram
participant User as ユーザー
participant Input as 入力フィールド
participant Effect as createEffect
participant Timer as タイマー
participant API as API サーバー
User->>Input: "React" と入力
Input->>Effect: query 更新
Effect->>Timer: 500ms 待機開始
User->>Input: "ReactJS" と入力
Input->>Effect: query 更新
Effect->>Timer: 前のタイマーをクリア
Effect->>Timer: 500ms 待機開始
Timer->>API: 検索リクエスト送信
API-->>Effect: 検索結果を返却
Effect->>Input: 結果を表示
ユーザーが素早く入力を変更すると、前のタイマーがキャンセルされ、最新の入力に対してのみリクエストが送信されます。
実践例 2: WebSocket 接続の管理
WebSocket を使ったリアルタイム通信の実装例を見てみましょう。
typescriptimport {
createSignal,
createEffect,
onCleanup,
} from 'solid-js';
typescript// メッセージの型定義
type Message = {
id: string;
user: string;
text: string;
timestamp: number;
};
typescriptfunction ChatRoom() {
// 接続状態を管理する Signal
const [connected, setConnected] = createSignal(false);
// メッセージリストを管理する Signal
const [messages, setMessages] = createSignal<Message[]>([]);
// 入力中のメッセージを管理する Signal
const [inputText, setInputText] = createSignal("");
typescript// WebSocket への参照を保持する変数
let ws: WebSocket | null = null;
typescript // コンポーネントがマウントされたら WebSocket 接続を確立
createEffect(() => {
// WebSocket 接続を作成
ws = new WebSocket("wss://example.com/chat");
typescript// 接続が開かれたときの処理
ws.onopen = () => {
console.log('WebSocket 接続が確立されました');
setConnected(true);
};
typescript// メッセージを受信したときの処理
ws.onmessage = (event) => {
const message: Message = JSON.parse(event.data);
setMessages((prev) => [...prev, message]);
};
typescript// エラーが発生したときの処理
ws.onerror = (error) => {
console.error('WebSocket エラー:', error);
};
typescript// 接続が閉じられたときの処理
ws.onclose = () => {
console.log('WebSocket 接続が閉じられました');
setConnected(false);
};
typescript // ✅ コンポーネントがアンマウントされる前に接続を閉じる
onCleanup(() => {
if (ws) {
ws.close();
console.log("WebSocket 接続をクリーンアップしました");
}
});
});
typescript // メッセージ送信関数
const sendMessage = () => {
const text = inputText().trim();
if (!text || !ws || ws.readyState !== WebSocket.OPEN) {
return;
}
typescript // メッセージを送信
const message: Message = {
id: crypto.randomUUID(),
user: "現在のユーザー",
text: text,
timestamp: Date.now()
};
ws.send(JSON.stringify(message));
setInputText("");
};
typescript return (
<div>
<h2>チャットルーム</h2>
<div>
状態: {connected() ? "✅ 接続中" : "❌ 切断中"}
</div>
typescript<div style={{ height: '300px', overflow: 'auto' }}>
{messages().map((msg) => (
<div key={msg.id}>
<strong>{msg.user}</strong>: {msg.text}
</div>
))}
</div>
typescript <div>
<input
type="text"
value={inputText()}
onInput={(e) => setInputText(e.currentTarget.value)}
onKeyPress={(e) => {
if (e.key === "Enter") {
sendMessage();
}
}}
placeholder="メッセージを入力..."
disabled={!connected()}
/>
<button onClick={sendMessage} disabled={!connected()}>
送信
</button>
</div>
</div>
);
}
このコードのポイントは以下の通りです。
- Effect 内で WebSocket 接続を確立する
onCleanupで接続を必ず閉じ、リソースリークを防ぐ- メッセージの送信は Effect 外で行う(ユーザーイベント駆動)
- 接続状態を Signal で管理し、UI に反映する
実践例 3: 無限スクロールの実装
スクロール位置を監視して、追加データを読み込む無限スクロールを実装してみましょう。
typescriptimport {
createSignal,
createEffect,
onCleanup,
} from 'solid-js';
typescript// アイテムの型定義
type Item = {
id: number;
title: string;
content: string;
};
typescriptfunction InfiniteScroll() {
// アイテムリストを管理する Signal
const [items, setItems] = createSignal<Item[]>([]);
// 現在のページ番号を管理する Signal
const [page, setPage] = createSignal(1);
// ローディング状態を管理する Signal
const [loading, setLoading] = createSignal(false);
// すべてのデータを読み込んだかを管理する Signal
const [hasMore, setHasMore] = createSignal(true);
typescript// 初期データを読み込む
createEffect(() => {
loadMoreItems();
});
typescript // スクロールイベントを監視
createEffect(() => {
// スクロールイベントのハンドラ
const handleScroll = () => {
// 画面の下端までのスクロール量を計算
const scrollHeight = document.documentElement.scrollHeight;
const scrollTop = document.documentElement.scrollTop;
const clientHeight = document.documentElement.clientHeight;
typescript // 下端から 200px 以内までスクロールしたら追加読み込み
if (scrollHeight - scrollTop - clientHeight < 200) {
if (!loading() && hasMore()) {
loadMoreItems();
}
}
};
typescript// スクロールイベントを登録
window.addEventListener('scroll', handleScroll);
typescript // ✅ コンポーネントがアンマウントされる前にイベントを削除
onCleanup(() => {
window.removeEventListener("scroll", handleScroll);
});
});
typescript // データを読み込む関数
const loadMoreItems = async () => {
if (loading() || !hasMore()) {
return;
}
typescriptsetLoading(true);
typescript try {
// API からデータを取得
const response = await fetch(`/api/items?page=${page()}`);
const newItems: Item[] = await response.json();
typescript // データがない場合は読み込み完了
if (newItems.length === 0) {
setHasMore(false);
} else {
// 既存のアイテムに追加
setItems((prev) => [...prev, ...newItems]);
setPage((p) => p + 1);
}
} catch (error) {
console.error("データ読み込みエラー:", error);
} finally {
setLoading(false);
}
};
typescript return (
<div>
<h2>無限スクロール</h2>
<div>
{items().map((item) => (
<div key={item.id} style={{ padding: "20px", border: "1px solid #ccc" }}>
<h3>{item.title}</h3>
<p>{item.content}</p>
</div>
))}
</div>
typescript {loading() && <p>読み込み中...</p>}
{!hasMore() && <p>すべてのデータを読み込みました</p>}
</div>
);
}
このコードのポイントは以下の通りです。
- スクロールイベントを Effect で監視する
onCleanupでイベントリスナーを必ず削除する- データの読み込みは Effect 外の関数で行う
- ローディング状態を管理して、重複リクエストを防ぐ
以下の図は、無限スクロールの動作フローを示しています。
mermaidstateDiagram-v2
[*] --> Idle: ページ読み込み
Idle --> Scrolling: ユーザーがスクロール
Scrolling --> CheckPosition: スクロール位置確認
CheckPosition --> Idle: 下端から遠い
CheckPosition --> Loading: 下端に近い
Loading --> FetchData: API リクエスト
FetchData --> UpdateItems: データ取得成功
FetchData --> ErrorState: データ取得失敗
UpdateItems --> Idle: アイテム追加
UpdateItems --> Complete: データなし
ErrorState --> Idle: エラー処理
Complete --> [*]: 読み込み完了
ユーザーが下端近くまでスクロールすると、自動的に次のページのデータを読み込み、シームレスに表示し続けます。
図で理解できる要点:
- リアルタイム検索ではデバウンスと AbortController でリクエストを最適化
- WebSocket 接続は onCleanup で必ず閉じる
- 無限スクロールではイベントリスナーの適切な削除が重要
まとめ
この記事では、SolidJS の createEffect と onCleanup の正しい使い方について、無限ループの原因から解決策、実践的な具体例まで詳しく解説しました。
重要なポイントを振り返りましょう。
createEffect を使う際の鉄則
| # | 鉄則 | 説明 |
|---|---|---|
| 1 | 読み取り専用 | Effect 内で監視している Signal を更新しない |
| 2 | 副作用に専念 | API 呼び出しや DOM 操作など、外部リソースの操作のみを行う |
| 3 | createMemo を活用 | 派生値の計算には Effect ではなく Memo を使う |
| 4 | untrack で制御 | 特定の Signal を監視対象から除外する |
onCleanup を使う際の鉄則
| # | 鉄則 | 説明 |
|---|---|---|
| 1 | タイマーの解除 | setInterval/setTimeout は必ず clearInterval/clearTimeout する |
| 2 | イベントリスナーの削除 | addEventListener したら removeEventListener する |
| 3 | 接続の切断 | WebSocket や他の接続は必ず close する |
| 4 | リクエストのキャンセル | AbortController で不要なリクエストをキャンセルする |
よくあるエラーと対処法
エラーコード: Error: Exceeded maximum number of effect iterations (500)
解決方法:
- Effect 内で監視している Signal を更新していないか確認する
- Signal 同士が循環参照していないか確認する
- 必要に応じて untrack を使って依存関係を制御する
- 派生値の計算には createMemo を使う
SolidJS のリアクティビティシステムは強力ですが、正しく理解して使わないと無限ループなどの問題に遭遇します。この記事で紹介した原則とパターンを守れば、安全で効率的なコードを書けるようになるでしょう。
実際のアプリケーション開発では、リアルタイム検索、WebSocket 接続、無限スクロールなど、さまざまな場面で createEffect と onCleanup を活用できます。ぜひこの記事を参考に、SolidJS の開発を楽しんでください。
関連リンク
articleSolidJS フォーム設計の最適解:コントロール vs アンコントロールドの棲み分け
articleSolidJS コンポーネント間通信チート:Context・イベント・store の選択早見
articlesolidJS × SolidStart を Cloudflare Pages にデプロイ:Edge 最適化の手順
articleSolidJS のアニメーション比較:Motion One vs Popmotion vs CSS Transitions
articleSolidJS で無限ループが止まらない!createEffect/onCleanup の正しい書き方
articleSolidJS の Control Flow コンポーネント大全:Show/For/Switch/ErrorBoundary を使い分け
articleWebSocket Close コード早見表:正常終了・プロトコル違反・ポリシー違反の実務対応
articleStorybook 品質ゲート運用:Lighthouse/A11y/ビジュアル差分を PR で自動承認
articleWebRTC で高精細 1080p/4K 画面共有:contentHint「detail」と DPI 最適化
articleSolidJS フォーム設計の最適解:コントロール vs アンコントロールドの棲み分け
articleWebLLM 使い方入門:チャット UI を 100 行で実装するハンズオン
articleShell Script と Ansible/Make/Taskfile の比較:小規模自動化の最適解を検証
blogiPhone 17シリーズの発表!全モデルiPhone 16から進化したポイントを見やすく整理
blogGoogleストアから訂正案内!Pixel 10ポイント有効期限「1年」表示は誤りだった
blog【2025年8月】Googleストア「ストアポイント」は1年表記はミス?2年ルールとの整合性を検証
blogGoogleストアの注文キャンセルはなぜ起きる?Pixel 10購入前に知るべき注意点
blogPixcel 10シリーズの発表!全モデル Pixcel 9 から進化したポイントを見やすく整理
blogフロントエンドエンジニアの成長戦略:コーチングで最速スキルアップする方法
review今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
reviewついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
review愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
review週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
review新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
review科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来