Migrating from Svelte

HellaJS shares many philosophical similarities with Svelte, including fine-grained reactivity and compile-time optimizations. However, HellaJS takes a more functional approach while maintaining the reactive principles that make Svelte powerful.

This guide will help you transition from Svelte to HellaJS by showing equivalent patterns and highlighting key differences.

Key Differences

  • Granular DOM Updates: Both frameworks update only changed parts, but HellaJS uses direct signal bindings vs Svelte’s reactive statements
  • Signals vs Reactive Variables: HellaJS signals use function call syntax, while Svelte uses assignment-based reactivity
  • No Compilation Required: HellaJS works with standard JavaScript, while Svelte requires compilation of .svelte files
  • Function-Based Components: HellaJS uses pure functions returning JSX, while Svelte uses component files with script/template/style blocks

Component Structure

Svelte Single File Components

Svelte components combine script, template, and styles in a single .svelte file with clear separation.

<!-- Counter.svelte -->
<script>
  let count = 0;
  
  function increment() {
    count += 1;
  }
</script>

<div>
  <h1>{count}</h1>
  <button on:click={increment}>+</button>
</div>

<style>
  div {
    padding: 2rem;
    text-align: center;
  }
</style>

HellaJS Function Components

HellaJS components are JavaScript functions that return JSX, with styling handled separately or via CSS-in-JS.

const Counter = () => {
  const count = signal(0);
  
  const increment = () => count(count() + 1);
  
  return (
    <div class="counter">
      <h1>{count}</h1>
      <button onClick={increment}>+</button>
    </div>
  );
};

Key Differences

  • File Structure: Svelte uses single-file components, HellaJS uses standard JavaScript files
  • Template Syntax: Both use similar template syntax, but HellaJS uses JSX
  • Script Organization: Svelte script blocks vs JavaScript function scope
  • Style Handling: Svelte scoped styles vs HellaJS CSS-in-JS or external stylesheets

Reactive State

Svelte Reactive Variables

Svelte uses assignment-based reactivity where variable assignments automatically trigger updates.

<script>
  let name = 'World';
  let count = 0;
  
  // Reactive statements automatically re-run when dependencies change
  $: greeting = `Hello, ${name}!`;
  $: doubled = count * 2;
</script>

<div>
  <h1>{greeting}</h1>
  <p>Count: {count}, Doubled: {doubled}</p>
  <button on:click={() => count += 1}>+</button>
  <input bind:value={name} />
</div>

HellaJS Signals

HellaJS uses signals with function call syntax for reactive state management.

const App = () => {
  const name = signal('World');
  const count = signal(0);
  
  // Derived values using computed or simple functions
  const greeting = () => `Hello, ${name()}!`;
  const doubled = () => count() * 2;
  
  return (
    <div>
      <h1>{greeting}</h1>
      <p>Count: {count}, Doubled: {doubled}</p>
      <button onClick={() => count(count() + 1)}>+</button>
      <input value={name} onChange={e => name(e.target.value)} />
    </div>
  );
};

Key Differences

  • Syntax Pattern: Svelte uses $: reactive = expression, HellaJS uses const reactive = () => expression
  • Value Access: Svelte accesses values directly, HellaJS requires function calls signal()
  • Updates: Svelte uses assignment (count += 1), HellaJS uses setters (count(count() + 1))
  • Reactivity Detection: Svelte analyzes assignments, HellaJS tracks function calls automatically

Derived State

Svelte Reactive Statements

Svelte reactive statements automatically recompute when their dependencies change.

<script>
  let todos = [
    { id: 1, text: 'Learn Svelte', done: false },
    { id: 2, text: 'Build app', done: true }
  ];
  let filter = 'all';
  
  // Reactive statement - automatically updates when todos or filter change
  $: filteredTodos = todos.filter(todo => {
    if (filter === 'completed') return todo.done;
    if (filter === 'active') return !todo.done;
    return true;
  });
  
  $: todoStats = {
    total: todos.length,
    completed: todos.filter(t => t.done).length,
    active: todos.filter(t => !t.done).length
  };
</script>

