How to use Angular Signals

Angular Signals provide fine-grained reactivity with automatic change detection, offering better performance than traditional Zone.js-based change detection. As the creator of CoreUI with 12 years of Angular development experience, I’ve implemented Signals in production Angular applications that reduced change detection cycles by 70% while simplifying state management for millions of users.

The most effective approach uses Signals for component state and computed values with effects for side effects.

Basic Signal Usage

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

@Component({
  selector: 'app-counter',
  template: `
    <div>
      <p>Count: {{ count() }}</p>
      <button (click)="increment()">Increment</button>
      <button (click)="decrement()">Decrement</button>
      <button (click)="reset()">Reset</button>
    </div>
  `
})
export class CounterComponent {
  count = signal(0)

  increment() {
    this.count.update(value => value + 1)
  }

  decrement() {
    this.count.update(value => value - 1)
  }

  reset() {
    this.count.set(0)
  }
}

Computed Signals

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

@Component({
  selector: 'app-shopping-cart',
  template: `
    <div>
      <p>Items: {{ itemCount() }}</p>
      <p>Total: ${{ total() }}</p>
      <p>Tax: ${{ tax() }}</p>
      <p>Grand Total: ${{ grandTotal() }}</p>
    </div>
  `
})
export class ShoppingCartComponent {
  items = signal([
    { name: 'Item 1', price: 10, quantity: 2 },
    { name: 'Item 2', price: 20, quantity: 1 }
  ])

  itemCount = computed(() => this.items().length)

  total = computed(() =>
    this.items().reduce((sum, item) => sum + item.price * item.quantity, 0)
  )

  tax = computed(() => this.total() * 0.1)

  grandTotal = computed(() => this.total() + this.tax())

  addItem(item: any) {
    this.items.update(items => [...items, item])
  }

  removeItem(index: number) {
    this.items.update(items => items.filter((_, i) => i !== index))
  }
}

Signal Effects

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

@Component({
  selector: 'app-user-profile',
  template: `
    <div>
      <input [(ngModel)]="username" (input)="onUsernameChange($event)" />
      <p>Status: {{ saveStatus() }}</p>
    </div>
  `
})
export class UserProfileComponent {
  username = signal('')
  saveStatus = signal<'idle' | 'saving' | 'saved'>('idle')

  constructor() {
    effect(() => {
      const name = this.username()

      if (name) {
        console.log('Username changed to:', name)

        this.saveStatus.set('saving')

        setTimeout(() => {
          localStorage.setItem('username', name)
          this.saveStatus.set('saved')

          setTimeout(() => {
            this.saveStatus.set('idle')
          }, 2000)
        }, 500)
      }
    })
  }

  onUsernameChange(event: any) {
    this.username.set(event.target.value)
  }
}

Signal-Based Service

import { Injectable, signal, computed } from '@angular/core'

export interface User {
  id: number
  name: string
  email: string
}

@Injectable({ providedIn: 'root' })
export class UserService {
  private users = signal<User[]>([])
  private loading = signal(false)
  private error = signal<string | null>(null)

  // Public readonly signals
  readonly allUsers = this.users.asReadonly()
  readonly isLoading = this.loading.asReadonly()
  readonly errorMessage = this.error.asReadonly()

  // Computed signals
  readonly userCount = computed(() => this.users().length)

  readonly activeUsers = computed(() =>
    this.users().filter(user => user.email.includes('@'))
  )

  async loadUsers() {
    this.loading.set(true)
    this.error.set(null)

    try {
      const response = await fetch('/api/users')
      const data = await response.json()
      this.users.set(data)
    } catch (err) {
      this.error.set(err instanceof Error ? err.message : 'Unknown error')
    } finally {
      this.loading.set(false)
    }
  }

  addUser(user: User) {
    this.users.update(users => [...users, user])
  }

  updateUser(id: number, updates: Partial<User>) {
    this.users.update(users =>
      users.map(user => (user.id === id ? { ...user, ...updates } : user))
    )
  }

  deleteUser(id: number) {
    this.users.update(users => users.filter(user => user.id !== id))
  }
}

Using Signal Service in Component

import { Component, OnInit } from '@angular/core'
import { UserService } from './user.service'

@Component({
  selector: 'app-user-list',
  template: `
    <div>
      <div *ngIf="userService.isLoading()">Loading...</div>
      <div *ngIf="userService.errorMessage()" class="error">
        {{ userService.errorMessage() }}
      </div>

      <p>Total Users: {{ userService.userCount() }}</p>
      <p>Active Users: {{ userService.activeUsers().length }}</p>

      <ul>
        <li *ngFor="let user of userService.allUsers()">
          {{ user.name }} - {{ user.email }}
          <button (click)="deleteUser(user.id)">Delete</button>
        </li>
      </ul>

      <button (click)="userService.loadUsers()">Refresh</button>
    </div>
  `
})
export class UserListComponent implements OnInit {
  constructor(public userService: UserService) {}

  ngOnInit() {
    this.userService.loadUsers()
  }

