Image description

In modern Angular development, developers seek better reactivity without excessive boilerplate or dependency on complex state management tools. Angular 16+ introduced Signals—a new reactivity model that enables a more declarative and efficient approach to state tracking in your components.

In this article, we will explore how to use Angular Signals with strings and objects through a real-world example: a reactive user profile editor. We'll demonstrate best practices using signal(), computed(), effect(), and update() to efficiently manage user data and respond to changes in a declarative style.


What Are Signals?

Signals are reactive primitives introduced in Angular 16 that allow you to manage reactive state with fine-grained control. Unlike RxJS Observables, signals:

  • Do not require subscription management.
  • Trigger updates automatically in views.
  • Are optimized for performance.

Three core APIs include:

  • signal(): defines a reactive writable value.
  • computed(): creates derived signals based on one or more others.
  • effect(): executes side effects when signals change.

Use Signals with Strings and Objects

In our example, we’ll demonstrate signals using:

  • Primitive string types (firstName, lastName).
  • An object type UserProfile, which we will mutate reactively.

Update and Display User Data

We'll build a user profile editor that dynamically updates the display as you type. It will react to:

  • Changes in the name fields.
  • Email updates.

React to Specific Changes with effect()

We'll use effect() to monitor signal changes and log a warning when a user sets their email to a known placeholder domain (e.g., @example.com). This technique is also useful for form validation, logging, analytics, or triggering external updates.


Use computed() to Derive a Full Name

Rather than manually combining fields every time, we use computed() to derive the full name reactively. This ensures data consistency without duplication of logic.


Show How to update() Objects Safely

Instead of replacing an entire object, update() allows updating nested properties in a controlled and efficient way. This avoids unnecessary object references and re-renders.


Advanced Example: user-profile.component.ts

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

interface UserProfile {
  firstName: string;
  lastName: string;
  email: string;
}

@Component({
  selector: 'app-user-profile',
  standalone: true,
  imports: [FormsModule], // 👈 required to use ngModel
  template: `
    
      User Profile
      First Name: 
      Last Name: 
      Email: 
      
      Full Name: {{ fullName() }}
      Email: {{ user().email }}
    
  `,
  styles: [
    `.profile-card {
      background: #f5f5f5;
      padding: 1rem;
      border-radius: 8px;
      width: 320px;
    }`,
    `label { display: block; margin-bottom: 0.5rem; }`,
    `input { width: 100%; padding: 0.25rem; margin-top: 0.25rem; }`
  ]
})
export class UserProfileComponent {
  firstName = signal('Ada');
  lastName = signal('Lovelace');
  user: WritableSignal<UserProfile> = signal({
    firstName: 'Ada',
    lastName: 'Lovelace',
    email: 'ada@angular.dev'
  });
  fullName = computed(() => `${this.firstName()} ${this.lastName()}`);
  constructor() {
    effect(() => {
      const currentEmail = this.user().email;
      if (currentEmail.endsWith('@example.com')) {
        console.warn(`Warning: Placeholder email detected: ${currentEmail}`);
      }
    });
  }
  updateFirstName(value: string) {
    this.firstName.set(value);
    this.user.update(current => ({
      ...current,
      firstName: value
    }))
  }
  updateLastName(value: string) {
    this.lastName.set(value);
    this.user.update(current => ({
      ...current,
      lastName: value
    }))
  }
  updateEmail(value: string) {
    this.lastName.set(value);
    this.user.update(current => ({
      ...current,
      email: value
    }))
  }
}

🔍 Key Concepts in This Example

Feature Purpose
signal() Reactive primitive for both primitives and complex types
computed() Derives values automatically when dependencies change
effect() Executes logic based on reactive value changes
update() Updates part of an object without replacing the whole signal value
ngModel + signals Demonstrates how signals integrate with Angular forms

Bonus Challenge

For extended learning and refinement:

  • Add a signal that tracks form validity (e.g., isValidName, isValidEmail).
  • Create a reset() method to restore original values using .set().
  • Add a list of name changes using signal.
  • Create a toggle mode (isEditMode) with computed readonly fields.

Conclusion

Angular Signals offer a powerful and elegant solution for managing state reactively without the overhead of RxJS. With minimal code, you can:

  • Track and display changes in real-time
  • Create dynamic derived data
  • React to changes declaratively with effects

This new paradigm simplifies how we manage component logic and enhances maintainability, especially in complex applications.

Angular's future is reactive — and it's a great time to embrace Signals as your new go-to state primitive.


angular #typescript #signals #frontend #architecture