A Fresh Signals API for Component Communication in Angular 17.3
Angular 17.3 ships with a developer preview of a new Signals API, first showcased during the ngConf 2024 keynote delivered by Jeremy Elbourn and Minko Gechev.
This addition aims to streamline how components interact and manage data. The following sections break down four core pieces of the API—Signal Queries, Signal Inputs, Signal Outputs (with an important caveat), and Model Inputs—and look at what each one brings to the table.
1. Signal Queries: A More Direct Way to Grab Template References
For a long time, fetching references to elements or directives in a template meant reaching for decorators like @ViewChild or @ContentChildren. Those tools work, but they tend to add a fair amount of ceremony to the component class.
With the Signals API, there's a more streamlined option: you declare the reference as a signal right alongside your other component properties. This keeps the code more readable. On top of that, there's also a new .required modifier. If you mark a query signal as required, Angular will throw an error whenever the expected result is absent, which helps surface problems much earlier in the development cycle.
The snippet below shows the shift in style from the old decorator approach to the new signal-based one:
// Before (using decorators)
@Component({/* ... */})
export class Menu {
@ViewChild('trigger') trigger: ElementRef;
@ContentChildren(MenuItem)
items: QueryList<MenuItem> | undefined;
}
// After (using signals)
@Component({/* ... */})
export class Menu {
trigger = viewChild('trigger');
// New: mark as required
items = contentChildren(MenuItem).required;
}
2. Signal Inputs: They Work Inside Computed Values and Effects
The @Input decorator has been the go-to solution for getting data into a component. However, when you want to derive values or run side effects based on that input, the traditional pattern can start to feel restrictive.
Signal inputs are designed to fit into those reactive contexts without friction. Because they are signals, you can reference them directly inside expressions for computed values or within effect blocks, allowing for a more seamless data flow.
Here's a quick comparison between the classic input pattern and the new signal-based variant:
// Before (using @Input)
@Component({/* ... */})
export class Checkbox {
@Input() disabled = false;
@Input({required: true}) checked!: boolean;
}
// After (using signal inputs)
@Component({/* ... */})
export class Checkbox {
disabled = input(false);
checked = input.required<boolean>();
}
3. Outputs: Sticking with @Output for Event Emission
Even with the new input options, outputs are intentionally left out of the signals pattern. The team has decided to keep the well-established @Output decorator for this purpose. That means outputs still aren't signal-based. The reasoning is to maintain a clear line between how data flows in—via inputs—and how events flow out through outputs.
The next example illustrates where the boundary stays:
// Before (mixed styles)
@Component({/* ... */})
export class Checkbox {
disabled = input(false);
@Output() toggled = new EventEmitter<boolean>();
}
// After (consistent style)
@Component({/* ... */})
export class Checkbox {
disabled = input(false);
// Signal-based style for outputs (proposed)
toggled = output<boolean>();
}
For emitting events, the process remains exactly the same, with the emit method doing the heavy lifting.
4. Model Inputs: Cutting the Boilerplate Out of Two-Way Binding
Two-way data binding has been a hallmark of Angular. The typical setup involves pairing an @Input with a matching @Output that uses a specific naming convention to link them together.
Model inputs simplify all of that. They let you define a single signal that acts as the source of truth, so there's no need to declare separate input and output properties for the same value. That cuts out a chunk of boilerplate and makes the intention behind the binding much more obvious.
To show how that cleans up the code, here's a side-by-side look:
// Before (using @Input and @Output)
@Component({
// ...
template: '<cool-checkbox [(checked)]="isAdmin" />'
})
export class Profile {
isAdmin = false;
}
// After (using model input)
@Component({/* ... */})
export class CoolCheckbox {
checked = model(false);
}
// After (using model input, with signal
// change in Profile component)
@Component({/* ... */})
export class Profile {
// Create a signal for isAdmin property
isAdmin = signal(false);
}
The result is a writable signal inside the component that you can update directly, with any changes propagating back out to the parent automatically.
Where Things Stand
Adopting these Signals features can make your codebase more readable, easier to maintain, and more responsive by design. Just remember that all of this is still in developer preview, so expect refinements and possibly breaking changes as the API continues to settle.
