SCROLL TO EXPLORE • SCROLL TO EXPLORE •
Developer happiness in <mark>Angular 19</mark>

Developer happiness in Angular 19

Developer happiness, aiming for the next level!

Frank Merema - 7 min read

Developer happiness in Angular 19

Angular 19 has just been released and there are many nice features to explore. This time they spent again a lot of effort improving developers experience and listening carefully to all the wishes coming from the community. Get ready for a deep-dive first in the Dev experience improvements.

Dev experience improvements

Let's start looking at which features actually help improve the developer experience in Angular 19.

Standalone

Starting from Angular 19 all components are now by default standalone and you need to explicitly say standalone: false to make them part of a module. When using the Angular CLI the ng update command will automatically add or remove the standalone flag based on the current status of the component. If it's standalone already, remove the flag. If a module component, add the flag with the value false.

Signals

Starting with looking at the current status of the signals API. We see that many APIs have been promoted to be stable, and some new kids on the block.

The following have reached the stable status:

  • Input
  • Model
  • Output
  • viewChild(ren)
  • contentChild(ren)

New & Experimental

With APIs reaching stable there is room for new APIs to take the stage, so let's take a look at the new kids.

LinkedSignal

You can use linkedSignal to create local state just like with Signal. The difference is that this state is linked to a computed expression which provides an initial value. You can write new values to linkedSignal just like a regular signal. But when the linked computed expression changes, the linkedSignal returns to that computed value. More info on linkedSignal at angular.dev/guide/signals/linked-signal

@Component({/* ... */})
export class ShippingMethodPicker {
  shippingOptions: Signal<ShippingMethod[]> = getShippingOptions();
  // Select the first shipping option by default.
  selectedOption = signal(this.shippingOptions()[0]);
  
  changeShipping(newOptionIndex: number) {
    this.selectedOption.set(this.shippingOptions()[newOptionIndex]);
  }
}
// Source Angular.dev

Resource API

The resource function lets you work with asynchronous values, such as data from your server as part of Angular's signal graph. When you create a resource you specify a computed expression that returns a request object. This expression produces a new value whenever its signal dependencies change. This request object then goes to the specified async loader function, which can use any data client or similar asynchronous API. Resources provide you both the result of the request and the resource status as signals, meaning they work seamlessly with the rest of Angular's reactivity system. More info on the resource API at angular.dev/guide/signals/resource

import {resource, Signal} from '@angular/core';

const userId: Signal<string> = getUserId();
const userResource = resource({
  // Define a reactive request computation.
  // The request value recomputes whenever any read signals change.
  request: () => ({id: userId()}),
  // Define an async loader that retrieves data.
  // The resource calls this function every time the `request` value changes.
  loader: ({request}) => fetchUser(request),
});
// Created a computed based on the result of the resource's loader function.
const firstName = computed(() => userResource.value().firstName);

// Source angular.dev

Route-level render mode

With Angular 19, the option to decide how the server should handle the rendering of a route is shipped in developer preview. By default, Angular will server-side render all the parametrized routes in your app and prerender all routes without parameters.

When defining routes you can choose to use the ServerRoute interface provided by the framework and specify how routes should be rendered.

export const serverRoutes: ServerRoute[] = [
  { path: '/home', mode: RenderMode.Client },
  { path: '/login', mode: RenderMode.Server },
  { path: '/**', mode: RenderMode.Prerender },
];

For the above code we will specify how different routes should be rendered. The home path should be rendered only on the client side, whereas the login route should always be rendered on the server side. All other routes will be pre-rendered More info on route-level render mode at angular.dev/guide/hybrid-rendering

New automatic refactoring tools!

The signal input migration: converts inputs using the decorator API to the new signal-based inputs.

ng g @angular/core:signal-input-migration

The signal queries migration: swaps queries like viewChild and ContentChild with their signal-based equivalent.

ng g @angular/core:signal-queries-migration

The output migration: migrates components to the new function-based Output API.

ng g @angular/core:output-migration

To run them all at once there is this magic script but run with caution! If you have a big project, it might impact code that you wouldn't expect, making it hard to debug and track the issues. When doing it one-by-one it's easier to track the changes and impact.

ng generate @angular/core:signals

All these features are adopted in the Angular Language Service. This lets you run refactoring actions on your code directly in your favorite IDE.

Angular Material & CDK updates

