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
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
onUpdate
Executes whenever a reactive binding updates an element’s properties, attributes, or content.
onDestroy
Executes when an element is removed from the DOM. Use this for external resources that need manual cleanup.
const WebSocketComponent = () => {
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');
const cleanup = () => ws.close();
return (
<div
effects={[logCount, trackChanges]}
onDestroy={cleanup}
onUpdate={() => console.log('Element updated')}
>
<h1>{count}</h1>
<button onClick={() => count(count() + 1)}>
Increment
</button>
</div>
);
};
When to use each:
effects
- For reactive logic that should cleanup automatically (logging, analytics, derived state)onDestroy
- For external resources that need manual cleanup (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
handleClick
function in the button’s registry entry - When clicked, the global listener routes the event to your handler
Benefits: 1,000 buttons = 1 click listener (not 1,000). Events naturally bubble up the DOM tree.
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 the nodeRegistry system combining direct hooks and automatic observation.
- MutationObserver Integration - A global observer watches for DOM removals
- Registry-Based Tracking - Each eleme nt maintains a registry entry with cleanup functions for reactive bindings
- Automatic Cleanup - When elements are removed, all associated effects are disposed automatically
- 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