👁️

SolidJSにおけるcreateEffectとbatch

に公開

はじめに

SolidJS では、createSignalで定義したシグナル(状態)が変化すると、自動的に依存している createEffectが再実行されます。本記事では、複数のシグナルを同時に更新したい場合に、意図せずcreateEffectが途中段階で発火してしまう現象と、その解決策として提供されているbatch関数の使い方を解説します。

背景

以下のようなシンプルなカウンター&ログ出力コンポーネントを例にします。

import { createSignal, createEffect, For } from "solid-js";

export default function Demo() {
  const [countA, setCountA] = createSignal(0);
  const [countB, setCountB] = createSignal(0);
  const [logs, setLogs] = createSignal<string[]>([]);

  createEffect(() => {
    setLogs(prev => [
      ...prev,
      `Effect fired: A=${countA()}  B=${countB()}`,
    ]);
  });

  return (
    <div>
      <button onClick={() => {
        setCountA(a => a + 1);
        setCountB(b => b + 1);
      }}>
        Increment A and B
      </button>
      <ul>
        <For each={logs()}>
          {log => <li>{log}</li>}
        </For>
      </ul>
    </div>
  );
}
  • 初期マウント時にcreateEffectが走り、A=0 B=0のログが追加される。
  • ボタンを押すと setCountAsetCountB の順で更新が走る。

問題の再現

ボタンを1回クリックすると、以下のように意図しない中間状態がログに残ります。

  • A=0 B=0 (マウント時)
  • A=1 B=0 (countA 更新後)
  • A=1 B=1 (countB 更新後)

今回はログに積むだけなので良いですが、createEffectの内容次第では致命的なバグになりかねません。

なぜ起こるのか

SolidJS のリアクティブシステムは、シグナルが更新されると即座にその依存エフェクトを再実行します:

  1. setCountA が呼ばれる
  2. countAに依存しているcreateEffectが再実行 → この時点のcountB()はまだ古い値
  3. 続けてsetCountBが呼ばれる
  4. さらにcreateEffectが再実行 → 両方の値が新しくなった状態

これにより、2回のエフェクト発火が起き、中間状態がログに残ってしまうわけです。

解決策:batch を使う

SolidJS が提供するbatch関数で、複数のシグナル更新をひとまとめにできます。内部では更新の発火を保留し、まとめて反映させた後に一度だけエフェクトを走らせます。

import { createSignal, createEffect, For, batch } from "solid-js";

export default function Demo() {
  const [countA, setCountA] = createSignal(0);
  const [countB, setCountB] = createSignal(0);
  const [logs, setLogs] = createSignal<string[]>([]);

  createEffect(() => {
    setLogs(prev => [
      ...prev,
      `Effect fired: A=${countA()}  B=${countB()}`,
    ]);
  });

  return (
    <div>
      <button onClick={() => {
        batch(() => {
          setCountA(a => a + 1);
          setCountB(b => b + 1);
        });
      }}>
        Increment A and B
      </button>
      <ul>
        <For each={logs()}>
          {log => <li>{log}</li>}
        </For>
      </ul>
    </div>
  );
}

このようにすると、ボタン1回のクリックでA=1 B=1 だけがログに追加され、中間状態は発生しません。

おまけ on ユーティリティ

特定のシグナルだけをトリガーにしたい場合は、createEffect(on(...)) を使って依存関係を明示的に定義できます。

import { createSignal, createEffect, on } from "solid-js";

createEffect(on(
  [countA, countB],
  ([a, b]) => {
    setLogs(prev => [
      ...prev,
      `Effect fired: A=${a}  B=${b}`,
    ]);
  }
));
  • onの第1引数に依存シグナルの配列
  • 第2引数でまとめて取得した値を扱う

これならbatchを使わずとも、複数シグナルの同時反映後に一度だけエフェクトが走ります。

まとめ

  • SolidJS のcreateEffectはシグナル更新ごとに即座に走る
  • 複数更新を同時にまとめたいときはbatchを使うと一度だけ発火
  • より細かい制御にはonユーティリティも活用可能

以上です。リアクティブな更新タイミングを正しく制御することで、思わぬバグやパフォーマンス劣化を防げます。それでは快適なSolidJSライフを!ここまでお読みいただきありがとうございます。

Discussion