Migrating from Angular
HellaJS offers a simpler, more direct approach to building reactive UIs while maintaining many familiar concepts from Angular.
This guide will help you transition from Angular to HellaJS by showing equivalent patterns and highlighting key differences.
Key Differences
- Granular DOM Updates: HellaJS updates only the parts of the DOM that change, unlike Angular’s zone-based change detection
- Signals vs Observables: HellaJS signals automatically track dependencies and updates, compared to Angular’s RxJS-based reactivity
- Direct Function Components: HellaJS uses simple functions instead of Angular’s class-based components with decorators
- No Framework Overhead: HellaJS eliminates the need for zone.js, dependency injection, and complex lifecycle hooks
Component Architecture
Angular Components
Angular uses class-based components with decorators, templates, and complex lifecycle hooks.
@Component({
selector: 'app-counter',
template: `
<div>
<h1>{{ count }}</h1>
<button (click)="increment()">+</button>
</div>
`
})
export class CounterComponent {
count = 0;
increment() {
this.count++; // Triggers change detection for entire component tree
}
}
HellaJS Components
Simple functions that return JSX elements with direct reactive bindings.
const Counter = () => {
const count = signal(0);
return (
<div>
<h1>{count}</h1>
<button onClick={() => count(count() + 1)}>+</button>
</div>
);
};
Key Differences
- Class vs Function: HellaJS uses simple functions instead of Angular’s class-based components
- Direct Binding vs Template Syntax: HellaJS binds signals directly to DOM elements, while Angular uses template syntax
- Granular Updates vs Zone Detection: HellaJS updates only changed elements, while Angular triggers change detection across component trees
- No Decorators: HellaJS eliminates the need for @Component, @Input, @Output decorators
State Management
Angular Services & RxJS
Angular uses services with dependency injection and RxJS for state management.
@Injectable({
providedIn: 'root'
})
export class CounterService {
private countSubject = new BehaviorSubject(0);
count$ = this.countSubject.asObservable();
increment() {
this.countSubject.next(this.countSubject.value + 1);
}
}
@Component({
selector: 'app-counter',
template: `
<div>
<h1>{{ count$ | async }}</h1>
<button (click)="increment()">+</button>
</div>
`
})
export class CounterComponent {
count$ = this.counterService.count$;
constructor(private counterService: CounterService) {}
increment() {
this.counterService.increment();
}
}
HellaJS Signals
Signals provide reactive state that automatically updates dependent DOM elements.
// Global state - no service needed
const count = signal(0);
const Counter = () => {
return (
<div>
<h1>{count}</h1>
<button onClick={() => count(count() + 1)}>+</button>
</div>
);
};
// Any component can use the global signal directly
const Display = () => {
return <p>Current count: {count}</p>;
};
Key Differences
- Direct Imports vs Dependency Injection: HellaJS uses direct imports, while Angular requires dependency injection
- Signals vs Observables: HellaJS signals work directly in templates, while Angular requires async pipes or subscriptions
- Automatic Updates vs Manual Subscriptions: HellaJS automatically updates when signals change, unlike Angular’s manual subscription management
- No Services Required: HellaJS global signals eliminate the need for Angular’s service layer
Component Communication
Angular Input/Output
Angular uses @Input and @Output decorators for parent-child communication.
// Child Component
@Component({
selector: 'app-todo-item',
template: `
<div>
<input
type="checkbox"
[checked]="todo.done"
(change)="onToggle()"
/>
{{ todo.text }}
</div>
`
})
export class TodoItemComponent {
@Input() todo!: Todo;
@Output() toggle = new EventEmitter<number>();
onToggle() {
this.toggle.emit(this.todo.id);
}
}
// Parent Component
@Component({
selector: 'app-todo-list',
template: `
<div>
<app-todo-item
*ngFor="let todo of todos"
[todo]="todo"
(toggle)="toggleTodo($event)"
/>
</div>
`
})
export class TodoListComponent {
todos: Todo[] = [
{ id: 1, text: 'Learn Angular', done: false },
{ id: 2, text: 'Build app', done: true }
];
toggleTodo(id: number) {
this.todos = this.todos.map(t =>
t.id === id ? { ...t, done: !t.done } : t
);
}
}
HellaJS Props & Callbacks
Direct prop passing with simple function callbacks.
const TodoItem = ({ todo, onToggle }) => {
return (
<div>
<input
type="checkbox"
checked={todo.done}
onChange={() => onToggle(todo.id)}
/>
{todo.text}
</div>
);
};
const TodoList = () => {
const todos = signal([
{ id: 1, text: 'Learn HellaJS', done: false },
{ id: 2, text: 'Build app', done: true }
]);
const toggleTodo = (id) => {
todos(todos().map(t =>
t.id === id ? { ...t, done: !t.done } : t
));
};
return (
<div>
{forEach(todos, todo => (
<TodoItem key={todo.id} todo={todo} onToggle={toggleTodo} />
))}
</div>
);
};
Key Differences
- Props vs Decorators: HellaJS uses simple props, while Angular requires @Input/@Output decorators
- Direct Callbacks vs EventEmitters: HellaJS uses simple function callbacks, while Angular uses EventEmitter
- No Template Syntax: HellaJS eliminates Angular’s template binding syntax and structural directives
- Reactive Props: HellaJS props can be signals that maintain reactivity across component boundaries
Reactive Data
Angular Reactive Forms & Observables
Angular uses reactive forms with complex observable patterns.
@Component({
selector: 'app-user-form',
template: `
<form [formGroup]="userForm" (ngSubmit)="onSubmit()">
<input formControlName="name" placeholder="Name" />
<input formControlName="email" placeholder="Email" />
<p>{{ nameUppercase$ | async }}</p>
<button type="submit" [disabled]="userForm.invalid">Submit</button>
</form>
`
})
export class UserFormComponent implements OnInit {
userForm = this.fb.group({
name: ['', Validators.required],
email: ['', [Validators.required, Validators.email]]
});
nameUppercase$ = this.userForm.get('name')!.valueChanges.pipe(
map(name => name?.toUpperCase())
);
constructor(private fb: FormBuilder) {}
onSubmit() {
if (this.userForm.valid) {
console.log(this.userForm.value);
}
}
}
HellaJS Computed Values
Values automatically cache results and recompute only when their dependencies change.
const UserForm = () => {
const name = signal('');
const email = signal('');
const nameUppercase = computed(() => name().toUpperCase());
const isValid = computed(() => {
return name().length > 0 && email().includes('@');
});
const onSubmit = (e) => {
e.preventDefault();
if (isValid()) {
console.log({ name: name(), email: email() });
}
};
return (
<form onSubmit={onSubmit}>
<input
value={name}
onInput={e => name(e.target.value)}
placeholder="Name"
/>
<input
value={email}
onInput={e => email(e.target.value)}
placeholder="Email"
/>
<p>{nameUppercase}</p>
<button type="submit" disabled={!isValid()}>
Submit
</button>
</form>
);
};
Key Differences
- Direct Signals vs FormBuilder: HellaJS uses direct signals, while Angular requires FormBuilder and reactive forms
- Automatic Computed vs Pipes: HellaJS computed values update automatically, while Angular requires async pipes or operators
- Simple Validation vs Validators: HellaJS uses simple computed validation, while Angular requires Validators and complex form states
- No Observable Complexity: HellaJS eliminates the need for RxJS operators and subscription management
Routing & Navigation
Angular Router
Angular uses a complex router with guards, resolvers, and nested routes.
// app-routing.module.ts
const routes: Routes = [
{ path: '', component: HomeComponent },
{ path: 'users/:id', component: UserComponent },
{ path: 'todos', component: TodoListComponent, canActivate: [AuthGuard] }
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule {}
// Component
@Component({
selector: 'app-user',
template: `
<div>
<h1>User {{ userId }}</h1>
<p>{{ user?.name }}</p>
</div>
`
})
export class UserComponent implements OnInit {
userId: string;
user: User | null = null;
constructor(
private route: ActivatedRoute,
private userService: UserService
) {}
ngOnInit() {
this.userId = this.route.snapshot.paramMap.get('id')!;
this.userService.getUser(this.userId).subscribe(user => {
this.user = user;
});
}
}
HellaJS Router
Simple function-based routing with reactive parameters.
const App = () => {
mount(<>Loading...</>);
const user = signal(null);
router({
routes: {
'/': () => mount(<Home />),
'/users/:id': async (params) => {
// Automatically reactive to param changes
const userData = await fetch(`/api/users/${params.id}`).then(r => r.json());
user(userData);
mount(<User user={user} userId={params.id} />);
},
'/todos': {
before: () => {
// Simple auth guard
if (!isAuthenticated()) {
navigate('/login');
return false;
}
},
handler: () => mount(<TodoList />)
}
}
});
const User = ({ user, userId }) => (
<div>
<h1>User {userId}</h1>
<p>{user()?.name}</p>
</div>
);
Key Differences
- Function Handlers vs Components: HellaJS uses simple functions, while Angular requires component classes
- Direct Parameter Access vs ActivatedRoute: HellaJS passes parameters directly to handlers, unlike Angular’s service injection
- Simple Guards vs Complex Guards: HellaJS uses simple before functions, while Angular requires guard classes and interfaces
- Reactive by Default: HellaJS routing integrates seamlessly with signals, unlike Angular’s subscription-based approach
Side Effects & Lifecycle
Angular Lifecycle Hooks
Angular uses complex lifecycle hooks with manual subscription management.
@Component({
selector: 'app-timer',
template: `<div>Timer: {{ seconds }}</div>`
})
export class TimerComponent implements OnInit, OnDestroy {
seconds = 0;
private interval: any;
private subscription = new Subscription();
ngOnInit() {
this.interval = setInterval(() => {
this.seconds++;
}, 1000);
// Manual subscription management required
this.subscription.add(
this.someService.data$.subscribe(data => {
console.log('Data updated:', data);
})
);
}
ngOnDestroy() {
if (this.interval) {
clearInterval(this.interval);
}
this.subscription.unsubscribe();
}
}
HellaJS Effects & Lifecycle
Automatic dependency tracking and cleanup with built-in lifecycle hooks.
const Timer = () => {
const seconds = signal(0);
const interval = setInterval(() => {
seconds(seconds() + 1);
}, 1000);
// Effect automatically tracks dependencies
effect(() => {
console.log('Seconds updated:', seconds());
});
return (
<div onDestroy={() => clearInterval(interval)}>
Timer: {seconds}
</div>
);
};
// Or for more complex effects with async operations
const DataComponent = () => {
const userId = signal(1);
const user = signal(null);
const loading = signal(false);
effect(async () => {
const id = userId(); // Automatically tracked
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);
}
});
return (
<div>
{loading() ? (
<div>Loading...</div>
) : (
<div>{user()?.name}</div>
)}
</div>
);
};
Key Differences
- Automatic Dependency Tracking: HellaJS automatically tracks signal reads, while Angular requires manual subscription management
- Element-Level Cleanup: HellaJS uses onDestroy on elements, while Angular requires component-level lifecycle hooks
- Built-in Async Support: HellaJS effects support async/await directly, while Angular requires complex RxJS patterns
- No Memory Leaks: HellaJS automatically handles cleanup and cancellation, unlike Angular’s manual subscription management
List Rendering & Change Detection
Angular ngFor
Angular uses structural directives with trackBy functions for performance.
@Component({
selector: 'app-todo-list',
template: `
<ul>
<li *ngFor="let todo of todos; trackBy: trackByTodo"
[class.completed]="todo.done">
<input
type="checkbox"
[checked]="todo.done"
(change)="toggleTodo(todo.id)"
/>
{{ todo.text }}
</li>
</ul>
`
})
export class TodoListComponent {
todos: Todo[] = [
{ id: 1, text: 'Learn Angular', done: false },
{ id: 2, text: 'Build app', done: true }
];
trackByTodo(index: number, todo: Todo): number {
return todo.id; // Required for performance
}
toggleTodo(id: number) {
this.todos = this.todos.map(t =>
t.id === id ? { ...t, done: !t.done } : t
);
}
}
HellaJS forEach
Optimized function for reactive list rendering with automatic key-based diffing.
const TodoList = () => {
const todos = signal([
{ id: 1, text: 'Learn HellaJS', done: false },
{ id: 2, text: 'Build app', done: true }
]);
const toggleTodo = (id) => {
todos(todos().map(t =>
t.id === id ? { ...t, done: !t.done } : t
));
};
return (
<ul>
{forEach(todos, todo => (
<li key={todo.id} class={todo.done ? 'completed' : ''}>
<input
type="checkbox"
checked={todo.done}
onChange={() => toggleTodo(todo.id)}
/>
{todo.text}
</li>
))}
</ul>
);
};
Key Differences
- Automatic Performance: HellaJS forEach uses advanced diffing algorithms (LIS) automatically, while Angular requires manual trackBy functions
- Reactive by Default: HellaJS lists are inherently reactive, while Angular requires change detection cycles
- Simpler Syntax: HellaJS eliminates structural directives and template syntax
- Key-Based Optimization: HellaJS automatically optimizes based on keys, while Angular requires manual trackBy implementation
Dependency Injection vs Direct Imports
Angular Services & DI
Angular uses a complex dependency injection system with providers and decorators.
@Injectable({
providedIn: 'root'
})
export class ApiService {
constructor(private http: HttpClient) {}
getUsers() {
return this.http.get<User[]>('/api/users');
}
}
@Injectable({
providedIn: 'root'
})
export class UserService {
private users$ = new BehaviorSubject<User[]>([]);
constructor(private apiService: ApiService) {}
loadUsers() {
this.apiService.getUsers().subscribe(users => {
this.users$.next(users);
});
}
get users() {
return this.users$.asObservable();
}
}
@Component({
selector: 'app-users',
template: `
<div>
<button (click)="refresh()">Refresh</button>
<div *ngFor="let user of users$ | async">
{{ user.name }}
</div>
</div>
`
})
export class UsersComponent implements OnInit {
users$ = this.userService.users;
constructor(private userService: UserService) {}
ngOnInit() {
this.userService.loadUsers();
}
refresh() {
this.userService.loadUsers();
}
}
HellaJS Direct State & Resources
Simple direct imports with global state and resource management.
// Global state and resources - no services needed
const users = signal([]);
const loadUsers = async () => {
const response = await fetch('/api/users');
const data = await response.json();
users(data);
};
// Or use resources for automatic state management
const usersResource = resource(() =>
fetch('/api/users').then(r => r.json())
);
const Users = () => {
// Option 1: Simple global state
return (
<div>
<button onClick={loadUsers}>Refresh</button>
{forEach(users, user => (
<div key={user.id}>{user.name}</div>
))}
</div>
);
// Option 2: Using resources
return (
<div>
<button onClick={() => usersResource.request()}>Refresh</button>
{usersResource.loading() && <div>Loading...</div>}
{usersResource.data() && forEach(usersResource.data, user => (
<div key={user.id}>{user.name}</div>
))}
</div>
);
};
Key Differences
- Direct Imports vs DI: HellaJS uses simple imports, while Angular requires complex dependency injection setup
- Global State vs Services: HellaJS global signals eliminate the need for Angular’s service layer
- No Providers: HellaJS eliminates the need for provider configuration and injection tokens
- Resources vs Http Services: HellaJS resources provide automatic loading states without manual subscription management
Testing Considerations
Angular Testing
Angular requires complex testing setup with TestBed and mock services.
describe('CounterComponent', () => {
let component: CounterComponent;
let fixture: ComponentFixture<CounterComponent>;
let counterService: jasmine.SpyObj<CounterService>;
beforeEach(() => {
const spy = jasmine.createSpyObj('CounterService', ['increment']);
TestBed.configureTestingModule({
declarations: [CounterComponent],
providers: [{ provide: CounterService, useValue: spy }]
});
fixture = TestBed.createComponent(CounterComponent);
component = fixture.componentInstance;
counterService = TestBed.inject(CounterService) as jasmine.SpyObj<CounterService>;
});
it('should increment counter', () => {
component.increment();
expect(counterService.increment).toHaveBeenCalled();
});
});
HellaJS Testing
Simple function testing without complex setup.
import { describe, it, expect } from 'vitest';
import { signal } from '@hellajs/core';
describe('Counter', () => {
it('should increment counter', () => {
const count = signal(0);
// Direct function call
count(count() + 1);
expect(count()).toBe(1);
});
it('should render counter', () => {
const count = signal(5);
const result = Counter({ initialValue: 5 });
// Test component function directly
expect(result.children[0].children).toBe(count);
});
});
Key Differences
- Simple Function Testing: HellaJS components are just functions that can be tested directly
- No TestBed Required: HellaJS eliminates Angular’s complex testing module setup
- Direct Signal Testing: Signals can be tested without mock services or dependency injection
- No Change Detection: HellaJS doesn’t require fixture.detectChanges() or zone.js testing utilities