Signals of Change: How Angular is seriously delivering on DevEx (Part 1)

The world of front end frameworks moves quickly. New technologies emerge, paradigms shift, and what was once the cutting edge quickly becomes yesterday's news. In this ever-changing environment, Angular, one of the pioneering front-end frameworks, has certainly had its peaks and valleys.

Angular has faced its fair share of criticism over the years. Developers have expressed negative views on various aspects of the framework:

"Angular feels old. It seems tried and tested but it's years behind React in terms of innovation and Quality of Life features."

"There are too many ways to do the same thing. It's hard to understand what the best or recommended approach is."

"RxJS is not intuitive...and how the heck do you test an Observable anyway?"

And these are just a few of the complaints. The list goes on.

It appears that the Angular team has been listening and has been hard at work addressing these concerns. In the past year, they've released a slew of new features and improvements. In this article, we'll take a look at some of the most significant changes and how they're making Angular a more enjoyable framework to work with. Hopefully the information here will provide you with a fresh perspective on Angular and inspire you to give it another look.

Introducing Signals: A Reactive Programming Model That Just Makes Sense

Reactive programming, a type of event-driven programming, is a core concept in modern applications, but it can often be a source of confusion and complexity. Up until recently, reactivity in Angular applications was primarily handled by RxJS. The library provides a rich set of tools for handling reactivity, covering many use cases. It's a great library, however, it's not without its issues. Most notably, it's not the most intuitive library to use, and testing Observables can be challenging. It's also easy to misuse, which can lead to performance issues.

A Simpler Method of Handling Reactive Programming

Angular Signals introduces a straightforward approach to handling reactivity. At its core, Signals are just like functions, providing an easy way to access and update data. Consider this example:

import { signal } from '@angular/core'

const name = signal('John Doe')

console.log(name()) // John Doe

name.set('Jane Doe')

console.log(name()) // Jane Doe

Signals are similar to functions; you call them to retrieve the current value. However, unlike functions, Signals are reactive. If the data they depend on changes, they automatically update. This is due to a some intelligent behind-the-scenes magic that Angular Signals perform. Let's take a quick look at the interface for a Signal:

type Signal<T> = (() => T) & {
  [SIGNAL]: unknown
}

As you can see, a signal is just a function that returns a value. The SIGNAL property is a unique symbol that allows the framework to identify Signals and perform the necessary handling an optimizations.

To change a Signal's value, you use specific methods like set, update, and mutate, ensuring a controlled approach to modifying data. In another blog post, we'll take a closer look at these methods, how they work, and patterns for using them.

Deriving Data is Simple

Angular Signals simplifies data derivation by allowing you to create computed Signals that depend on other Signals. These computed Signals update automatically when their dependencies change:

import { computed, signal } from '@angular/core'

const distanceInMiles = signal(5)
const distanceInKilometers = computed(() => distanceInMiles() * 1.609)

console.log(distanceInKilometers()) // 8.045

distanceInMiles.set(10)

console.log(distanceInKilometers()) // 16.09

This straightforward mechanism makes deriving data a breeze. Did you notice that we don't have to explicitly track dependencies when computing our distanceInKilometers? Angular Signals automatically detect dependencies and update the computed Signal when they change.

Conventional Side-Effects

Angular Signals promotes a conventional approach to handling side effects. You can use the effect function to define side effects that run when specific Signals change. For example, persisting data to local storage is as simple as this:

import { effect, signal } from '@angular/core'

const dataToPersist = signal('Some data')

effect(() => {
  localStorage.setItem('myData', dataToPersist())
})

dataToPersist.set('Some new data') // the new value is automatically persisted to local storage

Whenever dataToPersist changes, it automatically gets saved to local storage.

