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.

  1. Function Detection - The mount system identifies function references in props and children
  2. Effect Creation - Each reactive binding creates a dedicated effect using the core reactivity system
  3. Direct DOM Binding - Effects update specific DOM properties without intermediate virtual representations
  4. 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:

  1. Creates a global click listener on document.body (if none exists)
  2. Stores your handleClick function in the button’s registry entry
  3. 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.

  1. HellaNode Resolution - Templates are converted to HellaNode objects
  2. Element Creation - Real DOM elements are created for each HellaNode
  3. Property Binding - Static properties are set directly, reactive properties create effects
  4. Child Mounting - Child elements are recursively processed and appended
  5. Effect Activation - All reactive bindings begin tracking their dependencies
  6. Lifecycle Registration - Elements are registered for automatic cleanup on removal