Granular Templates
An approach that binds reactive values directly to DOM elements, enabling surgical updates when state changes.
Templating in JSX is familiar, with a few small differences.
Reactive Bindings
Behind the scenes, function references are used to create reactive bindings between signals and DOM elements.
JSX abstracts the creation of function references, allowing you to optionally omit arrow functions.
Valid Binding Syntax
✅ Arrow event: onClick={() => console.log(1)}
✅ Arrowless event: onClick={handleClick}
✅ Reference binding: <h1>{count}</h1>
✅ Value binding: <h1>{count()}</h1>
✅ Derived function: <h2>{() => count() * 2}</h2>
✅ Derived expression: <h2>{count() * 2}</h2>
Beware of template literals. You must call the signal.
✅ Reactive literal: <h1>{`Count: ${count()}`}</h1>
⛔ Static literal: <h1>{`Count: ${count}`}</h1>
Derived Functions
Create derived functions inside components or use computed to memoize the return value.
const TodoStats = () => {
const todos = signal([
{ id: 1, text: 'Learn HellaJS', done: false },
{ id: 2, text: 'Build an app', done: true }
]);
// Memoized function
const completedTodos = computed(() => todos().filter(t => t.done));
// Derived function
const completedTotal = () => completedTodos().length;
const remainingTotal = () => todos().length - completedTotal();
return (
<div>
<p>Completed: {completedTotal}</p>
<p>Remaining: {remainingTotal}</p>
</div>
);
};
When to use each approach:
- Use derived functions for simple transformations, filtering, mapping, etc.
- Use computed for values that will benefit from memoization
Inline Reactivity
When any function call is present in an expression it automatically becomes reactive.
const Counter = () => {
const count = signal(0);
return (
<div>
<h1>Count: {count}</h1>
<p>Double {count() * 2}</p>
<p>{count() % 2 === 0 ? "Even" : "Odd"}</p>
<button onClick={() => console.log(count())}>
Log Count
</button>
</div>
);
};
Components
Simple functions that return JSX elements (and transform to HellaNodes). Supports reactive data flow through signal props.
Component Composition
Compose naturally through props and children. Since components are just functions, composition is straightforward.
const Card = ({ title, variant = 'default' }, children) => (
<div class={`card card-${variant}`}>
<div class="card-header"><h2>{title}</h2></div>
<div class="card-content">{children}</div>
</div>
);
const Button = ({ variant = 'primary', onClick, children }) => (
<button class={`btn btn-${variant}`} onClick={() => onClick()}>
{children}
</button>
);
// Components compose naturally
const MyPage = () => (
<Card title="Welcome" variant="highlighted">
<p>Welcome to our application!</p>
<Button onClick={() => alert('Hello!')}>Get Started</Button>
</Card>
);
Reactive Data Flow
Pass reactive signals between components, they maintain their reactivity across component boundaries.
const TodoCard = ({ todo }) => (
<div class="todo-card">
<h3>{todo().text}</h3>
<p>Priority: {todo().priority}</p>
<span class={`status ${todo().done ? 'completed' : 'pending'}`}>
{todo().done ? 'Completed' : 'Pending'}
</span>
</div>
);
const TodoApp = () => {
const selectedTodo = signal({
id: 1,
text: 'Learn HellaJS',
priority: 'high',
done: false
});
return (
<div class="todo-app">
<TodoCard todo={selectedTodo} />
<button onClick={() => selectedTodo({
id: 2,
text: 'Build awesome app',
priority: 'medium',
done: true
})}>
Switch Todo
</button>
</div>
);
};
Control Flow
Conditional Rendering
Familiar JSX conditional rendering.
const Counter = () => {
const count = signal(0);
return (
<div>
<h1>Count: {count}</h1>
<h2>Doubled: {count() * 2}</h2>
<p>Count is: {count() % 2 === 0 ? 'even' : 'odd'}</p>
{count() > 5 && <p>Great job! Count is greater than 5!</p>}
<button onClick={() => count(count() + 1)}>
Increment
</button>
</div>
);
};
Lists and Iteration
Create static lists using regular array methods for one-time rendering, or reactive lists using the optimized forEach function.
✅ Reactive: <ul>{forEach(todos, todo => <li>{todo.text}</li>)}</ul>
⛔ Static: <ul>{todos().map(todo => <li>{todo.text}</li>)}</ul>
const TodoList = () => {
const todos = signal([
{ id: 1, text: 'Learn HellaJS', done: false },
{ id: 2, text: 'Build an app', done: false }
]);
const addTodo = (text) => {
todos([...todos(), {
id: Date.now(),
text,
done: false
}]);
};
return (
<div>
<ul>
{forEach(todos, todo => (
<li key={todo.id} class={todo.done ? 'done' : ''}>
<input
type="checkbox"
checked={todo.done}
onChange={e => {
const updated = todos().map(t =>
t.id === todo.id ? { ...t, done: e.target.checked } : t
);
todos(updated);
}}
/>
{todo.text}
</li>
))}
</ul>
</div>
);
};
Fragments and Children
Group elements without a wrapper element. Great for when a component’s structure doesn’t align with semantic HTML.
const Modal = ({ isOpen, onClose, children }) => {
return (
<>
{isOpen() && (
<>
<div class="overlay" onClick={onClose} />
<div class="modal">
<button class="close" onClick={onClose}>×</button>
{children}
</div>
</>
)}
</>
);
};
const App = () => {
const showModal = signal(false);
return (
<div>
<button onClick={() => showModal(true)}>Open Modal</button>
<Modal isOpen={showModal} onClose={() => showModal(false)}>
<h2>Modal Content</h2>
<p>This is inside the modal!</p>
</Modal>
</div>
);
};
Element Lifecycle
Mount Lifecycle
onBeforeMount - Called synchronously before the element is created. Use for initialization logic that must run before DOM creation.
onMount - Called asynchronously (via requestAnimationFrame) after the element is mounted to the DOM. Safe for DOM measurements and third-party library initialization.
const Component = () => {
return (
<div
onBeforeMount={() => {
console.log('Preparing to mount');
}}
onMount={() => {
console.log('Element is now in DOM');
// Safe to measure dimensions, initialize libraries, etc.
}}
>
Content
</div>
);
};
Update Lifecycle
onBeforeUpdate - Called before reactive properties, attributes, or content update on the element.
onUpdate - Called after reactive properties, attributes, or content update on the element.
const Counter = () => {
const count = signal(0);
return (
<div
data-count={count}
onBeforeUpdate={() => {
console.log('About to update');
}}
onUpdate={() => {
console.log('Element updated');
}}
>
<h1>{count}</h1>
<button onClick={() => count(count() + 1)}>Increment</button>
</div>
);
};
Destroy Lifecycle
onBeforeDestroy - Called before the element is removed from the DOM. Use to cancel pending operations or save state.
onDestroy - Called after the element is removed from the DOM and all effects are cleaned up. Use for cleanup of external resources.
const WebSocketComponent = () => {
const ws = new WebSocket('ws://localhost:8080');
return (
<div
onBeforeDestroy={() => {
console.log('About to cleanup');
}}
onDestroy={() => {
ws.close();
console.log('Connection closed');
}}
>
Content
</div>
);
};
effects
An array of reactive effect functions that automatically cleanup when the element is removed from the DOM.
const Timer = () => {
const count = signal(0);
const status = signal('running');
const logCount = () => console.log(`Count is now: ${count()}`);
const updateStatus = () => status() === 'running' && console.log('Timer is running');
return (
<div effects={[logCount, updateStatus]}>
<h1>{count}</h1>
<p>Status: {status}</p>
<button onClick={() => count(count() + 1)}>
Increment
</button>
</div>
);
};
Key Benefits:
- Automatic cleanup - Effects are disposed when the element is removed
- No manual management - No need to track effect disposers
- Reactive by default - Effects automatically track signal dependencies
Complete Lifecycle Example
const FullLifecycle = () => {
const count = signal(0);
// Effect functions for automatic cleanup
const logCount = () => console.log(`Count: ${count()}`);
const trackChanges = () => count() > 5 && console.log('High count detected');
// External resource requiring manual cleanup
const ws = new WebSocket('ws://localhost:8080');
return (
<div
effects={[logCount, trackChanges]}
onBeforeMount={() => console.log('1. Before mount')}
onMount={() => console.log('2. Mounted')}
onBeforeUpdate={() => console.log('3. Before update (only on updates)')}
onUpdate={() => console.log('4. After update (only on updates)')}
onBeforeDestroy={() => console.log('5. Before destroy')}
onDestroy={() => {
ws.close();
console.log('6. Destroyed');
}}
>
<h1>{count}</h1>
<button onClick={() => count(count() + 1)}>
Increment
</button>
</div>
);
};
Lifecycle Execution Order
onBeforeMount- before element creationonMount- after element is in DOM (async via requestAnimationFrame)onBeforeUpdate+onUpdate- on reactive updates (not during initial render)onBeforeDestroy- before removalonDestroy- after removal and effect cleanup
When to Use Each Hook
effects- For reactive logic that should cleanup automatically (logging, analytics, derived state)onBeforeMount- For initialization logic before DOM creationonMount- For DOM measurements, third-party library initializationonBeforeUpdate/onUpdate- For responding to reactive changesonBeforeDestroy- For canceling pending operations, saving stateonDestroy- For cleanup of external resources (timers, WebSockets, subscriptions)
Reactive Elements
The element function provides DOM manipulation with reactive support, allowing you to imperatively interact with existing DOM elements. Use forEach to work with multiple elements.
Basic Usage
Select elements and chain methods for text content, attributes, and event handling.
import { signal } from '@hellajs/core';
import { element, elements } from '@hellajs/dom';
const count = signal(0);
const isDisabled = signal(false);
// Single element reactive bindings
element('.counter').text(count);
element('.input').attr({ disabled: isDisabled });
element('.button').on('click', () => count(count() + 1));
// Method chaining
element('.node')
.text('Hello World')
.attr({
class: 'active',
'data-value': someSignal
})
.on('click', handleClick);
// Multiple elements using elements()
const status = signal('ready');
const statusElements = elements('.status-indicator');
statusElements.forEach(elem => {
elem.text(() => `Status: ${status()}`)
.attr({ class: () => `indicator ${status()}` });
});
Multiple Elements
When selecting multiple elements with elements(), use forEach to apply operations to each element individually.
const items = elements('.item');
// forEach for individual element control
items.forEach((item, index) => {
item.text(`Item ${index + 1}`)
.attr({ 'data-index': index.toString() })
.on('click', () => console.log(`Clicked item ${index}`));
});
// Direct DOM access for performance
items.forEach((item, index) => {
if (item.node) {
item.node.textContent = `Fast item ${index}`;
item.node.setAttribute('data-fast', 'true');
}
});
Optimization Strategies
Derived vs Computed
// ✅ Simple transformations: use derived functions
const doubledCount = () => count() * 2;
// ✅ Expensive operations: use computed for memoization
const expensiveFilter = computed(() =>
largeArray().filter(item => complexCondition(item))
);
Resource Cleanup
Use effects for reactive logic that needs automatic cleanup, and onDestroy for external resources.
const Timer = () => {
const count = signal(0);
const isHighCount = signal(false);
// Reactive effects - automatically cleaned up
const trackCount = () => console.log(`Count changed: ${count()}`);
const updateHighCount = () => isHighCount(count() > 10);
// External resource - requires manual cleanup
const interval = setInterval(() => count(count() + 1), 1000);
const cleanupTimer = () => clearInterval(interval);
return (
<div
effects={[trackCount, updateHighCount]}
onDestroy={cleanupTimer}
>
<h1>{count}</h1>
<p>{isHighCount() ? 'High count!' : 'Low count'}</p>
</div>
);
};
Internal Mechanics
A sophisticated yet lightweight approach to binding reactive data directly to DOM elements.
Architecture
Every template element is represented as a HellaNode (Virtual Node) object containing three core properties.
- tag - The HTML element name or fragment identifier
- props - Element attributes, properties, and lifecycle hooks
- children - Array of child HellaNodes or primitive values
HellaNode {
tag: "div",
props: { class: () => count() > 5 ? "active" : "" },
children: [count, "items"]
}
Unlike virtual DOM systems that diff entire trees, HellaNodes serve as templates, establishing reactive connections during render.
Manual HellaNodes
For scenarios where JSX isn’t available or when programmatically generating templates, you can construct HellaNodes directly.
✅ Reactive: children: count
✅ Reactive: children: () => count() * 2
✅ Reactive: children: () => count() % 2 === 0 ? "Even" : "Odd"
✅ Good: onClick: () => console.log(1)
⛔ Static: children: () => count
⛔ Static: children: count() % 2 === 0 ? "Even" : "Odd"
⛔ Bad: onClick: console.log(1)
const Counter = () => {
const count = signal(0);
return {
tag: 'div',
children: [
{
tag: 'h1',
children: count // Function reference for reactivity
},
{
tag: 'button',
props: { onClick: () => count(count() + 1) },
children: 'Increment'
}
]
};
};
Binding Process
When mounting a template, the system distinguishes between static values and function references to establish reactive bindings.
- Function Detection - The mount system identifies function references in props and children
- Effect Creation - Each reactive binding creates a dedicated effect using the core reactivity system
- Direct DOM Binding - Effects update specific DOM properties without intermediate virtual representations
- Lifecycle Integration - Element lifecycle hooks are wired directly to DOM mutation events
Signal Change → Effect Execution → Direct DOM Update
↑ ↓
└── No Virtual DOM Diffing ←──────────┘
Dynamic Content
Dynamic children (conditional renders and function expressions) use a sophisticated comment-marker system for efficient DOM updates.
- Boundary Markers - HTML comments mark the start and end of dynamic content regions
- Content Replacement - When expressions change, only content between markers is replaced
- Fragment Support - Multiple elements can be inserted or removed as cohesive units
- Cleanup Coordination - Removed elements trigger automatic effect cleanup and lifecycle hooks
Event Delegation
Global event delegation system for maximum efficiency. A single listener per event type is attached to document.body.
When you write <button onClick={handleClick}>, HellaJS:
- Creates a global click listener on
document.body(if none exists) - Stores your
handleClickfunction directly on the button element - When clicked, the global listener routes the event to your handler via property lookup
Benefits: 1,000 buttons = 1 click listener (not 1,000). Events naturally bubble up the DOM tree. Direct property storage eliminates Map lookup overhead.
List Optimization
The forEach helper implements advanced list diffing using the Longest Increasing Subsequence (LIS) algorithm.
- Key-Based Tracking - Items are tracked by unique keys for efficient reordering
- Minimal DOM Operations - Only elements that actually need moving are repositioned
- Memory Efficiency - Node references are cached and reused across updates
- Performance Scaling - Algorithms ensure performance remains optimal even with large lists
Memory Management
Element lifecycle is managed through direct property storage combined with automatic observation.
- MutationObserver Integration - A global observer watches for DOM removals and processes each removed node
- Property-Based Storage - Effects and handlers are stored directly on elements as properties (
__hella_effects,__hella_handlers) - Automatic Cleanup - When elements are removed, the observer recursively cleans all descendants
- Memory Efficiency - No separate registry Map overhead, reducing memory footprint by ~50%
- Memory Safety - Circular references are prevented through coordinated cleanup sequencing
Rendering
The complete rendering process follows this optimized pipeline.
- HellaNode Resolution - Templates are converted to HellaNode objects
- Element Creation - Real DOM elements are created for each HellaNode
- Property Binding - Static properties are set directly, reactive properties create effects
- Child Mounting - Child elements are recursively processed and appended
- Effect Activation - All reactive bindings begin tracking their dependencies
- Lifecycle Registration - Elements are registered for automatic cleanup on removal