Nested Effects
Effects can be nested to create scoped reactive contexts.
Effect Ownership
When you create an effect inside another effect, the inner effect becomes "owned" by the outer one:
const [showPanel, setShowPanel] = createSignal(false);
const [count, setCount] = createSignal(0);
createEffect(() => {
if (showPanel()) {
console.log("Panel shown");
// This effect only exists while showPanel is true
createEffect(() => {
console.log("Count:", count());
});
}
});
setShowPanel(true); // "Panel shown", "Count: 0"
setCount(1); // "Count: 1"
setShowPanel(false); // "Panel shown" (inner effect is disposed)
setCount(2); // Nothing (inner effect no longer exists)
Automatic Cleanup
When an outer effect re-runs, all inner effects are automatically disposed:
const [page, setPage] = createSignal("home");
const [data, setData] = createSignal(null);
createEffect(() => {
const currentPage = page();
console.log("Loading page:", currentPage);
// This effect is disposed when page changes
createEffect(() => {
console.log("Data updated:", data());
});
});
setData({ title: "Home" }); // "Data updated: {title: Home}"
setPage("about"); // "Loading page: about" (inner effect disposed)
setData({ title: "About" }); // "Data updated: {title: About}" (new inner effect)
Use Cases
Conditional Subscriptions
const [isLoggedIn, setIsLoggedIn] = createSignal(false);
const [notifications, setNotifications] = createSignal([]);
createEffect(() => {
if (isLoggedIn()) {
// Only subscribe to notifications when logged in
createEffect(() => {
const notifs = notifications();
updateNotificationBadge(notifs.length);
});
}
});
Scoped Resources
const [selectedUser, setSelectedUser] = createSignal(null);
createEffect(() => {
const user = selectedUser();
if (!user) return;
// WebSocket connection scoped to selected user
const ws = new WebSocket(`/ws/user/${user.id}`);
// Inner effect for messages
createEffect(() => {
ws.onmessage = (e) => {
handleMessage(JSON.parse(e.data));
};
});
onCleanup(() => {
ws.close();
});
});
Dynamic Effect Chains
const [tabs, setTabs] = createSignal(["home", "profile"]);
createEffect(() => {
// Create an effect for each tab
tabs().forEach(tab => {
createEffect(() => {
console.log(`Tab ${tab} is active:`, activeTab() === tab);
});
});
});
// When tabs change, old effects are disposed, new ones created
setTabs(["home", "settings", "profile"]);
Owner Context
Effects track their "owner" - the context that created them:
import { getOwner, runWithOwner } from '@luna_ui/luna';
const [count, setCount] = createSignal(0);
createEffect(() => {
const owner = getOwner();
// Later, run code in this owner's context
setTimeout(() => {
runWithOwner(owner, () => {
// This effect is owned by the original effect
createEffect(() => {
console.log("Delayed effect:", count());
});
});
}, 1000);
});
Memo Ownership
Memos also participate in ownership:
createEffect(() => {
// This memo is owned by the effect
const doubled = createMemo(() => count() * 2);
console.log("Doubled:", doubled());
});
// When effect re-runs, memo is recreated
Common Patterns
Conditional Feature Flags
const [features, setFeatures] = createSignal({ analytics: false });
createEffect(() => {
if (features().analytics) {
// Analytics tracking only when enabled
createEffect(() => {
trackPageView(currentPage());
});
}
});
Tab Content
const [activeTab, setActiveTab] = createSignal("overview");
createEffect(() => {
const tab = activeTab();
// Each tab has its own reactive context
if (tab === "overview") {
createEffect(() => loadOverviewData());
} else if (tab === "details") {
createEffect(() => loadDetailsData());
} else if (tab === "comments") {
createEffect(() => loadCommentsData());
}
});
Caution: Memory Leaks
Be careful with effects created outside reactive contexts:
// Wrong: effect never cleaned up
document.addEventListener("click", () => {
createEffect(() => {
console.log(count()); // Memory leak!
});
});
// Correct: manage lifecycle
let dispose;
document.addEventListener("click", () => {
dispose?.(); // Clean up previous
dispose = createEffect(() => {
console.log(count());
});
});
Try It
Create a "tabs" component where each tab has its own counter that's only tracked while that tab is active:
Solution
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());
});
}
});
// Only logs for the active tab
setCountA(1); // "Tab A count: 1" (if tab A is active)
setActiveTab("b"); // "Switched to tab: b"
setCountA(2); // Nothing (tab A effect disposed)
setCountB(1); // "Tab B count: 1"
Next
Learn about Show (Conditional Rendering) โ