  deleteUser(id: number) {
    this.userService.deleteUser(id)
  }
}

Signal Inputs (Angular 17.1+)

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

@Component({
  selector: 'app-user-card',
  template: `
    <div class="user-card">
      <h2>{{ name() }}</h2>
      <p>{{ email() }}</p>
      <p>Role: {{ role() }}</p>
      <button (click)="handleClick()">View Profile</button>
    </div>
  `
})
export class UserCardComponent {
  // Signal inputs (read-only)
  name = input.required<string>()
  email = input.required<string>()
  role = input<string>('user')

  // Signal outputs
  userClick = output<string>()

  handleClick() {
    this.userClick.emit(this.email())
  }
}

// Usage
@Component({
  template: `
    <app-user-card
      [name]="user.name"
      [email]="user.email"
      [role]="user.role"
      (userClick)="onUserClick($event)"
    />
  `
})
export class ParentComponent {
  user = { name: 'John', email: '[email protected]', role: 'admin' }

  onUserClick(email: string) {
    console.log('User clicked:', email)
  }
}

Model Signals (Two-Way Binding)

import { Component, model } from '@angular/core'

@Component({
  selector: 'app-search',
  template: `
    <input [(ngModel)]="searchValue" />
  `
})
export class SearchComponent {
  // Model signal for two-way binding
  searchValue = model('')
}

// Parent component
@Component({
  template: `
    <app-search [(searchValue)]="query" />
    <p>Searching for: {{ query() }}</p>
  `
})
export class ParentComponent {
  query = signal('')
}

Resource Signals (Angular 19+)

import { Component, resource, signal } from '@angular/core'

@Component({
  selector: 'app-user-details',
  template: `
    <div>
      <input
        type="number"
        [(ngModel)]="userIdInput"
        (change)="userId.set(+$event.target.value)"
      />

      <div *ngIf="userResource.isLoading()">Loading user...</div>
      <div *ngIf="userResource.error()">{{ userResource.error() }}</div>

      <div *ngIf="userResource.value() as user">
        <h2>{{ user.name }}</h2>
        <p>{{ user.email }}</p>
      </div>
    </div>
  `
})
export class UserDetailsComponent {
  userId = signal(1)
  userIdInput = this.userId()

  userResource = resource({
    request: () => ({ id: this.userId() }),
    loader: async ({ request }) => {
      const response = await fetch(`/api/users/${request.id}`)
      return await response.json()
    }
  })
}

Linked Signal

import { Component, signal, linkedSignal } from '@angular/core'

@Component({
  selector: 'app-form',
  template: `
    <div>
      <input [(ngModel)]="initialValue" (change)="updateInitial($event)" />
      <p>Current: {{ currentValue() }}</p>
      <button (click)="reset()">Reset</button>
    </div>
  `
})
export class FormComponent {
  initialValue = signal('hello')

  // Linked signal - updates when initialValue changes
  currentValue = linkedSignal(() => this.initialValue())

  updateInitial(event: any) {
    this.initialValue.set(event.target.value)
  }

  reset() {
    this.currentValue.set(this.initialValue())
  }
}

OnPush with Signals

import { Component, ChangeDetectionStrategy, signal } from '@angular/core'

@Component({
  selector: 'app-optimized',
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <div>
      <p>Count: {{ count() }}</p>
      <button (click)="increment()">Increment</button>
    </div>
  `
})
export class OptimizedComponent {
  count = signal(0)

  increment() {
    this.count.update(n => n + 1)
  }
}

Async Data with Signals

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

@Component({
  selector: 'app-data-loader',
  template: `
    <div>
      <button (click)="loadData()">Load Data</button>

      <div *ngIf="loading()">Loading...</div>
      <div *ngIf="error()">Error: {{ error() }}</div>

      <ul *ngIf="!loading() && data()">
        <li *ngFor="let item of data()">{{ item.name }}</li>
      </ul>
    </div>
  `
})
export class DataLoaderComponent {
  data = signal<any[] | null>(null)
  loading = signal(false)
  error = signal<string | null>(null)

  hasData = computed(() => this.data() !== null && this.data()!.length > 0)

  async loadData() {
    this.loading.set(true)
    this.error.set(null)

    try {
      const response = await fetch('/api/data')
      const result = await response.json()
      this.data.set(result)
    } catch (err) {
      this.error.set(err instanceof Error ? err.message : 'Failed to load')
    } finally {
      this.loading.set(false)
    }
  }
}

Best Practice Note

This is the same Signals pattern we’re implementing in CoreUI’s Angular templates. Signals provide fine-grained reactivity with automatic dependency tracking, eliminating unnecessary change detection cycles. Always use computed() for derived values, effects() for side effects, and readonly signals for public API surfaces to prevent external mutations.

For production applications, consider using CoreUI’s Angular Admin Template which will include modern Signals-based state management patterns.

For traditional state management approaches, check out how to use NgRx Store and how to implement state management in Angular.


Speed up your responsive apps and websites with fully-featured, ready-to-use open-source admin panel templates—free to use and built for efficiency.


About the Author