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 the 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. 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 clarifies the concept:
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' }
]);
}
In this example, colorOptions stores a list of user-selectable colors. favoriteColorId represents the user's selected color. The linked signal recalculates its value whenever the colorOptions list changes.
New (Experimental) API: resource
Here’s an example of using the resource API:
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);
}
With the resource API:
-
The request parameter links the resource to an input signal (in our case, fruitId).
-
The loader function fetches data asynchronously.
- You can access signals like isLoading, value, and error.
- You can reload the data or update the local state of the resource using methods like reload and update.
RxJS Interop
Angular now provides a counterpart of the resource method called rxResource. This version uses an Observable as the loader function while retaining all other properties as 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 Angular 19, the effect() function has received key updates based on community feedback. A major change is the removal of the allowSignalWrites flag, which was previously used to restrict when signals could be set within effect(). Now, signals can be set by default, making it easier to implement updates.
effect(
() => {
console.log(this.users());
}
);
New Equality Function in RxJS Interop
// 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,
});
New afterRenderEffect Function
The afterRenderEffect function is an experimental API in Angular that handles side effects that should only occur after the component has finished rendering. This function allows you to react to state changes only after the DOM is updated.
counter = signal(0);
constructor() {
afterRenderEffect(() => {
console.log('after render effect', this.counter());
});
afterRender(() => {
console.log('after render', this.counter());
});
}
afterRenderEffect tracks dependencies and runs after each render cycle, whereas afterRender runs after every render cycle regardless of dependencies.
New Template Variable Syntax @let
Angular introduced the @let syntax in version 18.1, which became stable in version 19. This feature simplifies defining and reusing variables within templates, making it easier to store and reference expressions.
@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>
Variables defined with @let are read-only and scoped to the current template and its descendants.
Experimental Incremental Hydration
To enable it, add the following configuration:
export const appConfig: ApplicationConfig = {
providers: [
provideClientHydration(
withIncrementalHydration()
)
]
};
- idle
- interaction
- immediate
- timer(ms)
- hover
- viewport
- never (stays dehydrated indefinitely)
- when {{ condition }}
New routerOutletData Input for RouterOutlet
Parent Component:
<router-outlet [routerOutletData]="routerOutletData()"></router-outlet>
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>
This allows all additional options (such as query params, query params handling strategy, relativeTo etc.) to be passed directly into the UrlTree object.
- '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 to preserve or merge. Previously, the strategy could only be set individually for each navigation, either through RouterLink or router.navigate options.
Standalone by Default
Standalone Component:
@Component({
imports: [],
selector: 'home',
template: './home-component.html',
// standalone in Angular 19!
})
export class HomeComponent { ... }
For non-standalone components, an explicit flag must be provided:
@Component({
selector: 'home',
template: './home-component.html',
standalone: false
// non-standalone in Angular 19!
})
export class HomeComponent { ... }
This simplifies Angular, making it more accessible to new developers and improving features like lazy loading and component composition.
New Migrations for Standalone API and Injections
Before Migration:
constructor(private productService: ProductService) {}
After Migration:
private productService = inject(ProductService);
Post-migration, you might encounter compilation issues, particularly in tests where instances are directly created. The migration utility provides several options to handle abstract classes, backward-compatible constructors, and nullable settings to ensure smooth transitions.
Additionally, a separate migration facilitates the lazy loading of standalone components in routing configurations, transforming direct component references into dynamic imports for performance optimization.
Before Migration:
{
path: 'products',
component: ProductsComponent
}
After Migration:
{
path: 'products',
loadComponent: () => import('./products/products.component').then(m => m.ProductsComponent)
}
Initializer Provider Functions
Example:
export const appConfig: ApplicationConfig = {
providers: [
provideAppInitializer(() => {
console.log('app initialized');
})
]
};
Automatic flush() in fakeAsync
Before Angular v19:
it('async test description', fakeAsync(() => {
// ...
flush();
}));
After Angular v19:
it('async test description', fakeAsync(() => {
// No need to call flush() manually anymore
}));
New Angular Diagnostics
New Diagnostics in Angular v19:
-
Uninvoked Functions: Flags cases where a function is used in an event binding but isn’t called, usually due to missing parentheses in the template.
-
Unused Standalone Imports: Identifies instances where standalone components, directives, or pipes are imported but not used in the module or component.
Strict Standalone Flag
Playwright Support in Angular CLI
To add Playwright support to your project, use the following command:
ng add playwright-ng-schematics
TypeScript Support
Example:
const availableProducts = productIds
.map(id => productCatalog.get(id))
.filter(product => product !== undefined);
Control Flow Narrowing for Constant Indexed Accesses: TypeScript narrows expressions like obj[key] when both obj and key are constants.
Example:
function logUpperCase(key: string, dictionary: Record<string, unknown>): void {
if (typeof dictionary[key] === 'string') {
console.log(dictionary[key].toUpperCase());
}
}
Support for TypeScript Isolated Modules
To enable isolatedModules, update your TypeScript configuration (tsconfig.json):
"compilerOptions": {
"isolatedModules": true
}
Angular Language Service Enhancements
-
Angular diagnostics for unused standalone imports.
-
Migration from @Input to signal-input.
- In-template autocompletion for directives not yet imported.
Server Route Configuration (Experimental)
Example:
import { RenderMode, ServerRoute } from '@angular/ssr';
export const serverRouteConfig: ServerRoute[] = [
{ path: '/login', renderMode: RenderMode.Server },
{ path: '/fruits', renderMode: RenderMode.Prerender },
{ path: '/**', renderMode: RenderMode.Client }
];
Vikas Mishra
A highly skilled Angular & React Js Developer. Committed to delivering efficient, high-quality solutions by simplifying complex projects with technical expertise and innovative thinking.
Reply