Migrating from Vue
HellaJS offers a fine-grained reactive approach to building UIs with concepts familiar to Vue 3 developers. This guide will help you transition from Vue 3 to HellaJS by showing equivalent patterns and highlighting architectural differences.
Key Differences
- Granular DOM Updates: HellaJS updates individual DOM nodes, while Vue 3 updates components with optimized diffing
- Signals vs Refs: HellaJS signals use function calls, while Vue 3 refs use
.value
property access - Dedicated List Function: HellaJS uses forEach for optimized list rendering, while Vue 3 uses v-for directive
- Element Lifecycle: HellaJS lifecycle hooks are tied to elements, while Vue 3 hooks are component-scoped
Template Syntax
Vue Template Syntax
Template-based with directives and interpolation.
<template>
<div>
<h1>{{ title }}</h1>
<p v-if="showMessage">{{ message }}</p>
<p v-else>No message</p>
<ul>
<li v-for="item in items" :key="item.id">
{{ item.name }}
</li>
</ul>
<button
:class="{ active: isActive }"
:disabled="loading"
@click="handleClick"
>
{{ buttonText }}
</button>
</div>
</template>
<script setup>
import { ref } from 'vue';
const title = ref('Hello');
const showMessage = ref(true);
const message = ref('Welcome');
const items = ref([{ id: 1, name: 'Item 1' }]);
const isActive = ref(false);
const loading = ref(false);
const buttonText = ref('Click me');
const handleClick = () => {
console.log('Clicked');
};
</script>
HellaJS JSX Syntax
JSX-based with JavaScript expressions and conditional rendering.
const Component = () => {
const title = signal('Hello');
const showMessage = signal(true);
const message = signal('Welcome');
const items = signal([{ id: 1, name: 'Item 1' }]);
const isActive = signal(false);
const loading = signal(false);
const buttonText = signal('Click me');
const handleClick = () => {
console.log('Clicked');
};
return (
<div>
<h1>{title}</h1>
{showMessage() ? <p>{message}</p> : <p>No message</p>}
<ul>
{forEach(items, item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
<button
class={isActive() ? 'active' : ''}
disabled={loading()}
onClick={handleClick}
>
{buttonText}
</button>
</div>
);
};
Key Differences
- Template Language: HellaJS uses JSX, while Vue 3 uses template syntax with directives and compile-time optimizations
- Conditional Rendering: HellaJS uses JavaScript conditionals, while Vue 3 uses v-if/v-else with efficient rendering
- Class Binding: HellaJS uses string concatenation or computed values, while Vue 3 uses object/array syntax with reactive classes
- Event Binding: HellaJS uses standard event props, while Vue 3 uses @ syntax with automatic listeners
Component Rendering
Vue Virtual DOM
Uses a virtual DOM with compile-time optimizations and re-renders components when state changes, then diffs the virtual DOM to update the real DOM.
<template>
<div>{{ count }}</div>
</template>
<script setup>
import { ref } from 'vue';
const count = ref(0);
</script>
HellaJS DOM Binding
Creates direct reactive bindings between signals and DOM elements, eliminating the need for diffing and re-renders.
const Counter = () => {
const count = signal(0);
return <div>{count}</div>; // Only this text node updates
};
Key Differences
- Direct Binding vs Virtual DOM: HellaJS binds signals directly to DOM elements, while Vue 3 uses optimized virtual DOM with compile-time hints
- Granular Updates vs Re-rendering: HellaJS updates only the specific DOM nodes that change, while Vue 3 re-executes render functions with optimizations
- Update Granularity: HellaJS updates at individual DOM node level, while Vue 3 updates at component level
Component Communication
Vue Props and Emits
Props down, events up pattern with defineProps and defineEmits.
<!-- Child.vue -->
<template>
<div>
<h2>{{ title }}</h2>
<button @click="$emit('increment', count + 1)">
Count: {{ count }}
</button>
</div>
</template>
<script setup>
defineProps({
title: String,
count: Number
});
defineEmits(['increment']);
</script>
<!-- Parent.vue -->
<template>
<Child
:title="title"
:count="counter"
@increment="counter = $event"
/>
</template>
<script setup>
import { ref } from 'vue';
const title = ref('My Counter');
const counter = ref(0);
</script>
HellaJS Props and Callbacks
Direct prop passing with callback functions for communication.
const Child = ({ title, count, onIncrement }) => {
return (
<div>
<h2>{title}</h2>
<button onClick={() => onIncrement(count() + 1)}>
Count: {count}
</button>
</div>
);
};
const Parent = () => {
const title = signal('My Counter');
const counter = signal(0);
return (
<Child
title={title}
count={counter}
onIncrement={(newCount) => counter(newCount)}
/>
);
};
Key Differences
- Props Declaration: HellaJS uses function parameters, while Vue 3 uses defineProps with validation
- Event Communication: HellaJS uses callback props, while Vue 3 uses defineEmits for type-safe events
- Reactivity: HellaJS passes signals directly for granular reactivity, while Vue 3 props are automatically reactive
- Type Safety: HellaJS relies on TypeScript for type safety, while Vue 3 has built-in props validation and TypeScript support
Component State
Vue ref
and reactive
Manages component state with ref for primitives and reactive for objects.
<template>
<div>
<h1>Hello, {{ name }}!</h1>
<p>Count: {{ count }}</p>
<button @click="count++">+</button>
<input v-model="name" />
</div>
</template>
<script setup>
import { ref } from 'vue';
const name = ref('World');
const count = ref(0);
</script>
HellaJS signal
Signals provide reactive state that automatically updates dependent DOM elements.
const App = () => {
const name = signal('World');
const count = signal(0);
return (
<div>
<h1>Hello, {name}!</h1>
<p>Count: {count}</p>
<button onClick={() => count(count() + 1)}>+</button>
<input value={name} onChange={e => name(e.target.value)} />
</div>
);
};
Key Differences
- API Pattern: HellaJS uses function call pattern for signals, while Vue 3 uses
.value
for refs - Reactivity: HellaJS updates specific DOM bindings directly, while Vue 3 triggers component re-renders
- Dependency Tracking: HellaJS tracks dependencies at DOM element level, while Vue 3 tracks at component level
Cached State
Vue computed
Computed properties for expensive calculations with automatic dependency tracking.
<template>
<div>
<p>{{ filteredTodos.length }} todos</p>
<button @click="addTodo">Add</button>
</div>
</template>
<script setup>
import { ref, computed } from 'vue';
const todos = ref([]);
const filter = ref('all');
const filteredTodos = computed(() => {
return todos.value.filter(todo => {
if (filter.value === 'completed') return todo.done;
if (filter.value === 'active') return !todo.done;
return true;
});
});
const addTodo = () => {
todos.value.push({ id: Date.now(), text: 'New todo', done: false });
};
</script>
HellaJS computed
Values automatically cache results and recompute only when their dependencies change.
const TodoApp = () => {
const todos = signal([]);
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 addTodo = () => {
todos([...todos(), { id: Date.now(), text: 'New todo', done: false }]);
};
return (
<div>
<p>{filteredTodos().length} todos</p>
<button onClick={addTodo}>Add</button>
</div>
);
};
Key Differences
- Dependency Management: HellaJS computed automatically tracks dependencies, similar to Vue 3 computed
- Caching Strategy: HellaJS uses lazy evaluation and automatic invalidation when dependencies change, like Vue 3
- API Consistency: HellaJS uses consistent function call pattern for all reactives, while Vue 3 uses
.value
Global State
Vue Pinia Store
Pinia provides centralized state management with stores and actions.
<!-- store/counter.js -->
<script>
import { defineStore } from 'pinia';
export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0,
theme: 'light'
}),
actions: {
increment() {
this.count++;
},
setTheme(newTheme) {
this.theme = newTheme;
}
}
});
</script>
<!-- Component.vue -->
<template>
<button @click="store.increment">
Current count: {{ store.count }}
</button>
</template>
<script setup>
import { useCounterStore } from '@/stores/counter';
const store = useCounterStore();
</script>
HellaJS Global Signals
Global signals can be imported and used directly in any component without stores or providers.
// Global signals - no store setup needed
const count = signal(0);
const theme = signal('light');
const Button = () => {
return (
<button onClick={() => count(count() + 1)}>
Current count: {count}
</button>
);
};
// Any component can use the global signals directly
const App = () => {
return (
<div class={`app theme-${theme()}`}>
<Button />
<button onClick={() => theme(theme() === 'light' ? 'dark' : 'light')}>
Toggle Theme
</button>
</div>
);
};
HellaJS Stores
Fine-grained reactivity where each property is independently reactive.
// Store
const user = store({
name: '',
email: '',
preferences: {
theme: 'light'
}
});
// Component
const UserProfile = () => {
return (
<div>
<input
value={user.name}
onInput={e => user.name(e.target.value)}
/>
<p>Theme: {user.preferences.theme}</p>
<button onClick={() => user.preferences.theme('dark')}>
Dark Theme
</button>
</div>
);
};
Key Differences
- Store Pattern: HellaJS uses global signals with direct imports, while Vue 3 Pinia provides structured stores with composition
- Boilerplate: HellaJS works with direct reactive primitives, while Vue 3 Pinia requires store setup and actions
- Update Granularity: HellaJS updates only DOM elements bound to specific signals, while Vue 3 Pinia updates components that use the store
Side Effects
Vue watchEffect
and watch
Watch functions for side effects with manual dependency tracking or explicit watchers.
<template>
<div v-if="loading">Loading...</div>
<div v-else>{{ user?.name }}</div>
</template>
<script setup>
import { ref, watchEffect } from 'vue';
const userId = ref(1);
const user = ref(null);
const loading = ref(false);
watchEffect(async () => {
if (!userId.value) return;
loading.value = true;
try {
const response = await fetch(`/api/users/${userId.value}`);
const data = await response.json();
user.value = data;
} catch (err) {
console.error('Failed to load user:', err);
} finally {
loading.value = false;
}
});
</script>
HellaJS effect
Automatically tracks dependencies and handles async operations with built-in cancellation.
const UserProfile = ({ userId }) => {
const user = signal(null);
const loading = signal(false);
effect(async () => {
const id = userId(); // Automatically tracked
if (!id) return;
loading(true);
try {
const response = await fetch(`/api/users/${id}`);
const data = await response.json();
user(data);
} catch (err) {
console.error('Failed to load user:', err);
} finally {
loading(false);
}
});
return (
<div>
{loading() ? <div>Loading...</div> : <div>{user()?.name}</div>}
</div>
);
};
Key Differences
- Dependency Tracking: HellaJS effect automatically tracks reactive dependencies, similar to Vue 3 watchEffect
- Async Handling: HellaJS effects support async/await with automatic cancellation, while Vue 3 watchEffect supports async functions
- Cleanup: HellaJS provides automatic cleanup and request cancellation, while Vue 3 watchEffect returns cleanup functions
- Race Conditions: HellaJS automatically cancels previous async operations, while Vue 3 requires manual race condition handling
Lists and Iteration
Vue v-for
Render lists using v-for directive with required key attributes for efficient reconciliation.
<template>
<ul>
<li v-for="todo in todos" :key="todo.id">
<input
type="checkbox"
v-model="todo.done"
@change="toggleTodo(todo, $event)"
/>
{{ todo.text }}
</li>
</ul>
</template>
<script setup>
import { ref } from 'vue';
const todos = ref([
{ id: 1, text: 'Learn Vue', done: false },
{ id: 2, text: 'Build app', done: true }
]);
const toggleTodo = (todo, event) => {
todo.done = event.target.checked;
};
</script>
HellaJS forEach
Optimized function for 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, checked) => {
todos(todos().map(t =>
t.id === todo.id ? { ...t, done: checked } : t
));
};
return (
<ul>
{forEach(todos, todo => (
<li key={todo.id}>
<input
type="checkbox"
checked={todo.done}
onChange={(e) => toggleTodo(todo, e.target.checked)}
/>
{todo.text}
</li>
))}
</ul>
);
};
Key Differences
- List Rendering: HellaJS uses dedicated forEach function, while Vue 3 uses v-for directive with keyed reconciliation
- Performance: HellaJS uses Longest Increasing Subsequence (LIS) for minimal DOM operations, while Vue 3 reconciles lists with optimized algorithms
- Reactivity: HellaJS forEach maintains granular reactive connections to individual items, while Vue 3 v-for re-renders when array changes
- Mutability: HellaJS requires immutable updates for proper reactivity, while Vue 3 can mutate reactive arrays directly
Lifecycle Hooks
Vue Lifecycle Hooks
Component-scoped lifecycle with composition API hooks.
<template>
<div>Timer: {{ count }}</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
const count = ref(0);
let interval;
onMounted(() => {
interval = setInterval(() => {
count.value++;
}, 1000);
});
onUnmounted(() => {
clearInterval(interval);
});
</script>
HellaJS onDestroy
Element-level lifecycle hooks that are tied directly to DOM elements rather than component instances.
const Timer = () => {
const count = signal(0);
const interval = setInterval(() => {
count(count() + 1);
}, 1000);
return (
<div onDestroy={() => clearInterval(interval)}>
Timer: {count}
</div>
);
};
Key Differences
- Lifecycle Scope: HellaJS hooks are element-scoped, while Vue 3 hooks are component-scoped
- Cleanup Pattern: HellaJS uses onDestroy element attribute, while Vue 3 uses onUnmounted and cleanup functions
- Automatic Management: HellaJS automatically handles element cleanup when removed from DOM, while Vue 3 handles component cleanup
- Granular Control: HellaJS allows different lifecycle behavior per element, while Vue 3 manages component lifecycle
Event Handling
Vue Event Handling
Event listeners with @ syntax and event modifiers.
<template>
<div>
<button @click="handleClick">Click me</button>
<input @keyup.enter="handleEnter" v-model="inputValue" />
<form @submit.prevent="handleSubmit">
<button type="submit">Submit</button>
</form>
</div>
</template>
<script setup>
import { ref } from 'vue';
const inputValue = ref('');
const handleClick = () => {
console.log('Button clicked');
};
const handleEnter = () => {
console.log('Enter pressed');
};
const handleSubmit = () => {
console.log('Form submitted');
};
</script>
HellaJS Event Handling
Direct event handler assignment with standard DOM events.
const EventExample = () => {
const inputValue = signal('');
const handleClick = () => {
console.log('Button clicked');
};
const handleKeyUp = (e) => {
if (e.key === 'Enter') {
console.log('Enter pressed');
}
};
const handleSubmit = (e) => {
e.preventDefault();
console.log('Form submitted');
};
return (
<div>
<button onClick={handleClick}>Click me</button>
<input
onKeyUp={handleKeyUp}
value={inputValue}
onChange={e => inputValue(e.target.value)}
/>
<form onSubmit={handleSubmit}>
<button type="submit">Submit</button>
</form>
</div>
);
};
Key Differences
- Event Syntax: HellaJS uses standard DOM event props, while Vue 3 uses @ syntax with built-in modifiers
- Event Modifiers: HellaJS uses standard JavaScript patterns, while Vue 3 provides convenient modifiers (.prevent, .stop, etc.)