<div>
  <p>{filteredTodos.length} todos</p>
  <p>Stats: {todoStats.active} active, {todoStats.completed} completed</p>
</div>

HellaJS Computed Values

Computed values automatically cache and recompute when dependencies change.

const TodoApp = () => {
  const todos = signal([
    { id: 1, text: 'Learn HellaJS', done: false },
    { id: 2, text: 'Build app', done: true }
  ]);
  const filter = signal('all');
  
  const filteredTodos = computed(() => {
    return todos().filter(todo => {
      if (filter() === 'completed') return todo.done;
      if (filter() === 'active') return !todo.done;
      return true;
    });
  });
  
  const todoStats = computed(() => ({
    total: todos().length,
    completed: todos().filter(t => t.done).length,
    active: todos().filter(t => !t.done).length
  }));
  
  return (
    <div>
      <p>{filteredTodos().length} todos</p>
      <p>Stats: {todoStats().active} active, {todoStats().completed} completed</p>
    </div>
  );
};

Key Differences

  • Declaration: Svelte uses $: name = expression, HellaJS uses computed(() => expression)
  • Caching: Both cache results until dependencies change
  • Access Pattern: Svelte accesses directly, HellaJS requires function calls
  • Dependency Tracking: Both automatically track dependencies, but HellaJS is more explicit about function calls

Component Props

Svelte Component Props

Svelte components export variables to create props, with optional default values and reactivity.

<!-- UserCard.svelte -->
<script>
  export let user;
  export let showEmail = true;
  export let theme = 'light';
  
  // Reactive statement using props
  $: displayName = user.firstName + ' ' + user.lastName;
</script>