In the newest release, the theming system is significantly simplified. Resulting in less code for creating custom themes and better to maintain code that’s easier to understand. What used to take hundreds of lines can now be accomplished with just one call to mat.theme. This new API uses a handful of application-wide CSS variables to control the styles of all the Angular Material components. It enables you to change styles at the application level by using mat.theme override mixin.

@use '@angular/material' as mat;

html {
  @include mat.theme((
    color: (
      primary: $fv-orange,
    ),
  ));
}

When you need even more fine-grained control, you have mixins available for all individual Angular Material components.

html {
  @include mat.card-overrides((
    elevated-container-color: red,
    elevated-container-shape: 32px,
    title-text-size: 2rem,
  ));
}

Maybe you want to create your own theme? A new theme color schematic is available which generates your own color palette. Also new is this beginner-friendly theming guide, providing a clear and easy path covering how to theme your applications. More info on More info on theming your apps at material.angular.io

ng g @angular/material:color-theme

One small side note: if you want to use all these awesome new features, you need to be on Angular 19 and using Material 3.

Finally, after ages there is this brand new time-picker component, ready to be used.

Angular Material Time Picker

Incremental Hydration (developer preview)

Let's start with a small look back on the previous releases before deep-diving into Incremental Hydration:

  • Angular v16 - full-application hydration: This ensures your server-side-rendered HTML is reused when your application bootstraps on the client side loading all necessary JavaScript.
  • Angular v17 - deferrable view @defer blocks: Angular native primitive for declaratively defer loading chunks of your application and therefore additional JavaScript making the initial bundle smaller
  • Angular v18 - Event replay: Event Replay ensures none of your user’s interactions are lost before the hydration phase finishes

Angular v19 - Incremental Hydration

Full application hydration requires your application to be hydrated all at once, which also means that the JavaScript that supports the HTML is also present. The Deferrable views reduce some of that JavaScript, but the trade-off is that the rendered placeholder block content is different from the eventual main content.

So why incremental Hydration?

To render the contents on the server instead of the placeholder would mean we still need to fetch all JavaScript needed to render the contents and that negates the entire purpose of defer blocks. Incremental Hydration beats this challenge by rendering the template on the server side to look how it should after the defer block is triggered. Then, on the client side, leave that content dehydrated, skipping the need to fetch the JavaScript.

How does it work?

Defer blocks have a set of triggers, which you’re already probably familiar with — immediate, idle, timer, interaction, viewport and hover. So in your template you can add @defer and add one of these triggers. This trigger specifies when the deferrable view dependencies are fetched. Once they’ve been fetched the placeholder content is replaced by the main content. Incremental Hydration builds on this functionality and adds a new set of triggers, Hydrate Triggers. These are exactly the same as the ones you’re already familiar with, except one new trigger: never.

@defer (hydrate on viewport) {
  <large-cmp />
} @placeholder {
  <div>Large component placeholder</div>
}

This signals to the server that the main block should be rendered and not the placeholder, resulting in the server fetching the defer block dependencies. Specifically, do all of this on the server but not on the client. Angular will leave the block dehydrated until the hydrate trigger fires. So it’s loaded eagerly on the server, but it’s deferred on the client. That results in your client-side JavaScript bundle being much smaller and faster to bootstrap while also having no temporary placeholder block contents. Finally, when your content hydrates, there’s no layout shift or flickering that occurs, just like full-application hydration.

Knowing this, one question pops up: “what happens if a user wants to interact with the content that’s dehydrated?”. Here Event Replay comes into play. Event Replay captures events and queues them up to be replayed later. This ensures that all interactions with the dehydrated content are captured, trigger hydration, get replayed when your content is ready and none of your user actions are left behind.

We already briefly touched the brand new never trigger, lets check this one out a bit more. There may be cases where you have content that doesn’t need to be hydrated, like static content from a blog. It ensures that the content inside that block never triggers hydration and, because of that, never needs to ship any JavaScript on the initial load. This gives you a new, great way to reduce the overhead of the static content in your apps. One small note: during a subsequent client-side render, a @defer block with hydrate never would still fetch dependencies, as hydration only applies to initial load of server-side rendered content. To start using it, just add withIncrementalHydration() to your provideClientHydration() call and you're all set. More info on incremental hydration at angular.dev/guide/incremental-hydration

Final thoughts

I'm really looking forward playing around with all these new features and seeing how the automatic schemas will transform my app into a full standalone, signal based application in a breeze! Let's hope there will be more awesome new features that land in the Angular ecosystem in the coming years!

Photo of Frank Merema

Frank Merema

Principal Engineer