Best Practices Setup patterns, lifecycle hooks, and cleanup.
| Scenario | Use Setup? | Example |
|---|---|---|
| Component has reactive state | Yes | signal(0), signal([]) |
| Component handles events | Yes | Click handlers, form submission |
| Component uses lifecycle hooks | Yes | onMount, onUnmount |
| Component receives props | Yes | Access via props parameter |
| Component emits events | Yes | Access via emitter parameter |
| Purely static display | Optional | Can omit setup entirely |
// With setup - component has state and behavior
app.component("Counter", {
setup: ({ signal }) => {
const count = signal(0);
return {
count,
increment: () => count.value++
};
},
template: (ctx) => `<button @click="increment">${ctx.count.value}</button>`
});
// Without setup - purely static component
app.component("Logo", {
template: () => `<img src="/logo.png" alt="Logo" />`
});
// Destructure only what's needed
setup: ({ signal }) => {
const count = signal(0);
return { count };
}
// Multiple utilities
setup: ({ signal, emitter, props }) => {
const items = signal(props.initialItems || []);
emitter.on("refresh", () => loadItems());
return { items };
}
Structure your setup function in this order:
setup: ({ signal, emitter, props }) => {
// 1. Props extraction
const { userId, initialData } = props;
// 2. Reactive state (signals)
const items = signal(initialData || []);
const loading = signal(false);
// 3. Computed/derived values (functions that read signals)
const getItemCount = () => items.value.length;
// 4. Actions/handlers (functions that modify state)
async function loadItems() {
loading.value = true;
const response = await fetch(`/api/users/${userId}/items`);
items.value = await response.json();
loading.value = false;
}
// 5. Event subscriptions
let unsubscribe = null;
// 6. Return public interface + lifecycle hooks
return {
// State & functions
items,
loading,
getItemCount,
loadItems,
// Lifecycle hooks (returned, not destructured!)
onMount: () => {
loadItems();
unsubscribe = emitter.on("refresh:items", loadItems);
},
onUnmount: () => {
if (unsubscribe) unsubscribe();
}
};
}
Only return what the template needs:
// Avoid: Returning everything
setup: ({ signal }) => {
const count = signal(0);
const internalCache = new Map(); // Not needed in template
const helperFn = () => { /* ... */ }; // Only used internally
function increment() {
helperFn();
count.value++;
internalCache.set(count.value, Date.now());
}
return { count, increment, internalCache, helperFn }; // Too much!
}
// Better: Return only template-facing API
setup: ({ signal }) => {
const count = signal(0);
const internalCache = new Map();
const helperFn = () => { /* ... */ };
function increment() {
helperFn();
count.value++;
internalCache.set(count.value, Date.now());
}
return { count, increment }; // Only what template needs
}
| Pattern | When to Use |
|---|---|
| Arrow with implicit return | Simple state, no logic |
| Arrow with block body | Most components (recommended) |
| Regular function | Need this binding (rare) |
// Arrow with implicit return - simplest components
app.component("SimpleCounter", {
setup: ({ signal }) => ({ count: signal(0) }),
template: (ctx) => `<p>${ctx.count.value}</p>`
});
// Arrow with block - most common, recommended
app.component("Counter", {
setup: ({ signal }) => {
const count = signal(0);
const increment = () => count.value++;
return { count, increment };
},
template: (ctx) => `
<button @click="increment">${ctx.count.value}</button>
`
});
| Hook | When Called | Common Use Cases |
|---|---|---|
onBeforeMount |
Before component renders to DOM | Normalize props, prepare data |
onMount |
After component renders to DOM | Fetch data, set up subscriptions |
onBeforeUpdate |
Before component re-renders | Compare old/new state |
onUpdate |
After component re-renders | DOM measurements, third-party sync |
onUnmount |
Before component is destroyed | Cleanup subscriptions, timers |
Note:
onUnmountis called when: (1)unmount()is called explicitly, (2) the parent component unmounts, or (3) a parent re-render removes the child’s host element from the DOM.
Component Created
|
v
onBeforeMount <- Props available, initial data ready
|
v (DOM renders)
onMount <- DOM available, fetch data, set up listeners
|
v (User interacts, state changes)
onBeforeUpdate <- Before re-render
|
v (DOM updates)
onUpdate <- After re-render
|
v (Component removed)
onUnmount <- Cleanup everything
Important: Lifecycle hooks are returned from setup, not destructured from it. They receive a context object with container and context. The onUnmount hook also receives a cleanup object containing { watchers, listeners, children } arrays for advanced cleanup scenarios.
setup: ({ signal }) => {
const count = signal(0);
return {
count,
// Hooks are returned as properties
onMount: ({ container, context }) => {
console.log("Mounted to:", container);
},
onUnmount: ({ container, context, cleanup }) => {
// cleanup contains { watchers, listeners, children } arrays
console.log("Unmounting...");
console.log("Active watchers:", cleanup.watchers.length);
console.log("Event listeners:", cleanup.listeners.length);
console.log("Child components:", cleanup.children.length);
}
};
}
Use for initialization that requires the DOM or async operations:
setup: ({ signal }) => {
const users = signal([]);
const loading = signal(true);
async function fetchUsers() {
const response = await fetch("/api/users");
users.value = await response.json();
loading.value = false;
}
return {
users,
loading,
onMount: async () => {
await fetchUsers();
}
};
}
Common onMount use cases:
Always clean up what you set up in onMount:
setup: ({ signal }) => {
const windowWidth = signal(window.innerWidth);
let intervalId = null;
let resizeHandler = null;
return {
windowWidth,
onMount: () => {
resizeHandler = () => { windowWidth.value = window.innerWidth; };
window.addEventListener("resize", resizeHandler);
intervalId = setInterval(() => console.log("Tick"), 1000);
},
onUnmount: () => {
window.removeEventListener("resize", resizeHandler);
clearInterval(intervalId);
}
};
}
What to clean up:
| Resource | Cleanup Method | Auto-cleaned? |
|---|---|---|
Template event listeners (@click, etc.) |
- | ✓ Yes |
| Signal watchers | - | ✓ Yes |
| Child components | - | ✓ Yes |
| Window/document event listeners | removeEventListener() |
No |
| Timers | clearTimeout(), clearInterval() |
No |
| Emitter subscriptions | Call unsubscribe function | No |
| WebSocket | socket.close() |
No |
| AbortController | controller.abort() |
No |
| Third-party libraries | Library-specific destroy method | No |
The onUnmount hook receives a cleanup object for advanced scenarios where you need to inspect or work with Eleva’s internal cleanup process:
setup: ({ signal, emitter }) => {
const data = signal([]);
return {
data,
onMount: () => {
// Set up various resources
emitter.on("data:refresh", loadData);
window.addEventListener("online", handleOnline);
},
onUnmount: ({ container, context, cleanup }) => {
// cleanup.watchers - Array of active signal watchers
// cleanup.listeners - Array of registered event listeners
// cleanup.children - Array of mounted child components
// Useful for debugging or conditional cleanup
if (cleanup.watchers.length > 0) {
console.log(`Cleaning up ${cleanup.watchers.length} watchers`);
}
// Your manual cleanup still needed for external resources
window.removeEventListener("online", handleOnline);
}
};
}
Note: Eleva automatically cleans up signal watchers, template event listeners, and child components. The
cleanupobject is provided for debugging, logging, or advanced scenarios where you need visibility into what’s being cleaned up.
// DON'T: Heavy synchronous work in onBeforeMount
return {
onBeforeMount: () => {
const result = heavyComputation(millionItems); // Blocks rendering!
}
};
// DON'T: Forget cleanup - this causes memory leaks!
return {
onMount: () => {
window.addEventListener("scroll", handleScroll);
// Memory leak if not removed in onUnmount!
}
// Missing onUnmount cleanup!
};
// DON'T: Set state in onUpdate (infinite loop)
return {
onUpdate: () => {
count.value++; // Triggers another update - infinite loop!
}
};
// DON'T: Async in onBeforeMount (blocks first paint)
return {
onBeforeMount: async () => {
await fetchData(); // Eleva waits, but this delays initial render
}
};
// DO: Async in onMount (non-blocking)
return {
onMount: async () => {
await fetchData(); // DOM already rendered, user sees content faster
}
};
// DO: Always clean up what you set up
let handler = null;
return {
onMount: () => {
handler = () => console.log("scrolled");
window.addEventListener("scroll", handler);
},
onUnmount: () => {
window.removeEventListener("scroll", handler);
}
};
| Task | Recommended Hook |
|---|---|
| Fetch initial data | onMount |
| Normalize props | onBeforeMount |
| Set up event listeners | onMount |
| Remove event listeners | onUnmount |
| Clear timers/intervals | onUnmount |
| Cancel pending requests | onUnmount |
| Initialize third-party library | onMount |
| Destroy third-party library | onUnmount |
| Focus an input element | onMount |
| Measure DOM elements | onMount or onUpdate |
| ← Selectors & Structure | Signals & Templates → |