T-CREATOR

SolidJS で無限ループが止まらない!createEffect/onCleanup の正しい書き方

SolidJS で無限ループが止まらない!createEffect/onCleanup の正しい書き方

SolidJS でリアクティブなアプリケーションを開発していると、createEffect を使った際に無限ループに遭遇することがあります。コンソールが止まらずにエラーが大量に出力されたり、ブラウザがフリーズしたりする経験をされた方も多いのではないでしょうか。

この記事では、SolidJS の createEffectonCleanup の正しい使い方を、無限ループが発生する原因から解決策まで、実例を交えて詳しく解説します。初心者の方でも安心して理解できるよう、段階的に説明していきますので、ぜひ最後までお読みください。

背景

SolidJS のリアクティビティシステム

SolidJS は、細かい粒度のリアクティビティ(Fine-grained Reactivity)を採用している JavaScript フレームワークです。React や Vue とは異なり、仮想 DOM を使わずに、シグナルベースのリアクティブシステムで UI を更新します。

このリアクティビティの中核を担うのが、以下の 3 つの要素です。

#要素役割
1Signal状態を保持し変更を追跡createSignal()
2EffectSignal の変更に反応して副作用を実行createEffect()
3MemoSignal から派生した計算値をキャッシュ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
  1. Effect が実行され、count() を読み取る
  2. Effect 内で setCount() を呼び出し、count を更新
  3. count の変更が Effect に通知される
  4. 再び 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>;
}

この場合、valueAvalueB が互いに更新し合い、無限ループが発生します。

パターン 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単方向データフローの原則データの流れを一方向に保つ
4createMemo の活用派生値の計算には 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 の createEffectonCleanup の正しい使い方について、無限ループの原因から解決策、実践的な具体例まで詳しく解説しました。

重要なポイントを振り返りましょう。

createEffect を使う際の鉄則

#鉄則説明
1読み取り専用Effect 内で監視している Signal を更新しない
2副作用に専念API 呼び出しや DOM 操作など、外部リソースの操作のみを行う
3createMemo を活用派生値の計算には Effect ではなく Memo を使う
4untrack で制御特定の Signal を監視対象から除外する

onCleanup を使う際の鉄則

#鉄則説明
1タイマーの解除setInterval/setTimeout は必ず clearInterval/clearTimeout する
2イベントリスナーの削除addEventListener したら removeEventListener する
3接続の切断WebSocket や他の接続は必ず close する
4リクエストのキャンセルAbortController で不要なリクエストをキャンセルする

よくあるエラーと対処法

エラーコード: Error: Exceeded maximum number of effect iterations (500)

解決方法:

  1. Effect 内で監視している Signal を更新していないか確認する
  2. Signal 同士が循環参照していないか確認する
  3. 必要に応じて untrack を使って依存関係を制御する
  4. 派生値の計算には createMemo を使う

SolidJS のリアクティビティシステムは強力ですが、正しく理解して使わないと無限ループなどの問題に遭遇します。この記事で紹介した原則とパターンを守れば、安全で効率的なコードを書けるようになるでしょう。

実際のアプリケーション開発では、リアルタイム検索、WebSocket 接続、無限スクロールなど、さまざまな場面で createEffectonCleanup を活用できます。ぜひこの記事を参考に、SolidJS の開発を楽しんでください。

関連リンク