チュートリアル: JavaScript

ネストした Effects

Effect をネストしてスコープ付きリアクティブコンテキストを作成できます。

Effect の所有権

別の Effect 内で Effect を作成すると、内側の Effect は外側の「所有」になります:

const [showPanel, setShowPanel] = createSignal(false);
const [count, setCount] = createSignal(0);

createEffect(() => {
  if (showPanel()) {
    console.log("Panel shown");

    // この Effect は showPanel が true の間のみ存在
    createEffect(() => {
      console.log("Count:", count());
    });
  }
});

setShowPanel(true);   // "Panel shown", "Count: 0"
setCount(1);          // "Count: 1"
setShowPanel(false);  // "Panel shown"(内側の Effect は破棄)
setCount(2);          // 何も出力されない(内側の Effect はもう存在しない)

自動クリーンアップ

外側の Effect が再実行されると、すべての内側の Effect は自動的に破棄されます:

const [page, setPage] = createSignal("home");
const [data, setData] = createSignal(null);

createEffect(() => {
  const currentPage = page();
  console.log("Loading page:", currentPage);

  // この Effect は page が変更されると破棄される
  createEffect(() => {
    console.log("Data updated:", data());
  });
});

setData({ title: "Home" });  // "Data updated: {title: Home}"
setPage("about");            // "Loading page: about"(内側の Effect 破棄)
setData({ title: "About" }); // "Data updated: {title: About}"(新しい内側の Effect)

ユースケース

条件付き購読

const [isLoggedIn, setIsLoggedIn] = createSignal(false);
const [notifications, setNotifications] = createSignal([]);

createEffect(() => {
  if (isLoggedIn()) {
    // ログイン時のみ通知を購読
    createEffect(() => {
      const notifs = notifications();
      updateNotificationBadge(notifs.length);
    });
  }
});

スコープ付きリソース

const [selectedUser, setSelectedUser] = createSignal(null);

createEffect(() => {
  const user = selectedUser();
  if (!user) return;

  // 選択されたユーザーにスコープされた WebSocket 接続
  const ws = new WebSocket(`/ws/user/${user.id}`);

  // メッセージ用の内側の Effect
  createEffect(() => {
    ws.onmessage = (e) => {
      handleMessage(JSON.parse(e.data));
    };
  });

  onCleanup(() => {
    ws.close();
  });
});

注意:メモリリーク

リアクティブコンテキスト外で作成された Effect に注意:

// 間違い: Effect がクリーンアップされない
document.addEventListener("click", () => {
  createEffect(() => {
    console.log(count());  // メモリリーク!
  });
});

// 正しい: ライフサイクルを管理
let dispose;
document.addEventListener("click", () => {
  dispose?.();  // 前のをクリーンアップ
  dispose = createEffect(() => {
    console.log(count());
  });
});

試してみよう

各タブが独自のカウンターを持ち、そのタブがアクティブな間のみ追跡される「タブ」コンポーネントを作成:

解答
const [activeTab, setActiveTab] = createSignal("a");
const [countA, setCountA] = createSignal(0);
const [countB, setCountB] = createSignal(0);

createEffect(() => {
  const tab = activeTab();
  console.log("Switched to tab:", tab);

  if (tab === "a") {
    createEffect(() => {
      console.log("Tab A count:", countA());
    });
  } else {
    createEffect(() => {
      console.log("Tab B count:", countB());
    });
  }
});

// アクティブなタブのみログ
setCountA(1);  // "Tab A count: 1"(タブ A がアクティブな場合)
setActiveTab("b");  // "Switched to tab: b"
setCountA(2);  // 何も出力されない(タブ A の Effect 破棄)
setCountB(1);  // "Tab B count: 1"

次へ

Show(条件付きレンダリング)→ について学ぶ