In summary, Angular Signals really change reactive programming in Angular applications. They provide clear patterns for reading and writing data, and offer an effortless way to derive data and manage side effects. They are also fully interoperable with RxJS Observables (we'll take a look at this in a future post). Angular Signals make reactivity not just easier but also more intuitive.

Standalone Components and APIs: When Less Really is More

As of version 14, Angular has introduced standalone components. This feature addresses the long standing criticisms around NgModules, and how they add unnecessary complexity to applications. Standalone components allow you to create components without having to declare them in a module. This is a huge win for Angular developers, as it simplifies the component creation process and reduces the amount of boilerplate code. The new way to scaffold applications also shipped with APIs that allow you to bootstrap application and configure dependency injection without having to create an NgModule. Let's take a look at how these new features work.

// main.ts
import { bootstrapApplication } from '@angular/platform-browser'
import { AppComponent } from './app/app.component'

bootstrapApplication(AppComponent, {
  providers: [],
})

Angular now exposes a function called bootstrapApplication that allows you to bootstrap an Angular application with a root component. The second argument of this function is an options object that allows us to configure the application. In our example, we're passing in an empty array of providers. This providers array is where we can specify dependencies for the application injector. This is the equivalent of the providers array in an NgModule. Let's see how it works by setting up some routes for our application:

// main.ts
import { bootstrapApplication } from '@angular/platform-browser'
import { AppComponent } from './app/app.component'
import { ProfileComponent } from './app/profile/profile.component'
import { DashboardComponent } from './app/dashboard/dashboard.component'
import { SettingsComponent } from './app/settings/settings.component'
import { provideRouter } from '@angular/router'
import { Routes } from '@angular/router'

export const AppRoutes: Routes = [
  {
    path: 'profile',
    component: AppComponent,
  },
  {
    path: 'dashboard',
    component: DashboardComponent,
  },
  {
    path: 'settings',
    component: SettingsComponent,
  },
]

bootstrapApplication(AppComponent, {
  providers: [provideRouter(AppRoutes)],
})

As you can see, we're using the provideRouter function to register our routes. This function is the equivalent of the RouterModule.forRoot function in an NgModule, but much less verbose. This is just a small peek at some of the new APIs that Angular has introduced in order to support standalone components. Its important to highlight that these APIs are tree shakeable, so you only pay for what you use.

Standalone Components

I've buried the lede a bit here; the real stars of the show are standalone components. It's actually very easy to create a standalone component. Let's take a look at the code for our AppComponent:

// app.component.ts
import { Component } from '@angular/core'
import { Component } from '@angular/core'
import { RouterModule } from '@angular/router'
import { AccountOverviewComponent } from './account-overview/account-overview.component'

@Component({
  selector: 'app-root',
  standalone: true,
  template: `
    <div class="app">
      <nav>
        <a routerLink="/profile">Profile</a>
        <a routerLink="/dashboard">Dashboard</a>
        <a routerLink="/settings">Settings</a>
      </nav>
      <router-outlet></router-outlet>
    </div>
  `,
  imports: [RouterModule],
})
export class AppComponent {}

As you can see, the code is very similar to what you would expect in an Angular Component, but there are two key differences: the standalone and imports properties. The standalone property is a boolean that tells Angular that this component is standalone. The imports property is an array of the dependencies that this component requires. We declare them right in the component since we're not using an NgModule. We're using the RouterModule to provide the router-outlet and routerLink directives.

Tooling and scaffolding

The Angular CLI allows you to generate new projects that are pre-cofigured to use standalone components.

  ng new --standalone

There are also schematics to help make the migration to standalone components in existing projects easier:

ng generate @angular/core:standalone

The schematics apply transformations to your project to convert it to use standalone components, and remove any unnecessary NgModule code.

In summary, standalone components and APIs really simplify the development experience in Angular. They reduce boilerplate code, make it easier to create components, and provide a more intuitive way to bootstrap and configure applications.

Vite + esbuild: Speed kills, and also makes development more enjoyable

Webpack has largely dominated the front-end build tool space for the past several years. Angular leverages Webpack as a development server, and to build and bundle applications. Now that newer build tools with promises of faster builds and better developer experience have emerged, it's only natural that Angular developers would want to take advantage of them. Two of the most popular build tools that have emerged are esbuild and Vite, and Angular has recently added support for both.

esbuild

esbuild describes itself as "An extremely fast bundler for the web". It's build times are significantly fast when compared to popular bundlers like Webpack, Rollup and Parcel. This performance can be attributed to the fact that it's written in Go, a language known for its speed and concurrency, and esbuild's implementation of several optimizations to speed up parsing and bundling.

Vite

Vite is a front-end build tool developed by Evan You, the creator of Vue.js. It aims to improve the development experience with "Next Generation Frontend Tooling", and boasts features like instant server start, hot module replacement, and out-of-the-box support for TypeScript, JSX, CSS and more.

A quick benchmark

In order to assess the performance gains of esbuild and Vite, I decided to run a quick benchmark against Ismael Ramos' Angular Example App. The app isn't very large, but I think it's still a good enough test to demonstrate the performance gains. I ran ng build and ng serve commands with both Webpack and esbuild with a focus on build times. Here are the results:

As you can see from the results there is a significant difference in build times with a more than 60% reduction in time to completion for both ng build and ng serve commands.

Opting in to esbuild and Vite

As of Angular 16, esbuild and Vite are still in the developer preview phase. This means that they are not enabled by default, and you have to opt in to use them. Luckily, this is pretty easy to do via the angular.json config file. In order to opt-in, update your config file with the following:

{
  ...
  "architect": {
    "build": {
      "builder": "@angular-devkit/build-angular:browser-esbuild",
  ...
}

Conclusion

Angular has made strides in addressing some of the long standing criticisms of the framework. They've eliminated mental overhead by limiting the need for NgModules and shipping the APIs and tooling to support standalone components. They've made reactivity more intuitive with Angular signals. Lastly, they've made development more refreshing by adding support for esbuild and Vite.

This post is just the first in a two part series where we'll explore Angular's new features that address issues around the developer experience. If you're interested, please subscribe to the newsletter to get notified when the next post is published. You can also look forward to a deep dive into Angular Signals, and reactivity across all of the major front-end frameworks.


References

Angular Signal RFC
Angular Signal-based Components RFC
Angular 16 announcement
Angular 15 announcement
Angular 14 announcement
r/dotnet discussions: Angular or react?