<div class="user-card {theme}">
  <h2>{displayName}</h2>
  {#if showEmail}
    <p>{user.email}</p>
  {/if}
</div>

<!-- Usage -->
<UserCard {user} {showEmail} theme="dark" />

HellaJS Component Props

HellaJS components receive props as function parameters, supporting destructuring and defaults.

const UserCard = ({ user, showEmail = true, theme = 'light' }) => {
  const displayName = () => user().firstName + ' ' + user().lastName;
  
  return (
    <div class={`user-card ${theme}`}>
      <h2>{displayName}</h2>
      {showEmail && <p>{user().email}</p>}
    </div>
  );
};

// Usage
<UserCard user={user} showEmail={showEmail} theme="dark" />

Key Differences

  • Prop Declaration: Svelte uses export let, HellaJS uses function parameters
  • Reactivity: Both maintain reactivity when passed reactive values
  • Default Values: Svelte assigns in script block, HellaJS uses parameter defaults
  • Prop Passing: Both support shorthand syntax for matching variable names

Global State

Svelte Stores

Svelte provides built-in stores for global state management.

// stores.js
import { writable, derived } from 'svelte/store';

export const theme = writable('light');
export const user = writable(null);

export const isLoggedIn = derived(user, $user => !!$user);
<!-- Component.svelte -->
<script>
  import { theme, user, isLoggedIn } from './stores.js';
  
  function toggleTheme() {
    theme.update(t => t === 'light' ? 'dark' : 'light');
  }
</script>

<div>
  <p>Theme: {$theme}</p>
  {#if $isLoggedIn}
    <p>Welcome, {$user.name}!</p>
  {/if}
  <button on:click={toggleTheme}>Toggle Theme</button>
</div>

HellaJS Global Signals

Global signals can be imported and used directly without store subscriptions.

// stores.js
export const theme = signal('light');
export const user = signal(null);

export const isLoggedIn = computed(() => !!user());
// Component.jsx
import { theme, user, isLoggedIn } from './stores.js';

const Component = () => {
  const toggleTheme = () => {
    theme(theme() === 'light' ? 'dark' : 'light');
  };
  
  return (
    <div>
      <p>Theme: {theme}</p>
      {isLoggedIn() && <p>Welcome, {user().name}!</p>}
      <button onClick={toggleTheme}>Toggle Theme</button>
    </div>
  );
};

Key Differences

  • Store System: Svelte has built-in stores, HellaJS uses signals directly
  • Subscription: Svelte uses $ prefix for auto-subscription, HellaJS calls signals directly
  • Updates: Svelte uses store.update(), HellaJS uses direct setter calls
  • Derived Values: Both support computed/derived values with automatic dependency tracking

Side Effects

Svelte Reactive Statements and Lifecycle

Svelte uses reactive statements for side effects and lifecycle functions for component-level effects.

<script>
  import { onMount, onDestroy } from 'svelte';
  
  let userId = 1;
  let user = null;
  let loading = false;
  
  // Reactive statement runs when userId changes
  $: if (userId) {
    loadUser(userId);
  }
  
  async function loadUser(id) {
    loading = true;
    try {
      const response = await fetch(`/api/users/${id}`);
      user = await response.json();
    } catch (error) {
      console.error('Failed to load user:', error);
    } finally {
      loading = false;
    }
  }
  
  let interval;
  onMount(() => {
    interval = setInterval(() => {
      console.log('Timer tick');
    }, 1000);
  });
  
  onDestroy(() => {
    if (interval) clearInterval(interval);
  });
</script>

<div>
  {#if loading}
    <p>Loading...</p>
  {:else if user}
    <p>{user.name}</p>
  {/if}
</div>

HellaJS Effects

Effects automatically track dependencies and handle async operations with built-in cleanup.

const UserProfile = () => {
  const userId = signal(1);
  const user = signal(null);
  const loading = signal(false);
  
  // Effect automatically tracks userId dependency
  effect(async () => {
    const id = userId();
    if (!id) return;
    
    loading(true);
    try {
      const response = await fetch(`/api/users/${id}`);
      const userData = await response.json();
      user(userData);
    } catch (error) {
      console.error('Failed to load user:', error);
    } finally {
      loading(false);
    }
  });
  
  // Setup timer with automatic cleanup
  const interval = setInterval(() => {
    console.log('Timer tick');
  }, 1000);
  
  return (
    <div onDestroy={() => clearInterval(interval)}>
      {loading() ? (
        <p>Loading...</p>
      ) : user() ? (
        <p>{user().name}</p>
      ) : null}
    </div>
  );
};

Key Differences

  • Effect Syntax: Svelte uses $: if (condition), HellaJS uses effect(() => {})
  • Async Support: HellaJS effects support async/await directly, Svelte requires separate async functions
  • Cleanup: Svelte uses lifecycle functions, HellaJS uses element-level onDestroy
  • Dependency Tracking: Both automatically track, but HellaJS is more explicit about what’s tracked

Lists and Iteration

Svelte Each Blocks

Svelte uses {#each} blocks with keyed iteration for efficient list rendering.

<script>
  let todos = [
    { id: 1, text: 'Learn Svelte', done: false },
    { id: 2, text: 'Build app', done: true }
  ];
  
  function toggleTodo(todo) {
    todo.done = !todo.done;
    todos = todos; // Trigger reactivity
  }
  
  function addTodo(text) {
    todos = [...todos, { id: Date.now(), text, done: false }];
  }
</script>

<ul>
  {#each todos as todo (todo.id)}
    <li class:completed={todo.done}>
      <input 
        type="checkbox" 
        bind:checked={todo.done}
        on:change={() => toggleTodo(todo)}
      />
      {todo.text}
    </li>
  {/each}
</ul>

HellaJS forEach

Optimized forEach function provides reactive list rendering with intelligent diffing.

const TodoList = () => {
  const todos = signal([
    { id: 1, text: 'Learn HellaJS', done: false },
    { id: 2, text: 'Build app', done: true }
  ]);
  
  const toggleTodo = (todo) => {
    todos(todos().map(t => 
      t.id === todo.id ? { ...t, done: !t.done } : t
    ));
  };
  
  const addTodo = (text) => {
    todos([...todos(), { id: Date.now(), text, done: false }]);
  };
  
  return (
    <ul>
      {forEach(todos, todo => (
        <li key={todo.id} class={todo.done ? 'completed' : ''}>
          <input 
            type="checkbox" 
            checked={todo.done}
            onChange={() => toggleTodo(todo)}
          />
          {todo.text}
        </li>
      ))}
    </ul>
  );
};

Key Differences

  • Syntax: Svelte uses {#each} blocks, HellaJS uses forEach() function
  • Keying: Both support keyed iteration for performance
  • Mutations: Svelte allows direct mutations with reassignment, HellaJS requires immutable updates
  • Performance: Both use advanced diffing algorithms for minimal DOM operations

Event Handling

Svelte Event Directives

Svelte uses on: directives for event handling with optional modifiers.

<script>
  let count = 0;
  let name = '';
  
  function handleClick(event) {
    count += 1;
    console.log('Clicked!', event);
  }
  
  function handleSubmit(event) {
    console.log('Submitted:', name);
  }
</script>

<div>
  <button on:click={handleClick}>
    Clicked {count} times
  </button>
  
  <button on:click|preventDefault={handleClick}>
    Click (prevent default)
  </button>
  
  <form on:submit|preventDefault={handleSubmit}>
    <input bind:value={name} />
    <button type="submit">Submit</button>
  </form>
</div>

HellaJS Event Props

HellaJS uses standard JSX event props with inline event handling.

const EventExample = () => {
  const count = signal(0);
  const name = signal('');
  
  const handleClick = (event) => {
    count(count() + 1);
    console.log('Clicked!', event);
  };
  
  const handleSubmit = (event) => {
    event.preventDefault();
    console.log('Submitted:', name());
  };
  
  return (
    <div>
      <button onClick={handleClick}>
        Clicked {count} times
      </button>
      
      <button onClick={(e) => {
        e.preventDefault();
        handleClick(e);
      }}>
        Click (prevent default)
      </button>
      
      <form onSubmit={handleSubmit}>
        <input 
          value={name} 
          onChange={e => name(e.target.value)} 
        />
        <button type="submit">Submit</button>
      </form>
    </div>
  );
};

Key Differences

  • Syntax: Svelte uses on:event, HellaJS uses onEvent props
  • Modifiers: Svelte has built-in modifiers, HellaJS handles modifiers in code
  • Event Objects: Both provide access to native event objects
  • Inline Handlers: Both support inline arrow functions

Lifecycle Hooks

Svelte Lifecycle Functions

Svelte provides component-level lifecycle functions.

<script>
  import { onMount, onDestroy, beforeUpdate, afterUpdate } from 'svelte';
  
  let count = 0;
  
  onMount(() => {
    console.log('Component mounted');
    const interval = setInterval(() => count += 1, 1000);
    
    return () => {
      console.log('Component destroyed');
      clearInterval(interval);
    };
  });
  
  beforeUpdate(() => {
    console.log('Before update, count:', count);
  });
  
  afterUpdate(() => {
    console.log('After update, count:', count);
  });
</script>

<div>Count: {count}</div>

HellaJS Element Lifecycle

HellaJS lifecycle hooks are tied directly to DOM elements rather than component instances.

const LifecycleExample = () => {
  const count = signal(0);
  
  const interval = setInterval(() => {
    count(count() + 1);
  }, 1000);
  
  return (
    <div 
      onDestroy={() => {
        console.log('Element destroyed');
        clearInterval(interval);
      }}
    >
      <p 
        onUpdate={() => console.log('Count updated to:', count())}
      >
        Count: {count}
      </p>
    </div>
  );
};

Key Differences

  • Scope: Svelte lifecycle is component-scoped, HellaJS is element-scoped
  • Mount/Destroy: Svelte uses onMount/onDestroy, HellaJS uses onDestroy element attribute
  • Updates: Svelte has beforeUpdate/afterUpdate, HellaJS has onUpdate per element
  • Granular Control: HellaJS allows different lifecycle behavior per element within a component

Animation and Transitions

Svelte Transitions

Svelte provides built-in transition directives and custom transitions.

<script>
  import { fade, slide, scale } from 'svelte/transition';
  import { flip } from 'svelte/animate';
  
  let items = [1, 2, 3, 4];
  let visible = true;
</script>

{#if visible}
  <div transition:fade>
    <h1 in:scale out:slide>Hello World!</h1>
  </div>
{/if}

{#each items as item (item)}
  <div animate:flip transition:slide>
    Item {item}
  </div>
{/each}

<button on:click={() => visible = !visible}>
  Toggle
</button>

HellaJS CSS Transitions

HellaJS relies on CSS transitions and animations with reactive styling.

const AnimationExample = () => {
  const visible = signal(true);
  const items = signal([1, 2, 3, 4]);
  
  const containerStyle = () => css({
    opacity: visible() ? 1 : 0,
    transform: visible() ? 'scale(1)' : 'scale(0.8)',
    transition: 'all 0.3s ease'
  });
  
  const itemStyle = css({
    padding: '1rem',
    margin: '0.5rem',
    background: '#f0f0f0',
    transition: 'all 0.3s ease',
    
    '&:hover': {
      transform: 'translateY(-2px)'
    }
  });
  
  return (
    <div>
      {visible() && (
        <div class={containerStyle}>
          <h1>Hello World!</h1>
          {forEach(items, item => (
            <div key={item} class={itemStyle}>
              Item {item}
            </div>
          ))}
        </div>
      )}
      
      <button onClick={() => visible(!visible())}>
        Toggle
      </button>
    </div>
  );
};

Key Differences

  • Built-in vs CSS: Svelte has built-in transitions, HellaJS uses CSS transitions/animations
  • Declarative vs Reactive: Svelte uses transition directives, HellaJS uses reactive CSS classes
  • Animation Libraries: Both can integrate with third-party animation libraries
  • Performance: Both provide smooth animations, but with different implementation approaches

Form Handling

Svelte Two-Way Binding

Svelte provides convenient two-way binding with bind: directives.

<script>
  let formData = {
    name: '',
    email: '',
    age: 0,
    subscribe: false,
    category: 'general'
  };
  
  function handleSubmit() {
    console.log('Form submitted:', formData);
  }
</script>

<form on:submit|preventDefault={handleSubmit}>
  <input bind:value={formData.name} placeholder="Name" />
  <input bind:value={formData.email} type="email" placeholder="Email" />
  <input bind:value={formData.age} type="number" placeholder="Age" />
  
  <label>
    <input bind:checked={formData.subscribe} type="checkbox" />
    Subscribe to newsletter
  </label>
  
  <select bind:value={formData.category}>
    <option value="general">General</option>
    <option value="support">Support</option>
    <option value="feedback">Feedback</option>
  </select>
  
  <button type="submit">Submit</button>
</form>

HellaJS Controlled Inputs

HellaJS uses controlled components with explicit event handling.

const FormExample = () => {
  const formData = signal({
    name: '',
    email: '',
    age: 0,
    subscribe: false,
    category: 'general'
  });
  
  const updateField = (field, value) => {
    formData({ ...formData(), [field]: value });
  };
  
  const handleSubmit = (e) => {
    e.preventDefault();
    console.log('Form submitted:', formData());
  };
  
  return (
    <form onSubmit={handleSubmit}>
      <input 
        value={formData().name}
        onChange={e => updateField('name', e.target.value)}
        placeholder="Name" 
      />
      <input 
        value={formData().email}
        onChange={e => updateField('email', e.target.value)}
        type="email" 
        placeholder="Email" 
      />
      <input 
        value={formData().age}
        onChange={e => updateField('age', +e.target.value)}
        type="number" 
        placeholder="Age" 
      />
      
      <label>
        <input 
          checked={formData().subscribe}
          onChange={e => updateField('subscribe', e.target.checked)}
          type="checkbox" 
        />
        Subscribe to newsletter
      </label>
      
      <select 
        value={formData().category}
        onChange={e => updateField('category', e.target.value)}
      >
        <option value="general">General</option>
        <option value="support">Support</option>
        <option value="feedback">Feedback</option>
      </select>
      
      <button type="submit">Submit</button>
    </form>
  );
};

Key Differences

  • Binding Approach: Svelte uses bind: directives, HellaJS uses controlled component pattern
  • Two-way vs One-way: Svelte has two-way binding, HellaJS uses one-way data flow with event handlers
  • Boilerplate: Svelte requires less code, HellaJS is more explicit about data flow
  • Form Libraries: Both can integrate with form validation and management libraries