Granular Templating
HellaJS uses an approach to templating that eliminates virtual DOM diffing and re-render cycles. Reactive values are bound directly to DOM elements and attributes, enabling surgical updates only where data actually changes.
Reactive Bindings
Behind the scenes, HellaJS uses function references to create reactive bindings between data and DOM elements.
JSX abstracts the reference process when a function is called inside the expression, allowing you to optionally omit arrow functions.
A few patterns are valid:
✅ Arrow event: onClick={() => () => console.log(1)}
✅ Arrowless event: onClick={console.log(1)}
✅ 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>
Resolve Props
Use the resolve:
prefix to bypass HellaJS’s automatic reactivity transformation and pass static values from functions.
const Counter = () => {
const staticFunction = () => "btn";
return (
<div>
{/*
Automatic reactive transformation
class: () => staticFunction()
*/}
<button class={staticFunction()}>Auto Transform</button>
{/*
Bypass reactive transformation
class: staticFunction()
*/}
<button resolve:class={staticFunction()}>Resolved Value</button>
</div>
);
};
For TypeScript/TSX files, you can use the resolve()
syntax sugar instead:
import { resolve } from '@hellajs/dom';
const Counter = () => {
const staticFunction = () => "btn";
return (
<div>
<button class={staticFunction()}>Auto Transform</button>
<button class={resolve(staticFunction())}>Resolved Value</button>
</div>
);
};
Use resolve props when:
- Integrating with third-party libraries that expect specific function signatures
- Working with non-reactive callback patterns
Signal References
Signals are automatically bound to the element thay are used in.
const Counter = () => {
const count = signal(0);
return (
<div>
<h1>{count}</h1>
<button onClick={() => count(count() + 1)}>
Increment
</button>
</div>
);
};
Derived Functions
Inside components, you can create derived functions with or without using computed()
.
const TodoStats = () => {
const todos = signal([
{ id: 1, text: 'Learn HellaJS', done: false },
{ id: 2, text: 'Build an app', done: true }
]);
// Simple derivation - no memoization needed
const activeTodos = () => todos().filter(t => !t.done);
const completedCount = () => todos().filter(t => t.done).length;
// Use computed when you need memoization for expensive operations
const expensiveCalculation = computed(() => {
console.log('Expensive calculation running'); // Only logs when todos change
return todos().reduce((sum, todo) => {
// Simulate expensive operation
return sum + todo.text.length * Math.random();
}, 0);
});
return (
<div>
<p>Active: {activeTodos().length}</p>
<p>Completed: {completedCount}</p>
<p>Score: {expensiveCalculation}</p>
</div>
);
};
When to use each approach:
- Derived functions (
const fn = () => ...
) - Simple transformations, filtering, mapping - Computed values (computed(() => …)) - Expensive calculations that benefit from caching
Both automatically track dependencies and update reactively, but computed()
caches results until dependencies change.
Inline Reactivity
When any function call is present in an expression it automatically becomes reactive. This also works for event handlers.
const Counter = () => {
const count = signal(0);
setInterval(() => {
count(count() + 1);
}, 1000);
return (
<div>
<h1>Count: {count}</h1>
<h2>Doubled: {count() * 2}</h2>
<button onClick={() => console.log(count())}>
Log Count
</button>
</div>
);
};
Element Lifecycle
onUpdate
Executes whenever a reactive binding updates an element’s properties, attributes, or content.
onDestroy
Executes when an element is removed from the DOM, providing an opportunity to perform cleanup tasks.
const Counter = () => {
const count = signal(0);
return (
<div onDestroy={console.log('Counter component destroyed')}>
<h1 onUpdate={console.log('Count updated to:', count())}>{count}</h1>
<button onClick={() => count(count() + 1)}>
Increment
</button>
</div>
);
};
Components
Nothing more than functions that return JSX elements (and ultimately VNodes). They support reactive data flow through signal props.
Static Components
Static components render fixed content that doesn’t change over time.
const Header = ({ title }) => <h1>{title}</h1>;
const Footer = ({ year = 2024 }) => <footer>© {year} My App</footer>;
Interactive Components
Interactive components create reactive bindings during their execution, automatically handling updates.
const Counter = ({ initialValue = 0 }) => {
const count = signal(initialValue);
return (
<div>
<h1>Count: {count}</h1>
<button onClick={() => count(count() + 1)}>
Increment
</button>
<button onClick={() => count(count() - 1)}>
Decrement
</button>
</div>
);
};
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>
);
};
Optimization Strategies
Untracked Reads
Use untracked to read signals without creating dependencies.
const Counter = () => {
const count = signal(0);
const debugMode = signal(true);
const doubled = () => {
const value = count();
// Read debugMode without making it a dependency
if (untracked(() => debugMode())) {
console.log('Debug: count is', value);
}
return value * 2;
};
return (
<div>
<h1>Count: {count}</h1>
<h2>Doubled: {doubled}</h2>
<button onClick={() => count(count() + 1)}>Increment</button>
<button onClick={() => debugMode(!debugMode())}>
Toggle Debug: {debugMode() ? 'On' : 'Off'}
</button>
</div>
);
};
Cleanup With onDestroy
Use onDestroy
on the root element to cleanup when the component is removed from the DOM.
const Counter = () => {
const count = signal(0);
const interval = setInterval(() => {
count(count() + 1);
}, 1000);
return (
<div onDestroy={clearInterval(interval)}>
<h1>{count}</h1>
</div>
);
};
Internal Mechanics
A sophisticated yet lightweight approach to binding reactive data directly to DOM elements.
VNode Architecture
Every template element is represented as a VNode (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 VNodes or primitive values
VNode {
tag: "div",
props: { class: () => count() > 5 ? "active" : "" },
children: [count, "items"]
}
Unlike virtual DOM systems that diff entire trees, VNodes serve as templates, establishing reactive connections during render.
VNode Templating
For scenarios where JSX isn’t available or when programmatically generating templates, you can construct VNodes 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
⛔ 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'
}
]
};
};
Reactive 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 Handling
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
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
Element Lifecycle Management
Element lifecycle is managed through a combination of direct hooks and automatic observation.
- MutationObserver Integration - A global observer watches for DOM removals
- Effect Registration - Each element maintains a set of cleanup functions for its reactive bindings
- Automatic Cleanup - When elements are removed, all associated effects are disposed automatically
- Memory Safety - Circular references are prevented through proper cleanup sequencing
Rendering Pipeline
The complete rendering process follows this optimized pipeline.
- VNode Resolution - Templates are converted to VNode objects
- Element Creation - Real DOM elements are created for each VNode
- 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