Converting to Angular Signal Syntax: Inputs, Outputs, and Dependency Injection
Angular has undergone a massive renaissance, introducing modern reactive primitives known as Signals. Signals provide a fine-grained, developer-friendly way to manage state changes and reactivity, leading to faster execution and simpler component change detection.
If you are upgrading an older Angular application or starting a new project, migrating to Signal-based syntax is one of the highest-impact improvements you can make. Let's look at simple, direct examples of how to rewrite your decorators and constructors using the new APIs.
1. Changing @Input to input()
In traditional Angular, we used the @Input() decorator to pass data into components. The variable was a standard property that was not reactive unless we intercepted it with a setter or implemented OnChanges.
With Signals, we use the input() function. The input becomes a read-only Signal, meaning we can track its changes dynamically and derive state reactively.
Before: Decorator-based Input
import { Component, Input } from '@angular/core';
@Component({
selector: 'app-user-profile',
template: `<div>User: {{ username }}</div>`
})
export class UserProfileComponent {
@Input() username: string = 'Guest';
@Input({ required: true }) userId!: string;
}
After: Signal-based Input
import { Component, input } from '@angular/core';
@Component({
selector: 'app-user-profile',
template: `<div>User: {{ username() }}</div>` // Note the parenthesis!
})
export class UserProfileComponent {
// Declared as a Signal input with a default value
username = input<string>('Guest');
// Declared as a required Signal input
userId = input.required<string>();
}
2. Changing @Output to output()
Outputs are used to emit custom events to parent components. Previously, we used @Output() coupled with the EventEmitter class.
Angular now provides a simplified output() function. It operates similarly but is lighter and does not rely on RxJS EventEmitter internally.
Before: Decorator-based Output
import { Component, Output, EventEmitter } from '@angular/core';
@Component({
selector: 'app-user-actions',
template: `<button (click)="onDelete()">Delete</button>`
})
export class UserActionsComponent {
@Output() userDeleted = new EventEmitter<string>();
onDelete() {
this.userDeleted.emit('user-123');
}
}
After: Signal-friendly Output
import { Component, output } from '@angular/core';
@Component({
selector: 'app-user-actions',
template: `<button (click)="onDelete()">Delete</button>`
})
export class UserActionsComponent {
// Use the output() function
userDeleted = output<string>();
onDelete() {
this.userDeleted.emit('user-123');
}
}
3. Using inject() Over Constructor Injection
For years, constructor-based dependency injection was the only way to request services in Angular.
Now, the inject() function provides a more flexible, readable alternative. It allows you to declare dependencies directly as class fields, which is especially helpful when using functional features like route guards or inheritance.
Before: Constructor Injection
import { Component, OnInit } from '@angular/core';
import { UserService } from './user.service';
@Component({
selector: 'app-user-dashboard',
template: `...`
})
export class UserDashboardComponent implements OnInit {
users: any[] = [];
constructor(private userService: UserService) {}
ngOnInit() {
this.userService.getUsers().subscribe(data => this.users = data);
}
}
After: inject() Function Injection
import { Component, inject, OnInit } from '@angular/core';
import { UserService } from './user.service';
@Component({
selector: 'app-user-dashboard',
template: `...`
})
export class UserDashboardComponent implements OnInit {
// Inject directly as a property
private userService = inject(UserService);
users: any[] = [];
ngOnInit() {
this.userService.getUsers().subscribe(data => this.users = data);
}
}
4. Derived State: computed() vs Getters
Previously, if you wanted to compute a value based on inputs, you would use a standard TypeScript getter. However, get methods run on every change detection cycle, even when the input values haven't changed.
With Signals, you wrap your calculations in computed(). The value is cached and only recalculates when the dependencies (the inputs or other signals) actually emit a new value.
Before: Getters or Lifecycle hook
import { Component, Input, OnChanges } from '@angular/core';
@Component({
selector: 'app-user-header',
template: `<h1>Hello, {{ displayName }}</h1>`
})
export class UserHeaderComponent implements OnChanges {
@Input() firstName: string = '';
@Input() lastName: string = '';
displayName: string = '';
ngOnChanges() {
this.displayName = `${this.firstName} ${this.lastName}`.trim();
}
}
After: Computed Signals
import { Component, input, computed } from '@angular/core';
@Component({
selector: 'app-user-header',
template: `<h1>Hello, {{ displayName() }}</h1>`
})
export class UserHeaderComponent {
firstName = input<string>('');
lastName = input<string>('');
// Dynamically computed and cached
displayName = computed(() => {
return `${this.firstName()} ${this.lastName()}`.trim();
});
}
Conclusion
Migrating your code to the new Angular Signal APIs results in cleaner templates, more predictable reactivity, and better component performance. Modernizing your codebase step-by-step with these patterns prepares your app for Angular's future zoneless change detection!