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