Angular 17 is a Completely Different Framework
Yes, Angular went through a big overhaul in the past couple of years, and the new v17 release is the result of all that work. The promise is a modern dev experience and better performance, so let’s spend the next few minutes reviewing all the new features Angular has to offer.
After all, this is one of the most popular frontend frameworks, and it is backed by a large team of very talented developers. If this team can finally nail the dev experience, and bring it closer to the modern standards, Angular could become a really powerful tool moving forward.
Setup
So let’s install the most recent CLI version, and initialize a new project.
npm install -g @angular/cli@next
ng new angular17 --ssr
The main.ts file is the entry point into our app, where we simply bootstrap the Angular application and mount an App Component to the DOM.
// src/app/main.ts
import { AppComponent } from "./app/app.component";
bootstrapApplication(AppComponent, {
providers: [provideClientHydration()],
}).catch((err) => console.error(err));
Next, I am creating a component, which, in Angular, is a plain JavaScript class annotated with a decorator.
// src/app/app.component.ts
@Component({
selector: "app-root",
standalone: true,
templateUrl: "./app.component.html",
})
export class AppComponent {}
The industry offers various options to define components, from React’s function based approach to Svelte single file components. Angular however sticks with the more traditional class based approach, and with independent templating and styling files.
State management
Ok, it’s time now to use one of the most exciting Angular features, an addition which makes a huge difference when it comes to reactivity and performance. Managing state is a big task in SPAs, and, again, the industry experimented a lot with this one during the years. Signals are pretty much universally accepted as THE solution for the state problem these days.
// src/app/app.component.ts
@Component({
selector: "app-root",
standalone: true,
templateUrl: "./app.component.html",
})
export class AppComponent {
users = signal<User[]>([]);
}
This new implementation is lightweight and concise compared to the previous Change Detection strategy Angular was employing.
Then, we’ll use the “onInit” lifecycle hook, to fetch some data from a backend service and populate our users signals.
// src/app/app.component.ts
@Component({
selector: "app-root",
standalone: true,
templateUrl: "./app.component.html",
})
export class AppComponent {
users = signal<User[]>([]);
ngOnInit() {
fetch("https://jsonplaceholder.typicode.com/users")
.then((resp) => resp.json())
.then(this.users.set);
}
}
Built-in control flow
With the data available, let’s jump into the HTML template, and look at another big addition in the framework.
<!-- src/app/app.component.html -->
<div>
@for (user of users(): track user.id) {
<h3>{{user.name}}</h3>
@if (!$last) {
<hr />
} } @empty {
<p>No users found!</p>
}
</div>
Angular has used structural directives (think of ngIf or ngFor) for templating purposes since its early days. However, the entire control flow implementation had to be revised to support the newly added fine-grained signals reactivity, and the team decided to make the best of this situation. As a result, we can now use a new built-in control flow, which stands out clearly from the rendered markup.
We are iterating through the list of users, and rendering a name in the DOM. Note that you are forced to use the track statement so that Angular can optimize DOM updates when the underlying list changes. Also, inside the for construct you’ll have access to helper variables like $index or $last.
Besides the usual @fors and @ifs, Angular 17 introduces the @defer construct which gives you full control over lazy loading parts of your application. This is a big step forward towards better performance, but, before looking at it in more detail, let’s allow users to push data in our list.
Reactive forms
Back in the component file I am adding a boolean signal, based on which, in the template, I’m conditionally rendering a toggle button and a form.
// src/app/app.component.ts
@Component({
selector: "app-root",
standalone: true,
templateUrl: "./app.component.html",
})
export class AppComponent {
users = signal<User[]>([]);
adding = signal(false);
ngOnInit() {
fetch("https://jsonplaceholder.typicode.com/users")
.then((resp) => resp.json())
.then(this.users.set);
}
}
<!-- /app/app.component.html -->
<div>
@for (user of users(); track user.id) {
<h3>{{user.name}}</h3>
@if (!$last) {
<hr />
} } @empty {
<p>No users found!</p>
} @if (adding()) {
<form [formGroup]="form" (ngSubmit)="save()">
<button type="text" formControlName="name" />
<button>Save</button>
</form>
} @if (!adding()) {
<button (click)="adding.set(true)">Add</button>
}
</div>
This gives us the chance to use one of the most powerful Angular features - reactive forms. Let’s bind this form to a value in the component, and define a handler for the submit event.
Back in the ts file, we first need to import the ReactiveFormsModule in our component. Then, let’s define a FormGroup with a required FormControl.
// src/app/app.component.ts
@Component({
selector: 'app-root',
standalone: true,
templateUrl: './app.component.html',
Imports: [ReactiveFormsModule]
})
export class AppComponent {
users = signal<User[]>([]);
adding = signal(false);
form = new FormGroup({name: new FormControl(‘ ’, Validators.required)});
ngOnInit() {
fetch('https://jsonplaceholder.typicode.com/users')
.then(resp => resp.json())
.then(this.users.set);
}
}
Finally, in the save method, when the form is submitted we are pushing the new value in the users signal.
// src/app/app.component.ts
@Component({
selector: 'app-root',
standalone: true,
templateUrl: './app.component.html',
Imports: [ReactiveFormsModule]
})
export class AppComponent {
users = signal<User[]>([]);
adding = signal(false);
form = new FormGroup({name: new FormControl(‘ ’, Validators.required)});
ngOnInit() {
fetch('https://jsonplaceholder.typicode.com/users')
.then(resp => resp.json())
.then(this.users.set);
}
save() {
this.users.update(old =>
[...old, { id: 1, name: this.form.value.name ?? "" }]);
}
}
Now, let’s discuss the rendering strategies aimed to improve the performance of your app.
Rendering strategies
First, it is worth mentioning that starting with v17, you can create a SSR enabled app with this simple command.
npm install -g @angular/cli@next
ng new angular17 --ssr
The real power however is brought by the new @defer block.
<!-- src/app/app.component.html -->
@defer (on hover){
<div>
@for (user of users(); track user.id) {
<h3>{{user.name}}</h3>
@if (!$last) {
<hr />
} } @empty {
<p>No users found!</p>
} @if (adding()) {
<form [formGroup]="form" (ngSubmit)="save()">
<button type="text" formControlName="name" />
<button>Save</button>
</form>
}
</div>
@if (!adding()) {
<button (click)="adding.set(true)">Add</button>
} } @placeholder {
<p>Users</p>
}
With this one you can easily lazy load parts of your application for better initial user experience and overall improved performance.There are a handful of triggers which will cause deferred sections to be rendered, and the block can be paired with a @placeholder to display some dummy content until the real data is rendered.
While Angular is making big steps forward, I get it might not be your cup of tea. If that’s the case, here are some more recent UI frameworks (SolidJS & HTMX) which are making big waves in the industry.
Until next time, thank you for reading!