Welcome to Angular 19! This latest release brings a wealth of new features and enhancements designed to streamline development and improve performance. From innovative reactive primitives like linkedSignal and the resource API to experimental Incremental Hydration and enhancements in Angular Language Service, Angular 19 is packed with tools to make your applications faster and more efficient. Dive into our comprehensive overview to discover all the exciting updates and learn how they can elevate your projects to the next level.
New (experimental) reactive primitive: linkedSignal
The linkedSignal is a writable signal that responds to changes in a source signal and can reset itself based on a computed value.
export declare function linkedSignal<S, D>(options: {
source: () => S;
computation: (source: NoInfer<S>, previous?: {
source: NoInfer<S>;
value: NoInfer<D>;
}) => D;
equal?: ValueEqualityFn<NoInfer<D>>;
}): WritableSignal<D>;
The initial signal value is calculated using the 'computation’ function, then the signal value can be changed manually using the 'set’ method, but when the 'source’ signal value changes, the linked signal value will be recalculated again using the 'computation’ method.
Let’s look at an example that should make this clear:
protected readonly colorOptions = signal<Color[]>([{
id: 1,
name: 'Red',
}, {
id: 2,
name: 'Green',
}, {
id: 3,
name: 'Blue',
}]);
protected favoriteColorId = linkedSignal<Color[], number | null>({
source: this.colorOptions,
computation: (source, previous) => {
if(previous?.value) {
return source.some(color => color.id === previous.value) ? previous.value : null;
}
return null;
}
});
protected onFavoriteColorChange(colorId: number): void {
this.favoriteColorId.set(colorId);
}
protected changeColorOptions(): void {
this.colorOptions.set([
{
id: 1,
name: 'Red',
},
{
id: 4,
name: 'Yellow',
},
{
id: 5,
name: 'Orange',
}
])
}}
We have signal colorOptions, which store a list of user-selectable colors (each with an ID and name). We also have a linked signal called favoriteColorId, which represents the user’s selected color from the list. The initial value of this signal is the result of the 'computation’ function, which will be null (because the previous state of the linked signal is not defined). Linked signal, like any other writable signal, provides a set method to set the id of the user’s chosen color (see onFavoriteColorChange function). Suppose that after selecting a color for some reason, the list of colors available for selection is changed (see changeColorOptions method). As a result of changing the value of the colorOptions signal, the value of the linked signal favoriteColorId is recalculated using the computation method. In the example above, if the selected color is also in the new list of available colors, the signal value remains the same. On the other hand, if the previously selected color is not in the new list, the value is set to null.
New (experimental) API: resource
Angular is introducing an experimental API called resource(), designed to manage asynchronous operations. It has built-in mechanisms to prevent race conditions, track loading state, handle errors, update the value manually, and trigger data fetching manually as needed.
Below is an example of resource usage:
fruitId = signal<string>('apple-id-1');
fruitDetails = resource({
request: this.fruitId,
loader: async (params) => {
const fruitId = params.request;
const response = await fetch(`https://api.example.com/fruit/${fruitId}`, {signal: params.abortSignal});
return await response.json() as Fruit;
}
});
protected isFruitLoading = this.fruitDetails.isLoading;
protected fruit = this.fruitDetails.value;
protected error = this.fruitDetails.error;
protected updateFruit(name: string): void {
this.fruitDetails.update((fruit) => (fruit ? {
...fruit,
name,
} : undefined))
}
protected reloadFruit(): void {
this.fruitDetails.reload();
}
protected onFruitIdChange(fruitId: string): void {
this.fruitId.set(fruitId);
}
Let’s start with the declaration of the resource. The optional request parameter accepts the input signal to which the asynchronous resource is linked (in our example, it is fruitId, but it could just as well be a computed signal consisting of multiple values). We also define the loader function, with which we asynchronously download data (the function should return promise). The created resource named fruitDetails allows us to, among other things:
- access the current value signal (which also returns undefined when the resource is not available at the moment),
- access the status signal (one of: idle, error, loading, reloading, resolved, local),
- access extra signals like ‘isLoading’ or ‘error’,
- trigger ‘loader’ function again (using ‘reload’ method),
- update local state of the resource (using ‘update’ method)
The resource will be automatically reloaded if the 'request’ signal (in our case fruitId) changes. The loader is also triggered when the resource is first created.
What about RxJS Interop? Angular also provides a RxJS counterpart of resource method called rxResource. In this case, the loader method returns Observable, but all other properties remain signals.
fruitDetails = rxResource({
request: this.fruitId,
loader: (params) => this.httpClient.get<Fruit>(`https://api.example.com/fruit/${params.request}`)
})
Updates to the effect() function
In the latest version of Angular, version 19, the effect() function has received pivotal updates based on extensive community feedback.
A significant change is the elimination of the allowSignalWrites flag. Originally, this flag was intended to limit when signals could be set within effect(), pushing developers towards using computed() for certain scenarios. However, it became clear that this restriction was more often a hindrance than a help, preventing the effective use of effect() where it made sense. In response, Angular 19 will allow signals to be set by default within effect(), removing unnecessary complexity and focusing on better ways to support good coding practices (see linkedSignal, Resource API).
effect(
() => {
console.log(this.users());
},
//This flag is removed in the new version
{ allowSignalWrites: true }
);
Additionally, there’s a major shift in the timing of when effects are executed. Moving away from the previous approach of queuing them as microtasks, effects will now be executed as part of the change detection cycle in the component hierarchy. This adjustment aims to fix issues with effects running too early or too late and ensures a more logical execution order aligned with the component tree.
These improvements are designed to enhance both the functionality and usability of the effect() function, making it more in tune with developers’ needs. Although effect() will continue in the developer preview phase in version 19, this allows for further refinements based on developer experiences with these new features.
New equality function in rxjs-interop
Angular’s toSignal function has been enhanced to support a custom equality function, providing developers more control over how value comparisons trigger updates. Previously, toSignal operated with a basic equality check, lacking the flexibility for developers to define what constitutes equality for their specific scenarios. This often resulted in unnecessary component updates.
With the recent update, developers can now specify a custom equality function that determines when updates should occur, optimizing performance by ensuring that updates are only triggered by meaningful data changes. This new feature not only allows for tailored value comparisons but also standardizes the use of an equality check where it was previously absent, making the behavior of signals more predictable and efficient.
// Create a Subject to emit array values
const arraySubject$ = new Subject<number[]>();
// Define a custom equality function to compare arrays based on their content
const arraysAreEqual = (a: number[], b: number[]): boolean => {
return a.length === b.length && a.every((value, index) => value === b[index]);
};
// Convert the Subject to a signal with a custom equality function
const arraySignal = toSignal(arraySubject$, {
initialValue: [1, 2, 3],
equals: arraysAreEqual, // Custom equality function for arrays
});
New afterRenderEffect function
The afterRenderEffect function in Angular is an experimental API designed to handle side effects that should only occur after the component has finished rendering. The effect runs after each render cycle if its dependencies change, allowing developers to react to state changes only after the DOM is updated.
In contrast to ‘afterRender’ and ‘afterNextRender’, this effect tracks specified dependencies and re-executes them after every render cycle whenever they change, making it ideal for ongoing post-render tasks tied to reactive data.
‘afterRender’ and ‘afterNextRender’ do not track any dependencies and always schedule a callback to run after the render cycle.
counter = signal(0);
constructor() {
afterRenderEffect(() => {
console.log('after render effect', this.counter());
})
afterRender(() => {
console.log('after render', this.counter())
})
}
In the given example, the afterRender callback will be executed after each render cycle. The afterRenderEffect callback, on the other hand, will be executed after rendering cycles only if the value of the signal counter has changed.
New template variable syntax @let
Angular introduced this @let syntax in 18.1, and made it stable in 19.0. This new feature simplifies the process of defining and reusing variables within templates. This addition addresses a significant community request by enabling developers to store results of expressions without the previous workarounds that were less ergonomic.
Here’s how you can utilize the @let syntax in your Angular templates:
@let userName = 'Jane Doe';
<h1>Welcome, {{ userName }}</h1>
<input #userInput type="text">
@let greeting = 'Hello, ' + userInput.value;
<p>{{ greeting }}</p>
@let userData = userObservable$ | async;
<div>User details: {{ userData.name }}</div>
@let allows for defining variables directly in the template, which can then be reused throughout that template. Remember that variables defined with @let are read-only and scoped to the current template and its descendants—they cannot be reassigned or accessed from parent or sibling components. This scoped and immutable nature ensures that templates remain predictable and easier to debug.
Experimental Incremental Hydration
Following Defferable views in v17 and event replay in v18, the Angular Team presents a preview of Incremental Hydration (a new way to hydrate parts of the application on demand).
To enable it, add it to the application configuration:
export const appConfig: ApplicationConfig = {
providers: [
provideClientHydration(
withIncrementalHydration()
)
...
]
};
The implementation of incremental hydration is built on top of defer block. When we want to utilize it, we have to add new hydrate trigger to it.
@defer (hydrate on hover) {
<app-hydrated-cmp />
}
The following incremental hydration trigger types are supported:
- idle,
- interaction,
- immediate,
- timer(ms),
- hover,
- viewport,
- never (component will stay dehydrated indefinitely),
- when {{ condition }}
Allowing parts of a server-rendered application to be selectively rehydrated on the client improves load times and interactivity by only activating necessary components initially.
New routerOutletData input for RouterOutlet
With Angular 19, a new routerOutletData input has been added to RouterOutlet, providing a streamlined way for parent components to send data to their child components routed through the outlet. When routerOutletData is set, the associated data becomes accessible in child components through the ROUTER_OUTLET_DATA token, which employs a Signal type. This design allows for dynamic updates, ensuring that changes in the input data automatically reflect within the child component, eliminating the need for static assignments.
Parent component:
<router-outlet [routerOutletData]="routerOutletData()" />
Child component routed through the outlet:
export class ChildComponent {
readonly routerOutletData: Signal<MyType> = inject(ROUTER_OUTLET_DATA);
}
RouterLink accepting UrlTree
As of version 18.1, the RouterLink directive input also accepts an object of type UrlTree.
<a [routerLink]="homeUrlTree">Home</a>
In this way, all additional options (such as query params, query params handling strategy, relativeTo etc.) can be passed directly in the UrlTree object.
When attempting to pass an UrlTree object via a routerLink input while using other inputs such as queryParams, or fragment, Angular will throw an error explaining that this is not allowed:
'Cannot configure queryParams or fragment when using a UrlTree as the routerLink input value.'
Default query params handling strategy
You can now set a default query parameter handling strategy for all routes directly in the provideRouter() configuration.
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(routes, withRouterConfig({defaultQueryParamsHandling: 'preserve'}))
]
};
While Angular’s default is the replace strategy, you can choose preserve or merge. Before this, the strategy could only be set individually for each navigation, either through RouterLink or router.navigate options.
Standalone by default
With the release of Angular v19, standalone: true will become the default setting for components, directives, and pipes in Angular.
The following component will be considered a standalone:
@Component({
imports: [],
selector: 'home',
template: './home-component.html',
// standalone in Angular 19!
})
export class HomeComponent {…}
For non-standalone components, an explicit flag has to be provided:
@Component({
selector: 'home',
template: './home-component.html',
standalone: false
// non-standalone in Angular 19!
})
export class HomeComponent {…}
This marks a significant evolution from v14’s introduction of standalone capabilities. This change simplifies the Angular framework, making it more accessible to new developers and enhancing features like lazy loading and component composition. For existing projects, an automated migration during the ng update will adjust the standalone flag settings accordingly, ensuring compatibility and facilitating a smoother transition to the new defaults.
New migrations for standalone API and injections
Angular has introduced an optional migration for enhancing dependency injection by shifting from constructor-based injection to using the inject() function.
ng g @angular/core:inject
This migration simplifies the code by replacing the constructor syntax:
constructor(private productService: ProductService) {}
with a more streamlined approach:
private productService = inject(ProductService);
Post-migration, you might encounter compilation issues, particularly in tests where instances are directly created. The migration utility offers several options to customize the process, such as handling abstract classes, maintaining backward-compatible constructors, and managing nullable settings to ensure smooth transitions without breaking existing code.
Additionally, a separate migration facilitates the lazy loading of standalone components in routing configurations, transforming direct component references into dynamic imports, which optimizes performance by loading components only when needed.
ng g @angular/core:route-lazy-loading
Direct reference before migration:
{
path: 'products',
component: ProductsComponent
}
Dynamic import (lazy loading) after migration:
{
path: 'products',
loadComponent: () => import('./products/products.component').then(m => m.ProductsComponent)
}
Initializer provider functions
Angular v19 has introduced new helper functions:
- provideAppInitializer,
- provideEnvironmentInitializer,
- providePlatformInitializer
to simplify the initializer setup process and provide a cleaner alternative to the traditional APP_INITIALIZER, ENVIRONMENT_INITIALIZER, and PLATFORM_INITIALIZER tokens. These functions act as syntactic sugar, allowing developers to configure application, environment, and platform-level initializers in a more readable and straightforward way.
export const appConfig: ApplicationConfig = {
providers: [
provideAppInitializer(() => {
console.log('app initialized');
})
]
};
Additionally, Angular v19 includes a migration tool to help transition existing initializers to this new format, making it easier to adopt the updated approach without manually refactoring code.
Automatic flush() in fakeAsync
In Angular v19, the flush() function in fakeAsync() tests is executed automatically at the test’s conclusion. Previously, developers needed to manually invoke flush() or discardPeriodicTasks() to clear pending asynchronous tasks, failing which an error regarding remaining periodic timers would appear. This manual step is eliminated in the newer version, simplifying test code and avoiding common errors related to task cleanup.
it('async test description', fakeAsync(() => {
// ...
flush(); // not needed in Angular 19!
}));
New angular diagnostics
Angular’s Extended Diagnostics are advanced, real-time code checks that identify potential issues and improve code quality during development. They go beyond standard errors and warnings, flagging subtle problems like unused functions, missing imports, and other best practice violations, helping developers catch issues early and keep their Angular applications clean and efficient.
In Angular 19, two more new angular diagnostics are available:
- Uninvoked functions – it flags cases where a function is used in an event binding but isn’t called, often due to missing parentheses in the template. To fix this, ensure functions in event bindings are followed by parentheses so they’re correctly executed rather than treated as properties.
- Unused Standalone Imports – it identifies instances where standalone components, directives, or pipes are imported but not utilized within the module or component. This typically occurs when these standalone entities are included in the imports array but aren’t referenced in the template or code. To resolve this, ensure that all imported standalone components, directives, or pipes are actively used in your application; otherwise, consider removing the unused imports to maintain a clean and efficient codebase.
Strict standalone flag
Angular has introduced the strictStandalone flag in angularCompilerOptions to help enforce standalone usage for components, directives, and pipes. By default, strictStandalone is set to false, so no enforcement occurs unless specifically activated.
Combining this with the fact that, as of version 19, all components are standalone by default, the result is that this flag prohibits having any components, directives and pipes that are overtly marked as non-standalone.
✘ [ERROR] TS-992023: Only standalone components/directives are allowed when 'strictStandalone' is enabled. [plugin angular-compiler]
Playwright support in Angular CLI
When running `ng e2e` CLI command without a configured e2e target in your project, you will be prompted which e2e package you’d like to use. Starting from v19, Playwright will be one of the options available. Under the hood it’s a schematic created by the community that can also be triggered directly (even for the Angular projects below v19) using the following command:
ng add playwright-ng-schematics
Typescript support
Angular v18.1 added support for typescript version 5.5. As of v19.0, support for 5.6 appears, while support for versions lower than 5.5 is dropped. Below are honorable mentions of a few selected features:
Inferred Type Predicates – TypeScript automatically infers type predicates and narrows the types in places where we previously had to define predicates explicitly.
const availableProducts = productIds
.map(id => productCatalog.get(id))
.filter(product => product !== undefined);
/* TypeScript now knows availableProducts are no longer considered as possibly undefined */
availableProducts.forEach(product => product.displayDetails());
Control Flow Narrowing for Constant Indexed Accesses – TypeScript can now narrow expressions like obj[key] when both obj and key are effectively constants.
function logUpperCase(key: string, dictionary:Record<string, unknown>): void {
if(typeof dictionary[key] === 'string') {
/* valid since ts 5.5 */
console.log(dictionary[key].toUpperCase());
}
}
Disallowed Nullish and Truthy Checks – Typescript will throw an error when truthy or nullish checks always evaluate to true (which in terms of JS syntax is correct, but usually implies some logical error). The following examples will cause an error:
if(/^[a-z]+$/) {
/* missing .test(value) call, regex itself is always truthy */
}
if (x => 0) {
/* "x => 0" is an arrow function, always truthy */
}
Support for Typescript isolated modules
Angular 18.2 introduced support for TypeScript’s isolatedModules, enabling up to a 10% boost in production build times by allowing code transpilation through the bundler, which optimizes TypeScript constructs and reduces Babel-based passes.
To enable isolatedModules support in your Angular project, update your TypeScript configuration (tsconfig.json) as follows:
"compilerOptions": {
...
"isolatedModules": true
}
It applies a few extra restrictions, like no cross-file type inference, allowing the export only const enums and forces explicit declarations of type-only exports (using import type syntax).
Without isolatedModules, full type-checking is performed for the entire codebase during the compilation process. In contrast, with isolatedModules enabled, each file is compiled independently, and certain cross-file type analyses are skipped.
Angular Language Service enhancements
The latest Angular Language Service supports the newest functionalities, such as:
- angular diagnostic for unused standalone imports,
- migration @input to signal-input,
- migration to signal queries,
- in-template autocompletion for all directives that are not yet imported,
You may expect some very useful refactoring schematics integrated into the IDE of your choice.
Server Route Configuration (experimental)
Angular introduces a new Server Route Configuration API to improve flexibility in hybrid rendering, allowing developers to define how specific routes should be rendered—whether on the server, pre-rendered, or on the client. This new configuration will make it easier to optimize performance by choosing the most suitable rendering mode for each route.
Here’s an example of how the server route configuration will look like:
import {RenderMode, ServerRoute} from '@angular/ssr';
export const serverRouteConfig: ServerRoute[] = [
{ path: '/login', renderMode: RenderMode.Server },
{ path: '/fruits', renderMode: RenderMode.Prerender },
{ path: '/**', renderMode: RenderMode.Client }
];
In this configuration:
- The /login route will use server-side rendering (SSR), ensuring the latest data is rendered on each request.
- The /fruits route is set for static site generation (SSG), generating the content during build time for faster loading.
- All other routes default to client-side rendering (CSR).
The proposed solution will also allow to define functions to resolve path params in dynamic paths in prerender mode:
export const serverRouteConfig2: ServerRoute[] = [
{
path: '/fruit/:id',
renderMode: RenderMode.Prerender,
async getPrerenderParams() {
const fruitService = inject(FruitService);
const fruitIds = await fruitService.getAllFruitIds();
return fruitIds.map(id => ({id}));
},
},
];
Summary
Angular 19 introduces a range of powerful updates designed to boost app performance, streamline reactivity, and enhance developer control. This release includes more intuitive state management, cleaner setup options, and improvements that make Angular faster and easier to work with. Plus, with exciting experimental features like Incremental Hydration and Server Route Configuration on the horizon, Angular continues to evolve, promising even greater flexibility and efficiency in future versions.
We’d love to hear your thoughts—let us know how these updates resonate with your development needs and what you think of Angular’s direction!