← Back to Guides

Angular 17: Signals and Modern Features

📖 14 min read | 📅 Updated: January 2025 | 🏷️ Web Development

Introduction to Angular 17

Angular 17 introduces revolutionary changes including Signals for reactive programming, standalone components by default, improved performance, and a simplified developer experience. This guide covers all the modern features you need to know.

1. Signals - New Reactive Primitive

Signals are Angular's new reactive primitive that provides fine-grained reactivity without Zone.js.

Basic Signal Usage

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

@Component({
  selector: 'app-counter',
  standalone: true,
  template: `
    <div>
      <p>Count: {{ count() }}</p>
      <p>Double: {{ doubled() }}</p>
      <button (click)="increment()">Increment</button>
    </div>
  `
})
export class CounterComponent {
  // Writable signal
  count = signal(0);
  
  // Computed signal
  doubled = computed(() => this.count() * 2);
  
  constructor() {
    // Effect runs when signals change
    effect(() => {
      console.log('Count changed:', this.count());
    });
  }
  
  increment() {
    this.count.update(value => value + 1);
    // Or: this.count.set(5);
  }
}

Signal with Objects

interface User {
  name: string;
  email: string;
  age: number;
}

export class UserComponent {
  user = signal<User>({
    name: 'John',
    email: 'john@example.com',
    age: 30
  });
  
  updateName(newName: string) {
    this.user.update(user => ({
      ...user,
      name: newName
    }));
  }
  
  // Computed from object property
  isAdult = computed(() => this.user().age >= 18);
}

2. Standalone Components

Standalone components don't require NgModules, simplifying Angular architecture.

import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { HttpClient } from '@angular/common/http';

@Component({
  selector: 'app-user-list',
  standalone: true,
  imports: [CommonModule, FormsModule],
  template: `
    <div>
      <input [(ngModel)]="searchQuery" placeholder="Search...">
      <ul>
        <li *ngFor="let user of filteredUsers()">
          {{ user.name }}
        </li>
      </ul>
    </div>
  `
})
export class UserListComponent {
  searchQuery = signal('');
  users = signal<User[]>([]);
  
  filteredUsers = computed(() => {
    const query = this.searchQuery().toLowerCase();
    return this.users().filter(user =>
      user.name.toLowerCase().includes(query)
    );
  });
  
  constructor(private http: HttpClient) {
    this.loadUsers();
  }
  
  async loadUsers() {
    const users = await this.http.get<User[]>('/api/users').toPromise();
    this.users.set(users);
  }
}

3. New Control Flow Syntax

Angular 17 introduces built-in control flow replacing structural directives.

@Component({
  template: `
    <!-- If/Else -->
    @if (isLoggedIn()) {
      <p>Welcome back!</p>
    } @else {
      <p>Please log in</p>
    }
    
    <!-- For loop -->
    @for (user of users(); track user.id) {
      <div>{{ user.name }}</div>
    } @empty {
      <p>No users found</p>
    }
    
    <!-- Switch -->
    @switch (status()) {
      @case ('loading') {
        <spinner></spinner>
      }
      @case ('success') {
        <data-view [data]="data()"></data-view>
      }
      @case ('error') {
        <error-message [error]="error()"></error-message>
      }
    }
  `
})
export class ModernComponent {
  isLoggedIn = signal(false);
  users = signal<User[]>([]);
  status = signal<'loading' | 'success' | 'error'>('loading');
  data = signal<any>(null);
  error = signal<string | null>(null);
}

4. Input and Output with Signals

import { Component, input, output } from '@angular/core';

@Component({
  selector: 'app-user-card',
  standalone: true,
  template: `
    <div class="card">
      <h3>{{ user().name }}</h3>
      <p>{{ user().email }}</p>
      <button (click)="handleDelete()">Delete</button>
    </div>
  `
})
export class UserCardComponent {
  // Signal-based input
  user = input.required<User>();
  
  // Signal-based output
  delete = output<string>();
  
  handleDelete() {
    this.delete.emit(this.user().id);
  }
}

// Parent component usage
@Component({
  template: `
    <app-user-card 
      [user]="selectedUser()"
      (delete)="onDeleteUser($event)"
    />
  `
})
export class ParentComponent {
  selectedUser = signal<User>({ id: '1', name: 'John', email: 'john@example.com' });
  
  onDeleteUser(userId: string) {
    console.log('Delete user:', userId);
  }
}

