Version: 1.0.0 Patterns and guidelines for building maintainable Eleva applications.
Follow this recommended order for component properties:
app.component("MyComponent", {
// 1. Setup - Initialize state (optional)
setup({ signal, emitter, props }) {
const state = signal(initialValue);
return { state, /* ...other exports */ };
},
// 2. Template - Define HTML structure (required)
template: (ctx) => `
<div>${ctx.state.value}</div>
`,
// 3. Style - Component-scoped CSS (optional)
style: `
div { color: blue; }
`,
// 4. Children - Child component mappings (optional)
children: {
".child-container": "ChildComponent"
}
});
Why this order?
setup initializes the data that template and style might referencetemplate defines the structure that style will stylestyle applies to the template’s elementschildren maps to elements created in the template| 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 |
Structure your setup function in this order for consistency:
setup: ({ signal, emitter, props }) => {
// 1. Props extraction (if needed)
const { userId, initialData } = props;
// 2. Reactive state (signals)
const items = signal(initialData || []);
const loading = signal(false);
const error = signal(null);
// 3. Computed/derived values (functions that read signals)
const getSelectedItem = () => items.value.find(i => i.selected);
const getItemCount = () => items.value.length;
// 4. Actions/handlers (functions that modify state)
async function loadItems() {
loading.value = true;
try {
const response = await fetch(`/api/users/${userId}/items`);
items.value = await response.json();
} catch (err) {
error.value = err.message;
} finally {
loading.value = false;
}
}
// 5. Event subscription ref (will be set in onMount)
let unsubscribe = null;
// 6. Return public interface + lifecycle hooks
return {
// State
items,
loading,
error,
// Computed
getSelectedItem,
getItemCount,
// Actions
loadItems,
// Lifecycle hooks
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
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
}
| Data Type | Use Signal? | Why |
|---|---|---|
| UI state (counts, toggles, form values) | Yes | Triggers re-render on change |
| Data from API | Yes | UI updates when data loads |
| Derived/computed values | No | Use functions instead |
| Constants | No | Never changes |
| Internal helpers (caches, refs) | No | Not displayed in UI |
setup: ({ signal }) => {
// Use signals for reactive UI state
const count = signal(0);
const isOpen = signal(false);
const items = signal([]);
// Don't use signals for constants
const API_URL = "/api/users";
const MAX_ITEMS = 100;
// Don't use signals for internal refs
let timerRef = null;
const cache = new Map();
// Don't use signals for computed values - use functions
const getItemCount = () => items.value.length;
const getTotal = () => items.value.reduce((a, b) => a + b.price, 0);
return { count, isOpen, items, getItemCount, getTotal };
}
CRITICAL: Mutations Don’t Trigger Reactivity
Eleva’s signals use identity comparison (===) to detect changes. When you mutate an object or array, the reference stays the same, so Eleva doesn’t know anything changed.
const user = signal({ name: "John", settings: { theme: "dark" } });
const items = signal([1, 2, 3]);
// WRONG: Mutation (won't trigger re-render!)
user.value.name = "Jane";
items.value.push(4);
// CORRECT: Replacement (triggers re-render!)
user.value = { ...user.value, name: "Jane" };
items.value = [...items.value, 4];
Array Operations Quick Reference:
| Instead of (won’t work) | Use this (works) |
|---|---|
arr.value.push(item) |
arr.value = [...arr.value, item] |
arr.value.pop() |
arr.value = arr.value.slice(0, -1) |
arr.value.shift() |
arr.value = arr.value.slice(1) |
arr.value.splice(i, 1) |
arr.value = arr.value.filter((_, idx) => idx !== i) |
arr.value[i] = x |
arr.value = arr.value.map((v, idx) => idx === i ? x : v) |
Use functions for values derived from signals:
setup: ({ signal }) => {
const items = signal([
{ name: "Widget", price: 10, qty: 2 },
{ name: "Gadget", price: 25, qty: 1 }
]);
const taxRate = signal(0.08);
// Computed as functions - recalculated on each render
const getSubtotal = () =>
items.value.reduce((sum, item) => sum + item.price * item.qty, 0);
const getTax = () => getSubtotal() * taxRate.value;
const getTotal = () => getSubtotal() + getTax();
return { items, taxRate, getSubtotal, getTax, getTotal };
}
For frequent updates (like search input), debounce to avoid excessive operations:
setup: ({ signal }) => {
const searchQuery = signal("");
const results = signal([]);
let debounceTimer = null;
function handleSearch(query) {
searchQuery.value = query;
clearTimeout(debounceTimer);
debounceTimer = setTimeout(async () => {
if (query.length >= 2) {
const response = await fetch(`/api/search?q=${query}`);
results.value = await response.json();
}
}, 300); // 300ms debounce
}
return {
searchQuery,
results,
handleSearch,
onUnmount: () => clearTimeout(debounceTimer)
};
}
| Task | Recommended Hook |
|---|---|
| Fetch initial data | onMount |
| Validate 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 |
| Sync state with external system | onUpdate |
// DON'T: Heavy synchronous work in onBeforeMount
return {
onBeforeMount: () => {
const result = heavyComputation(millionItems); // Blocks rendering!
}
};
// DON'T: Forget cleanup - causes memory leaks!
return {
onMount: () => {
window.addEventListener("scroll", handleScroll);
// Missing onUnmount cleanup!
}
};
// DON'T: Set state in onUpdate (infinite loop)
return {
onUpdate: () => {
count.value++; // Triggers another update - infinite loop!
}
};
// DO: Always clean up what you set up
let handler = null;
return {
onMount: () => {
handler = () => {};
window.addEventListener("scroll", handler);
},
onUnmount: () => {
window.removeEventListener("scroll", handler);
}
};
Quick Rule:
${}needsctx.—@eventsand:propsdon’t.
template: (ctx) => `
<p>${ctx.count.value}</p> <!-- JavaScript: use ctx -->
<button @click="increment">+</button> <!-- Event: no ctx -->
<child :data="items.value"></child> <!-- Prop: no ctx -->
`
// Ternary for simple conditions
template: (ctx) => `
${ctx.isLoading.value
? `<div class="spinner">Loading...</div>`
: `<div class="content">${ctx.data.value}</div>`
}
`
// Logical AND for show/hide
template: (ctx) => `
${ctx.showError.value && `<div class="error">${ctx.error.value}</div>`}
`
// Multiple conditions
template: (ctx) => {
if (ctx.loading.value) return `<div>Loading...</div>`;
if (ctx.error.value) return `<div>Error: ${ctx.error.value}</div>`;
return `<div>${ctx.data.value}</div>`;
}
Always use key attributes for optimal diffing:
template: (ctx) => `
<ul>
${ctx.items.value.map(item => `
<li key="${item.id}">${item.name}</li>
`).join('')}
</ul>
`
| Selector Type | Example | Use Case |
|---|---|---|
| Class | ".item" |
Multiple elements, list items |
| ID | "#sidebar" |
Single unique element |
| Data attribute | "[data-component]" |
Explicit component markers |
| Depth | Recommendation |
|---|---|
| 1-2 levels | Ideal, easy to understand |
| 3 levels | Acceptable, consider flattening |
| 4+ levels | Too deep, refactor |
| Scenario | Solution | Why |
|---|---|---|
| Pass value to child | Props | Direct value passing |
| Child notifies parent | Emitter | Events flow up |
| Siblings communicate | Emitter | Decoupled messaging |
| Many components need same data | Store | Central state |
// DON'T: Use JSON.stringify for props (not needed!)
:data='${JSON.stringify(object)}' // Old approach
:data="object" // Just pass directly
// DON'T: Use Store for parent-child only communication
store.dispatch("setParentData", data); // Overkill, use props
// DON'T: Mutate store state directly
store.state.user.value = newUser; // Use actions instead
store.dispatch("setUser", newUser); // Correct
UserProfile, not user-profile).| Use Case | Description |
|---|---|
| Performance-Critical Applications | From simple counters to data-intensive dashboards with 10K+ rows (via virtual scrolling) |
| Bundle-Sensitive Projects | At ~6KB with zero dependencies, ideal for embedded widgets, micro-frontends |
| Rapid Prototyping | Quick experimentation without build tooling overhead |
| Progressive Enhancement | Add interactivity to server-rendered pages |
| Customizable Solutions | Unopinionated architecture with plugin support |
Note: For enterprise applications requiring extensive tooling ecosystems, consider React or Vue. Eleva’s performance scales well, but its ecosystem is still growing.
For lists with 1000+ items, use a single template with keyed rows:
template: (ctx) => `
<table>
<tbody>
${ctx.rows.value.map(row => `
<tr key="${row.id}">
<td>${row.id}</td>
<td>${row.label}</td>
</tr>
`).join("")}
</tbody>
</table>
`
For 10K+ rows, use virtual scrolling - renders only visible rows.
Eleva automatically batches multiple signal changes:
// All 3 changes result in just 1 render
x.value = 10;
y.value = 20;
z.value = 30;
| Category | Key Points |
|---|---|
| Setup | Organize by: props → state → computed → actions → hooks |
| Signals | Use .value; replace don’t mutate; use functions for computed |
| Lifecycle | Always clean up in onUnmount; don’t update state in onUpdate |
| Templates | ${} uses ctx.; @events and :props don’t |
| Children | Keep nesting shallow; use specific selectors |
| Communication | Props (down), Emitter (up), Store (global) |
| ← Plugin System | Back to Main Docs | FAQ → |