Since the release of Angular 14, it is undeniable that each version brings a lot of new features, and Angular 16 will not be the exception.
For a while now we've been hearing about the integration of the Signals pattern in Angular, but Angular 16 brings much more than this novelty.
What are they? How to implement them?
Everything you want to know is here!
Signals
The Signal pattern is in place since the beginning in the Solid Js library and is based on the push/pull pattern.
As the name suggests, pull allows you to retrieve the value and push allows you to set a new one or to mutate it.
Anyway a signal always has a value and the retrieval of this value is always done synchronously.
In version 16 of Angular, the pattern base API will be integrated and will allow to get more performance in the way Angular handles change detection.
The question is why?
At the start of an Angular application, the framework overrides some of the browser's low-level APIs like the addEventListner function.
This override is done using the Zone JS library. Zone is a library that Angular uses under the hood to trigger a change detection by listening to
- DOM events like click, mouseover...
- HTTP requests
- setTimeout, setInterval functions
So when something changes in your application, a change detection is performed and your page is refreshed.
Like any application created with a client side rendering framework, your application is materialized by a component tree.
In Angular, each component is associated with a change detector. So it becomes logical that if something changes in one of the child components, the whole tree is re-evaluated without even taking into account the dependencies of each component. We call this dirty checking.
Even if your application's change detection is in OnPush mode, the change detection cycle will go through the entire tree, unlike the default mode, OnPush components with no change dependencies will not be re-evaluated.
Therefore it is clear that the change of detection in Angular is not optimal, and it is in order to solve this problem that the integration of Signal will help.
With Signal, the granularity of the change detection level is done at the level of the signal variable. Thus a change detection will be done at the component level only when the signal changes without the need to traverse the whole tree and without the need of Zone JS. In the future, Zone Js could be optional.
Creating a signal in Angular is easy, just call the signal function with an initial value.
const counter = signal(0) // create a signal with initial value to 0;
console.log(this.counter()) // display 0
The signal function returns a WritableSignal that allows us to mutate or set a new value to our signal.
/**
Set function let to set a signal's value with a new value. Usefull if you need to change the data structure when the new value is not dependent of the old one.
Notify all dependents.
**/
set(value: T): void;
/**
Update function let to update the signal's value if the update depends on the precedent value.
In other words this function help if you want to update the value of the signals in a immutable way.
Notify all dependents.
**/
update(updateFn: (value: T) => T): void;
/**
Mutate function let to update the signal's value by mutating it in place.
In other words this function is useful for making internal change to the signal's value without changing its internal identity.
Notify all dependents.
**/
mutate(mutatorFn: (value: T) => void): void;
/**
Return a readonly signal.
**/
asReadonly(): Signal<T>;
Computed
The calculated signals allow to create derived signals from other dependency signals.
const person = signal<{ firstname: string; lastname: string}>({ firstname: 'John', lastname: 'Doe'})
// Automatically updates when person() change;
const presentation = computed(() => ${person().firstname}${person().lastname}`;
A calculated signal is only re-evaluated if one of the dependent signals has changed
Computed can be a replacement of the famous Pipe.
Effect
The effects make it possible to carry out operations of side effects which read the value of zero or more signals, and is automatically scheduled to be re-run whenever any of those signals changes.
The Api is designed as follows:
function effect(
effectFn: (onCleanup: (fn: () => void) => void) => void,
options?: CreateEffectOptions
): EffectRef;
A concrete example can be the following
query = signal('');
users = signal([]);
effect(async (onCleanup) => {
const controller = new AbortController();
const response = await fetch('/users?query=' + query())
users.set(await response.json());
onCleanup(() => controller.abort())
})
Signals are a primitive reactive system in Angular. This means that they can be used in the component but also outside, for example in services.
Automatic route params mapping
Let's imagine a routing like this:
export const routes: Routes = [
{ path: 'search:/id',
component: SearchComponent,
resolve: { searchDetails: searchResolverFn }
}
]
Before Angular 16, it was mandatory to inject the ActivatedRoute service to retrieve the url parameter but also the query parameters or the data associated to this url.
@Component({...})
export class SearchComponent {
readonly #activateRoute = inject(ActivatedRoute);
readonly id$ = this.#activatedRoute.paramMap(map(params => params.get('id');
readonly data$ = this.#activatedRoute.data.(map(({ searchDetails }) => searchDetails)
}
With Angular 16 it will no longer be necessary to inject the ActivatedRoute service to retrieve the various route parameters because these can be directly binded to component inputs.
To activate this functionality, for an application that uses the module system, the option to activate is in the RouterModule options.
RouterModule.forRoot(routes, { bindComponentInputs: true })
For a standalone application, this is a function to call.
provideRoutes(routes, withComponentInputBinding());
Once the functionality is activated, the component is very simplified.
@Component({...})
export class SearchComponent {
@Input() id!: string;
@Input() searchDetails!: SearchDetails
}
Required Input
A much-awaited feature for the community was the ability to make certain inputs required.
Until now, there have been several workaround used to achieve this:
- raise an error in the NgOnInit Lifecycle if the variable was not defined
- modify the selector of our component to include the different inputs that were mandatory.
These two solutions had their advantages but also their disadvantages.
From version 16, making an input required will be a simple configuration object to be passed to the metadata of the input annotation.
@Input({ required: true }) name!: string;
New DestroyRef injector
Angular v16 has introduced a new provider called DestroyRef, which allows for registering destroy callbacks for a specific lifecycle scope. This feature is applicable to components, directives, pipes, embedded views, and instances of EnvironmentInjector.
This usage is quite simple.
@Component({...})
export class AppComponent {
constructor() {
inject(DestroyRef).onDestroy(() => {
// Writte your cleanup logic
})
}
}
With this new provider, Angular allows us to share some classic cleaning logic such as the unusubscription of our observables.
export function destroyed() {
const replaySubject = new replaySubject(1);
inject(DestroyRef).onDestroy(() => {
replaySubject.next(true);
replaySubject.complete();
});
return <T>() => takeUntil<T>(replaySubject.asObservable());
}
@Component({...})
export class AppComponent {
readonly #untilDestroyed = untilDestroyed();
ngOnInit() {
interval(1000)
.pipe(this.#untilDestroyed())
.subscribe(console.log);
}
}
Vite as Dev Server
With the arrival of Angular 14 has been introduced the possibility to use a new Javascript Bundler: EsBuild
This new Bundler has the ability to be very fast and could reduce the build time by about 40%. The main problem is that this new feature and performance gain could only be used for a build and not during development (dev server).
In the next release of Angular, Esbuild can also be used during development thanks to Vite.
To activate this feature in the angular.json file update the builder like this:
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser-esbuild",
"options": { ... }
Attention this functionality is still experimental.
Non Destructive hydration
Angular allows you to create server side rendering applications with the help of Angular Universal.
The problem was that in general we could find ourselves creating applications that were not very efficient, mainly due to the hydration.
Until now hydration was destructive. That is to say that the whole page was destroyed and then completely rebuilt and rendered once the browser had recovered the Javascript and executed it.
The good news is that the apis have been rewritten to allow partial hydration. That is to say that once the html is loaded and the DOM is done, the whole structure will be traversed to attach the different event listener and recreate the state of the application to make the application reactive but without rendering it a new time.
Conclusion
The version 16 of Angular brings without any doubt some nice new features. Some of them are still experimental like signals or vite like dev server.
Anyway these new features will undoubtedly change the way we code our Angular applications by making them less boilerplate, even more optimized and by opening the door to the integration of new technologies like vitest or playwright in a simpler way.
The version 16 of Angular is not yet released some api described in this article may still change. Nevertheless this gives you an idea of what you can expect from the next release of Angular.
Top comments (5)
Сongratulations 🥳! Your article hit the top posts for the week - dev.to/fruntend/top-10-posts-for-f...
Keep it up 👍
maybe it was changed meanwhile, the property to allow automatic router binding is bindToComponentInputs instead of bindComponentInputs
nice overview, anyway ! :)
Modern TypeScript is a nightmare in terms of syntax... More and more vanilla is added and IMHO it's getting more and more unreadable!
What makes things worse - it's up to a developer to write code in this or other way...
For instance, we're not obliged to supply a function return type. So when in the end we return some value - it might takes a while to figure out, what the heck is this object (JavaScript frameworks or libraries with its own "cartride-clips" of functions on top - like Angular or RxJs - just make the situation even worse):
return <T>() => takeUntil<T>(replaySubject.asObservable());
I appreciate the efforts of TypeScript to make JavaScript look like an object-oriented language. But I hate its hundreds counterintuitive quirk constructions (like "magic" manipulations with types) with more and more coming. I don't think it's possible to remember even half of them.
As a team coming from heavy object oriented codebases, it's easy to mixup types vs objects. I think objects are convoluted in typescript because they are convoluted in javascript. I love the move to options api by angular and vuejs; you get types and the safety around them without OOP. Atleast it makes it easier for us oop folks to not fall in the trap of feeling like we have create objects.
chin up comrade, hope is nigh!
Well this is awesome release.