5. Deferrable Views

Lazy load components and improve initial load time.

@Component({
  template: `
    <!-- Defer loading until visible -->
    @defer (on viewport) {
      <heavy-component />
    } @placeholder {
      <div>Loading...</div>
    } @loading (minimum 1s) {
      <spinner />
    } @error {
      <p>Failed to load component</p>
    }
    
    <!-- Defer with multiple triggers -->
    @defer (on interaction; on timer(5s)) {
      <chat-widget />
    } @placeholder {
      <button>Open Chat</button>
    }
    
    <!-- Prefetch on idle -->
    @defer (on idle; prefetch on immediate) {
      <analytics-dashboard />
    }
  `
})

6. Improved Dependency Injection

import { Component, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';

@Component({
  selector: 'app-data',
  standalone: true
})
export class DataComponent {
  // Function-based injection
  private http = inject(HttpClient);
  private logger = inject(LoggerService, { optional: true });
  
  // Constructor injection still works
  constructor() {
    this.loadData();
  }
  
  async loadData() {
    const data = await this.http.get('/api/data').toPromise();
    this.logger?.log('Data loaded', data);
  }
}

7. Router with Signals

import { Component, inject } from '@angular/core';
import { Router, ActivatedRoute } from '@angular/router';
import { toSignal } from '@angular/core/rxjs-interop';

@Component({
  selector: 'app-product-detail',
  standalone: true
})
export class ProductDetailComponent {
  private route = inject(ActivatedRoute);
  
  // Convert Observable to Signal
  productId = toSignal(this.route.paramMap.pipe(
    map(params => params.get('id'))
  ));
  
  queryParams = toSignal(this.route.queryParamMap);
  
  product = computed(() => {
    const id = this.productId();
    return id ? this.loadProduct(id) : null;
  });
}

8. Forms with Signals

import { Component, signal } from '@angular/core';
import { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';

@Component({
  selector: 'app-user-form',
  standalone: true,
  imports: [ReactiveFormsModule],
  template: `
    <form [formGroup]="form" (ngSubmit)="onSubmit()">
      <input formControlName="name" placeholder="Name">
      <input formControlName="email" type="email" placeholder="Email">
      <button type="submit" [disabled]="!form.valid">Submit</button>
    </form>
    
    <p>Form valid: {{ isValid() }}</p>
  `
})
export class UserFormComponent {
  form = new FormGroup({
    name: new FormControl('', Validators.required),
    email: new FormControl('', [Validators.required, Validators.email])
  });
  
  isValid = signal(false);
  
  constructor() {
    effect(() => {
      this.isValid.set(this.form.valid);
    });
  }
  
  onSubmit() {
    if (this.form.valid) {
      console.log(this.form.value);
    }
  }
}

9. Server-Side Rendering (SSR)

// Enable SSR in Angular 17
ng add @angular/ssr

// app.config.server.ts
export const serverConfig: ApplicationConfig = {
  providers: [
    provideServerRendering()
  ]
};

// Transfer state for hydration
import { Component, inject, TransferState, makeStateKey } from '@angular/core';

const DATA_KEY = makeStateKey<any>('data');

@Component({})
export class SSRComponent {
  private transferState = inject(TransferState);
  
  ngOnInit() {
    const cachedData = this.transferState.get(DATA_KEY, null);
    
    if (cachedData) {
      this.data.set(cachedData);
    } else {
      this.loadData().then(data => {
        this.data.set(data);
        this.transferState.set(DATA_KEY, data);
      });
    }
  }
}

10. Performance Optimizations

// OnPush change detection with signals
@Component({
  selector: 'app-optimized',
  standalone: true,
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <div>{{ count() }}</div>
  `
})
export class OptimizedComponent {
  count = signal(0);
  
  // Signals automatically trigger change detection
  increment() {
    this.count.update(n => n + 1);
  }
}

// Lazy loading routes
export const routes: Routes = [
  {
    path: 'admin',
    loadComponent: () => import('./admin/admin.component').then(m => m.AdminComponent)
  },
  {
    path: 'users',
    loadChildren: () => import('./users/routes').then(m => m.USERS_ROUTES)
  }
];
💡 Migration Tips:

Conclusion

Angular 17 represents a major leap forward with Signals, standalone components, and improved developer experience. These features make Angular more performant, easier to learn, and better aligned with modern web development practices. Start adopting these patterns in your projects to write cleaner, more maintainable Angular applications.