SolidJS lifecycle: onMount for DOM access after mount, onCleanup for resource disposal, effect lifecycle management, component vs effect cleanup.
Complete guide to managing component and effect lifecycle in SolidJS. Understanding lifecycle is essential for proper resource cleanup and avoiding memory leaks.
onMount runs code after components mount to the DOM. Perfect for accessing refs, setting up third-party libraries, and one-time initializations.
import { onMount } from "solid-js";
function MyComponent() {
let ref: HTMLButtonElement;
// Runs after component mounts to DOM
onMount(() => {
ref.disabled = true;
// Access DOM elements safely
// Initialize third-party libraries
// Set up one-time event listeners
});
return <button ref={ref}>Focus me!</button>;
}
Key characteristics:
createEffect with no dependencies)Use cases:
onCleanup registers cleanup methods that execute when components unmount or tracking scopes dispose. Essential for preventing memory leaks.
import { createSignal, onCleanup } from "solid-js";
function Component() {
const [count, setCount] = createSignal(0);
const handleClick = () => setCount((value) => value + 1);
// Add event listener
document.addEventListener("click", handleClick);
// Cleanup when component unmounts
onCleanup(() => {
document.removeEventListener("click", handleClick);
});
return <main>Document has been clicked {count()} times</main>;
}
When cleanup runs:
Effects have their own lifecycle. Cleanup runs before effect re-executes:
import { createEffect, onCleanup, createSignal } from "solid-js";
function Component() {
const [id, setId] = createSignal(1);
createEffect(() => {
const currentId = id();
// Setup
const subscription = subscribeToData(currentId);
// Cleanup runs:
// 1. Before effect re-executes (when id changes)
// 2. When effect is disposed (component unmounts)
onCleanup(() => {
subscription.unsubscribe();
});
});
return <button onClick={() => setId(id() + 1)}>Next</button>;
}
Cleanup execution order:
Component lifecycle:
onMount runs after DOM mountonCleanup runs on unmountEffect lifecycle:
function Component() {
const [count, setCount] = createSignal(0);
// Component body runs ONCE
console.log("Component initialized");
// Runs after mount
onMount(() => {
console.log("Component mounted");
});
// Runs on unmount
onCleanup(() => {
console.log("Component cleanup");
});
// Effect runs when count changes
createEffect(() => {
console.log("Effect runs:", count());
onCleanup(() => {
console.log("Effect cleanup");
});
});
return <button onClick={() => setCount(count() + 1)}>Count: {count()}</button>;
}
function Component() {
const handleResize = () => {
// Handle resize
};
window.addEventListener("resize", handleResize);
onCleanup(() => {
window.removeEventListener("resize", handleResize);
});
}
function Component() {
const [data, setData] = createSignal(null);
createEffect(() => {
const subscription = observable.subscribe((value) => {
setData(value);
});
onCleanup(() => {
subscription.unsubscribe();
});
});
}
function Component() {
createEffect(() => {
const interval = setInterval(() => {
// Do something
}, 1000);
onCleanup(() => {
clearInterval(interval);
});
});
}
function Component() {
createEffect(() => {
const controller = new AbortController();
fetch("/api/data", { signal: controller.signal })
.then((res) => res.json())
.then(setData);
onCleanup(() => {
controller.abort();
});
});
}
You can register multiple cleanups - they all run in reverse order:
function Component() {
onMount(() => {
const listener1 = setupListener1();
const listener2 = setupListener2();
onCleanup(() => {
listener2.cleanup();
});
onCleanup(() => {
listener1.cleanup();
});
// Cleanup order: listener1, then listener2 (reverse registration)
});
}
function Component() {
let inputRef: HTMLInputElement;
onMount(() => {
// Safe to access ref after mount
inputRef.focus();
inputRef.select();
});
return <input ref={inputRef} />;
}
function ChartComponent() {
let chartContainer: HTMLDivElement;
let chartInstance: Chart;
onMount(() => {
// Initialize chart library
chartInstance = new Chart(chartContainer, {
// config
});
});
onCleanup(() => {
// Cleanup chart instance
if (chartInstance) {
chartInstance.destroy();
}
});
return <div ref={chartContainer} />;
}
function Component() {
createEffect(() => {
let cleanup: (() => void) | undefined;
if (someCondition()) {
cleanup = setupSomething();
}
onCleanup(() => {
if (cleanup) {
cleanup();
}
});
});
}
function Component() {
createEffect(() => {
let cancelled = false;
fetchData().then((data) => {
if (!cancelled) {
setData(data);
}
});
onCleanup(() => {
cancelled = true;
});
});
}
// ❌ Wrong - ref may not be attached yet
function Component() {
let ref: HTMLDivElement;
ref.focus(); // Error!
return <div ref={ref} />;
}
// ✅ Correct
function Component() {
let ref: HTMLDivElement;
onMount(() => {
ref.focus(); // Safe
});
return <div ref={ref} />;
}
// ❌ Wrong - memory leak
function Component() {
document.addEventListener("click", handleClick);
// No cleanup!
}
// ✅ Correct
function Component() {
document.addEventListener("click", handleClick);
onCleanup(() => {
document.removeEventListener("click", handleClick);
});
}
// ❌ Wrong - onMount doesn't track
function Component() {
const [count, setCount] = createSignal(0);
onMount(() => {
// This only runs once, won't react to count changes
console.log(count());
});
}
// ✅ Correct - use createEffect
function Component() {
const [count, setCount] = createSignal(0);
createEffect(() => {
// This tracks count and runs when it changes
console.log(count());
});
}