Overview of the New Signal APIs in Angular
Google's Minko Gechev and Jeremy Elbourn announced many exciting things at NG Conf 2024. Among them is the addition of several new signal-based APIs. They are already in developer preview
, so we can play around with them. Let's dig into it, starting with signal-based inputs and the new matching outputs API.
Signal Based Inputs
Discussions about signal-based inputs have been taking place in the Angular community for some time now, and they are finally here.
Until now, you used the @Input()
decorator to define inputs. This is what you'd have to write in your component to declare an optional and a required input:
@Component(...)
export class MyComponent {
@Input() optionalInput = 'default value';
@Input({ required: true }) requiredInput: string;
}
}
With the new signal-based inputs, you can write much less boilerplate code. Here is how you can define the same inputs using the new syntax:
@Component(...)
export class MyComponent {
optionalInput = input('default value');
requiredInput = input.required<string>();
}
It's not only less boilerplate, but because the values are signals, you can also use them directly in computed signals and effects. That, effectively, means you get to avoid computing combined values in ngOnChanges
or using setters for your inputs to be able to compute derived values. In addition, input signals are read-only.
The New Output
I intentionally avoid calling them signal-based outputs because they are not. They still work the same way as the old outputs. The Angular team has just introduced a new output API that is more consistent with the latest inputs API and allows you to write less boilerplate, similar to the new input API.
Here is how you would define an output until now:
@Component(...)
export class MyComponent {
@Output() someEvent = new EventEmitter<string>();
}
Here is how you can define the same output using the new syntax:
@Component(...)
export class MyComponent {
someEvent = output<string>();
}
The thing I like about the new output API is that sometimes it happens to me that I forget to instantiate the EventEmitter
because I do this instead:
@Component(...)
export class MyComponent {
@Output() someEvent: EventEmitter<string>;
}
You won't forget to instantiate the output with the new syntax because the output
function does it for you.
Signal Queries
I am sure most readers know the @ViewChild
, @ViewChildren
, @ContentChild
, and @ContentChildren
decorators very well and have experienced the pain of triggering the infamous ExpressionChangedAfterItHasBeenCheckedError
or having the values unavailable when needed.
Here is a refresher on how you would use these decorators until now:
@Component(...)
export class MyComponent {
@ViewChild('someElement') someElement: ElementRef;
@ViewChildren('someElement') someElements: QueryList<ElementRef> | undefined;
@ContentChild(ChildComponent) someChild: ChildComponent;
@ContentChildren(ChildComponent) someChildren: QueryList<ChildComponent> | undefined;
}
With the new signal queries, similar to the new input API, the values are signals
, and you can use them directly in computed signals and effects. You can define the same queries using the new syntax:
@Component(...)
export class MyComponent {
someElement = viewChild('someElement');
someElements = viewChildren('someElement');
someChild = contentChild(ChildComponent);
someChildren = contentChildren(ChildComponent);
}
Jeremy Elbourn mentioned that the new signal queries have better type inference and are more consistent with the new input and output APIs. He also showcased a brand new feature not available with the old queries. You can now define a query as required
, and the Angular compiler will throw an error if the query has no result, guaranteeing that the value won't be undefined.
Here is how you can define a required query:
@Component(...)
export class MyComponent {
someElement = viewChild('someElement'); // Type of someElement is ElementRef | undefined
someElement = viewChild.required('someElement'); // Type of someElement is ElementRef
}
Model Inputs
Jeremy and Minko announced the last new feature is the model inputs. The name is vague, but the feature is cool—it simplifies the definition of two-way bindings.
Until now, to achieve two-way binding, you would have to define an input and an output following a given naming convention. @Input
and @Output
had to be defined with the same name (followed by "Change" in the case of the output). Then, you could use the template's [()]
syntax.
@Component(...)
export class MyComponent {
@Input() value: string;
@Output() valueChange = new EventEmitter<string>();
}
<child-component [(value)]="someValue"></child-component>
That way, you could keep the value in sync between the parent and the child component. With the new model inputs, you can define a two-way binding with a single line of code. Here is how you can define the same two-way binding using the new syntax:
@Component(...)
export class MyComponent {
value = model('default value');
}
The html template stays the same:
<child-component [(value)]="someValue"></child-component>
The model function returns a writable signal that can be updated directly. The value will be propagated back to any two-way bindings.
Conclusion
The new signal-based APIs are a great addition to Angular. They allow you to write less boilerplate code and make your components more reactive. The new APIs are already in developer preview, so you can start playing around with them today. I look forward to seeing how the community will adopt these new features and what other exciting things the Angular team has in store for us, such as zoneless apps by default.