# Angular 18: Signals and New Features
> Discover Angular 18 Signals, zoneless change detection, and new signal-based APIs to build more performant applications.
- Published: 2026-01-18
- Updated: 2026-03-31
- Author: SharpSkill
- Tags: angular 18, angular signals, zoneless, signal inputs, reactivity
- Reading time: 12 min
---
Angular 18 marks a turning point in the framework's evolution with the stabilization of Signals. This new reactive primitive fundamentally transforms how Angular components are built, offering a modern alternative to traditional decorators while paving the way for Zone.js-free change detection.
> **What you will learn**
>
> Angular 18's signal-based APIs: input(), model(), viewChild(), and zoneless configuration for lighter and more performant applications.
## Understanding Signals in Angular 18
Signals represent a new approach to reactivity in Angular. Unlike classic decorators like `@Input()` that rely on Zone.js change detection, Signals offer fine-grained and explicit reactivity. Each Signal encapsulates a value and automatically notifies consumers when that value changes.
This approach offers several advantages: better performance through targeted updates, native integration with `computed()` and `effect()` functions, and preparation for Angular's zoneless future.
```typescript
// signals-basics.component.ts
// Demonstration of fundamental Signal concepts
import { Component, signal, computed, effect } from '@angular/core';
@Component({
selector: 'app-counter',
standalone: true,
template: `
Counter: {{ count() }}
Double: {{ doubleCount() }}
`
})
export class CounterComponent {
// Writable signal - value can be modified
count = signal(0);
// Computed signal - automatically derived from count
// Only recalculates when count changes
doubleCount = computed(() => this.count() * 2);
constructor() {
// Effect - executed on every count change
// Useful for side effects (logs, API calls, etc.)
effect(() => {
console.log(`New counter value: ${this.count()}`);
});
}
increment() {
// update() allows modification based on previous value
this.count.update(value => value + 1);
}
decrement() {
this.count.update(value => value - 1);
}
reset() {
// set() directly replaces the value
this.count.set(0);
}
}
```
Signals work as reactive containers: `signal()` creates a writable Signal, `computed()` derives calculated values, and `effect()` allows executing actions in response to changes.
## Signal Inputs with input()
The `input()` function replaces the traditional `@Input()` decorator. It returns a read-only `InputSignal`, ensuring that data always flows from parent to child without accidental modification.
```typescript
// book-card.component.ts
// Component using signal inputs
import { Component, input, computed } from '@angular/core';
interface Book {
id: string;
title: string;
author: string;
price: number;
discountPercent?: number;
}
@Component({
selector: 'app-book-card',
standalone: true,
template: `
{{ book().title }}
By {{ book().author }}
@if (hasDiscount()) {
\${{ book().price }}\${{ discountedPrice() }}
} @else {
\${{ book().price }}
}
@if (featured()) {
Featured
}
`
})
export class BookCardComponent {
// Required input - template won't compile without this prop
book = input.required();
// Optional input with default value
featured = input(false);
// Computed based on input - automatically recalculated
hasDiscount = computed(() => {
const discount = this.book().discountPercent;
return discount !== undefined && discount > 0;
});
// Discounted price calculation
discountedPrice = computed(() => {
const { price, discountPercent } = this.book();
if (!discountPercent) return price;
return (price * (100 - discountPercent) / 100).toFixed(2);
});
}
```
Usage in a parent template remains similar, but with type safety and Signal reactivity:
```typescript
// book-list.component.ts
// Parent component using book-card
import { Component, signal } from '@angular/core';
import { BookCardComponent } from './book-card.component';
@Component({
selector: 'app-book-list',
standalone: true,
imports: [BookCardComponent],
template: `
@for (book of books(); track book.id) {
}
`
})
export class BookListComponent {
books = signal([
{ id: '1', title: 'Clean Code', author: 'Robert C. Martin', price: 35 },
{ id: '2', title: 'The Pragmatic Programmer', author: 'David Thomas', price: 42, discountPercent: 15 }
]);
featuredBookId = signal('1');
}
```
The major difference from `@Input()`: signal inputs are read-only. Calling `this.book.set()` from the child component is impossible, which reinforces unidirectional data flow.
## Two-Way Binding with model()
For cases requiring bidirectional synchronization, Angular 18 introduces `model()`. This function creates a writable Signal that automatically propagates changes to the parent component.
```typescript
// search-input.component.ts
// Component with bidirectional binding via model()
import { Component, model, output, computed } from '@angular/core';
@Component({
selector: 'app-search-input',
standalone: true,
template: `
`
})
export class CartComponent {
// Signal for mutable data
items = signal([]);
// Computed for derived values - avoids unnecessary recalculations
itemCount = computed(() => this.items().length);
total = computed(() =>
this.items().reduce((sum, item) => sum + item.price * item.quantity, 0)
);
constructor() {
// Effect for side effects (analytics, persistence)
effect(() => {
const currentItems = this.items();
// untracked avoids creating a dependency
untracked(() => {
localStorage.setItem('cart', JSON.stringify(currentItems));
});
});
}
addItem(product: Product) {
// update() for modifications based on previous state
this.items.update(current => {
const existing = current.find(i => i.id === product.id);
if (existing) {
return current.map(i =>
i.id === product.id
? { ...i, quantity: i.quantity + 1 }
: i
);
}
return [...current, { ...product, quantity: 1 }];
});
}
removeItem(id: string) {
this.items.update(current => current.filter(i => i.id !== id));
}
}
```
Key points to remember:
- Use `computed()` for derived values rather than recalculating in the template
- Prefer `update()` over `set()` when the new value depends on the old one
- Use `untracked()` in effects to avoid circular dependencies
- Always specify `track` in `@for` loops to optimize rendering
## Conclusion
Angular 18 lays the foundation for a Zone.js-free future through Signals. Key takeaways:
- ✅ **input()** replaces `@Input()` with stricter typing and guaranteed read-only access
- ✅ **model()** enables reactive two-way binding between parent and child
- ✅ **viewChild()** and **contentChild()** eliminate the need for lifecycle hooks
- ✅ **Zoneless** reduces bundle size and improves performance
- ✅ **computed()** and **effect()** complete the reactive ecosystem
- ✅ Gradual migration is possible component by component
Adopting Signals prepares Angular applications for future versions where zoneless mode will become the norm. This transition represents a wise investment for long-term maintainability and performance.
---
Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack.
HTML version of this page: https://sharpskill.dev/en/blog/angular/angular-18-signals-new-features