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.

  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 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.

  1. VNode Resolution - Templates are converted to VNode objects
  2. Element Creation - Real DOM elements are created for each VNode
  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