Master Angular Signals in 2025: Build Faster, Smarter Angular Apps

syncfusionsyncfusion
13 min read

TL;DR: Angular signals offer a simpler, more performant alternative to RxJS for managing reactive state in Angular apps. This guide explores how to use writable, computed, and linked signals to build scalable, reactive components in 2025.

Angular signals are transforming how developers manage state in Angular apps. If you’ve struggled with RxJS complexity or performance bottlenecks, signals offer a cleaner, more intuitive solution. In this guide, you’ll learn how to use signals to build reactive, scalable components in 2025 and beyond.

What are Angular signals?

Think of Angular signals as smart containers for your application state that automatically notify interested parties when their values change. Unlike traditional reactive patterns, signals provide a synchronous, pull-based reactivity model that’s both intuitive and performant.

At its core, a signal is a wrapper around a value that tracks where it’s being used throughout your application. This granular tracking allows Angular to optimize rendering updates with surgical precision, updating only the components and DOM elements that depend on the changed data.

import { signal } from "@angular/core";

// Create a signal with an initial value
const count = signal(0);

// Read the signal's value by calling it as a function
console.log("Current count:", count()); // 0

// Update the signal's value
count.set(5);
count.update((current) => current + 1);

The beauty of signals lies in their simplicity: they’re just getter functions that Angular can track automatically.

Why Angular signals matter in 2025

Signals solve critical challenges that have plagued Angular development for years:

  • Performance: Signals enable fine-grained reactivity, allowing Angular to skip entire component subtrees that don’t depend on changed state. This dramatically reduces change detection overhead, especially in complex applications.

  • Developer experience: Manual subscription management and memory leak concerns are eliminated. Signals provide a clean, synchronous API that’s easier to reason about and debug.

  • Framework evolution: As Angular moves toward a more reactive future, signals become the foundation for new features like signal-based components and improved SSR capabilities.

Core concepts: Understanding the signal trinity

Angular’s signal system has three fundamental building blocks that work together to create a complete reactive programming model.

Signals: Your reactive state containers

Writable signals are the foundation of your reactive state management:

import { signal, WritableSignal } from "@angular/core";

@Component({
    template: `
        <div>
            <p>User: {{ user().name }}</p>
            <p>Email: {{ user().email }}</p>
            <button (click)="updateUser()">Update User</button>
        </div>
    `,
})
export class UserComponent {
    user: WritableSignal<User> = signal({
        name: "John Doe",
        email: "john@example.com",
    });
    updateUser() {
        this.user.update((current) => ({
            ...current,
            email: "john.doe@newdomain.com",
        }));
    }
}

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

Computed signals: Derived state made easy

Computed signals automatically derive their values from other signals, creating a reactive dependency graph:

import { computed, signal } from "@angular/core";
import { FormsModule } from '@angular/forms';

@Component({
    template: `
        <div>
            <input [(ngModel)]="firstName" />
            <input [(ngModel)]="lastName" />
            <h2>Welcome, {{ fullName() }}!</h2>
            <p>Initials: {{ initials() }}</p>
        </div>
    `,
})
export class NameComponent {
    firstName = signal("");
    lastName = signal("");

    // Automatically updates when firstName or lastName changes
    fullName = computed(() => `${this.firstName()} ${this.lastName()}`.trim());

    initials = computed(() =>
    `${this.firstName().charAt(0)}${this.lastName().charAt(0)}`.toUpperCase());
}

Effects: Reacting to signal changes

Effects run side effects in response to signal changes, perfect for logging, persistence, or DOM manipulation:

import { effect, signal } from "@angular/core";

@Component({})
export class DataPersistenceComponent {
    userData = signal({ preferences: "dark-theme" });
    constructor() {
        // Automatically sync to localStorage when userData changes
        effect(() => {
            const data = this.userData();
            localStorage.setItem("userData", JSON.stringify(data));
            console.log("User data saved:", data);
        });
    }
}

Getting started with Angular signals

Basic setup

Start by importing the necessary functions from @angular/core. These imports provide everything you need to implement reactive patterns in your components:

import { signal, computed, effect, WritableSignal } from "@angular/core";

Your first signal-powered component

Here’s a complete counter component that demonstrates the reactive nature of signals and how they automatically update the UI:

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

@Component({
    selector: "app-counter",
    standalone: true,
    template: `
        <div class="counter-container">
            <h2>Count: {{ count() }}</h2>
            <p>Double: {{ doubleCount() }}</p>
            <p>Status: {{ status() }}</p>
            <button (click)="increment()">+</button>
            <button (click)="decrement()">-</button>
            <button (click)="reset()">Reset</button>
        </div>
    `,
})
export class CounterComponent {
    count = signal(0);
    doubleCount = computed(() => this.count() * 2);
    status = computed(() => {
        const value = this.count();
        if (value === 0) return "neutral";
        return value > 0 ? "positive" : "negative";
    });
    increment() {
        this.count.update((value) => value + 1);
    }
    decrement() {
        this.count.update((value) => value - 1);
    }
    reset() {
        this.count.set(0);
    }
}

How this reactive counter works:

The count signal holds the main state, while doubleCount and status are computed signals that automatically derive their values from count. When you click any button to change the count value, Angular instantly updates all dependent computed signals and re-renders only the affected parts of the template. Notice how you access signal values in the template by calling them as functions {{ count() }}, and how the computed signals automatically recalculate without any manual coordination. This demonstrates the core power of signals: declarative state management with automatic reactivity.

Advanced usage and patterns

Angular signals offer sophisticated patterns for managing intricate state relationships and asynchronous operations as your application grows in complexity.

Linked signals for the dependent state

The new linkedSignal function solves a common problem: managing state that should automatically update based on other state changes, while still allowing manual user interactions. It’s perfect for dropdown selections that need to be reset when options change.

import { signal, linkedSignal } from "@angular/core";

@Component({})
export class ShippingComponent {
    shippingOptions = signal([
        { id: 1, name: "Standard" },
        { id: 2, name: "Express" },
        { id: 3, name: "Overnight" },
    ]);

    // Automatically resets to the first option when shipping options change
    // but allows manual selection
    selectedOption = linkedSignal(() => this.shippingOptions()[0]);

    selectOption(option: ShippingOption) {
        this.selectedOption.set(option);
    }
}

interface ShippingOption {
    id: number;
    name: string;
}

How linkedSignal works

Unlike computed signals (which are read-only), linkedSignal creates a writable signal that automatically updates when its source changes. In this example, selectedOption will always default to the first shipping option whenever shippingOptions changes, but users can still manually select different options using the selectOption method. This prevents the common UI bug where a user’s selection becomes invalid after data updates, while preserving user choice when possible.

Async reactivity with resources

The experimental resource function brings async operations into the signals world, providing a declarative way to handle data fetching that automatically reacts to signal changes. This powerful feature bridges the gap between synchronous signals and asynchronous operations like HTTP requests.

import { resource, signal } from "@angular/core";

@Component({})
export class UserProfileComponent {
    userId = signal("123");
    userResource = resource({
        params: () => ({ id: this.userId() }),
        loader: ({ params, abortSignal }) =>
            fetch(`/api/users/${params.id}`, { signal: abortSignal }).then(
                (response) => response.json()
            ),
    });

    // Reactive computed based on resource state
    userName = computed(() => {
        if (this.userResource.hasValue()) {
            return this.userResource.value().name;
        }
        return "Loading...";
    });
}

In this example, the resource function creates a reactive data loader that:

  • Automatically triggers when the userId signal changes, thanks to the params

  • Handles request cancellation using the provided abortSignal when parameters change mid-flight.

  • Provides loading states through properties like isLoading, hasValue, and status.

  • Integrates seamlessly with other signals and computed values.

The resource object exposes several useful properties: value contains the loaded data, isLoading indicates request status, hasValue() acts as a type guard, and error captures any failures. This makes it incredibly easy to build robust UIs that handle loading states, errors, and data updates automatically.

Best practices and common pitfalls

Do’s

  • Use computed signals for the derived state instead of effects

  • Keep effects simple and focused on side effects only

  • Leverage untracked() when reading signals without creating dependencies

  • Use equality functions for custom comparison logic with complex objects

Don’ts

  • Avoid side effects in computed signals: they should be pure functions

  • Don’t use effects for state propagation: use computed signals instead

  • Avoid excessive signal granularity: group related states together

  • Don’t forget to handle loading states when working with resources

Common mistakes

// ❌ Wrong: Side effect in computed
const badComputed = computed(() => {
    const data = this.data();
    this.logService.log(data); // Side effect!
    return data.processed;
});

// ✅ Correct: Use effect for side effects
const goodComputed = computed(() => this.data().processed);

effect(() => {
    this.logService.log(this.data()); // Side effect in effect
});

Understanding the separation of concerns:

The key mistake above is mixing side effects ( like logging ) with computed signal logic. Computed signals should be pure functions that only transform input values and return results; they shouldn’t perform actions like logging, API calls, or DOM updates. When you include side effects in computed signals, Angular may call them multiple times during optimization, causing unwanted duplicate logs or operations. The correct approach separates these concerns: use computed signals purely for data transformation (goodComputed only processes the data), while effects handle all side effects like logging. This separation ensures predictable behavior and better performance.

Signals vs. RxJS observables

Use signals forUse RxJS for
Managing synchronous stateHandling complex async operations
Building reactive UI componentsNeeding time-based operators (debounce, throttle)
Optimizing change detection performanceManaging HTTP requests with retry logic
Working with simple data flowsWorking with event streams

Combining signals with RxJS

Sometimes you need the best of both worlds: Signals for simple reactivity and RxJS for complex async operations. Angular provides seamless interoperability between these two systems through conversion functions.

import { Component, inject, signal } from "@angular/core";
import { toObservable, toSignal } from "@angular/core/rxjs-interop";
import { debounceTime, distinctUntilChanged, switchMap } from "rxjs";

@Component({})
export class SearchComponent {
    searchTerm = signal("");
    private searchService = inject(SearchService); // A dummy service.

    // Convert signal to observable for RxJS operators
    searchResults = toSignal(
        toObservable(this.searchTerm).pipe(
            debounceTime(300),
            distinctUntilChanged(),
            switchMap((term) => this.searchService.search(term))
        )
    );

    onSearch(term: string) {
        this.searchTerm.set(term);
    }
}

How the signal-RxJS bridge works:

This example demonstrates a reactive search feature where user input triggers API calls with intelligent throttling. The toObservable() function converts the searchTerm signal into an RxJS observable, allowing us to use powerful operators like debounceTime (waits 300ms after the user stops typing), distinctUntilChanged (prevents duplicate requests), and switchMap (cancels previous requests when new ones arrive). Finally, toSignal() converts the result into a signal for easy template consumption. This hybrid approach gives you signal simplicity for UI state and RxJS power for complex async operations.

Real-world examples

UI reactivity: Dynamic theme system

This example demonstrates how signals create a seamless reactive theme system that automatically adapts to user preferences and system settings. The theme component showcases the power of computed signals to derive state from multiple sources while maintaining clean, predictable logic.

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

@Component({
    template: `
        <div [class]="themeClass()">
            <select (change)="setTheme($event)">
                <option value="light">Light</option>
                <option value="dark">Dark</option>
                <option value="auto">Auto</option>
            </select>
            <p>Current theme: {{ currentTheme() }}</p>
        </div>
    `,
})
export class ThemeComponent {
    selectedTheme = signal<"light" | "dark" | "auto">("auto");
    systemTheme = signal<"light" | "dark">("light");
    currentTheme = computed(() => {
        const selected = this.selectedTheme();
        return selected === "auto" ? this.systemTheme() : selected;
    });

    themeClass = computed(() => `theme-${this.currentTheme()}`);
    constructor() {
    // Listen for system theme changes
        const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
        this.systemTheme.set(mediaQuery.matches ? "dark" : "light");
        mediaQuery.addEventListener("change", (e) => {
            this.systemTheme.set(e.matches ? "dark" : "light");
        });
    }

    setTheme(event: Event) {
        const target = event.target as HTMLSelectElement;
        this.selectedTheme.set(target.value as any);
    }
}

How this reactive system works:

  • State management: Two writable signals (selectedTheme and systemTheme) hold the core state. The selectedTheme tracks user preference while systemTheme reflects the OS color scheme preference.

  • Smart theme resolution: The currentTheme computed signal determines which theme to use. When the user selects auto, it defers to the system theme; otherwise, it uses their explicit choice. This computation runs automatically whenever either source signal changes.

  • CSS class generation: The themeClass computed signal transforms the current theme into a CSS class name (like theme-dark or theme-light ), creating clean separation between theme logic and styling.

  • System integration: The constructor sets up a mediaQuery listener that automatically updates the systemTheme signal when the user changes their OS dark mode preference, creating a reactive connection between system settings and your application.

  • Automatic updates: When any signal changes, all dependent computed signals recalculate automatically, and Angular updates only the affected DOM elements.

This declarative approach lets you define a theme based on various inputs, while the signals system handles all reactive updates without manual subscription management or complex state synchronization.

Form state tracking

This example showcases how signals excel at managing complex form state with real-time validation. By leveraging computed signals, we create a reactive validation system that automatically updates as users type, providing instant feedback without manual event handling or complex state synchronization.

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

@Component({})
export class ContactFormComponent {
    formData = signal<ContactFormData>({
        name: "",
        email: "",
        message: "",
    });

    // Validation signals
    isNameValid = computed(() => this.formData().name.trim().length >= 2);
    isEmailValid = computed(() =>
        /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(this.formData().email)
    );
    isMessageValid = computed(() => this.formData().message.trim().length >= 10);

    // Form state signals
    isFormValid = computed(
        () => this.isNameValid() && this.isEmailValid() && this.isMessageValid()
    );

    formErrors = computed(() => ({
        name: !this.isNameValid() ? "Name must be at least 2 characters" : "",
        email: !this.isEmailValid() ? "Please enter a valid email" : "",
        message: !this.isMessageValid() ? "Message must be at least 10 characters" : "",
    }));

    updateField(field: keyof ContactFormData, value: string) {
        this.formData.update((current) => ({
            ...current,
            [field]: value,
        }));
    }
}

interface ContactFormData {
    name: string;
    email: string;
    message: string;
}

Breaking down the reactive form architecture:

  • Centralized form state: The formData signal acts as a single source of truth for all form values, eliminating the need to track individual form controls separately and ensuring consistency across the component.

  • Individual field validation: Each validation rule becomes its computed signal (isNameValid, isEmailValid, isMessageValid). These automatically re-evaluate whenever formData changes, providing real-time validation feedback without manual triggers.

  • Composite form validation: The isFormValid computed signal combines all individual validations using logical AND. This creates a master validation state that updates automatically when any field’s validity changes, perfect for enabling/disabling submit buttons.

  • Dynamic error messages: The formErrors computed signal generates user-friendly error messages based on validation state. It returns empty strings for valid fields and descriptive messages for invalid ones, making it easy to display conditional error UI.

  • Immutable updates: The updateField method uses the signal’s update function with immutable patterns to modify form data. This ensures Angular’s change detection can track modifications efficiently while maintaining predictable state transitions.

  • Type safety: The ContactFormData interface provides compile-time safety for field names and types, preventing typos and ensuring consistency.

Why this pattern is powerful:

  • Zero boilerplate: No need for complex form builders, validators, or subscription management

  • Instant reactivity: Validation updates happen automatically as users type

  • Performance optimized: Only UI elements that depend on changed validations re-render

  • Easily testable: Pure computed functions make unit testing straightforward

  • Composable: Individual validation signals can be reused across different components

This transforms form handling from an imperative, event-driven process into a declarative, reactive system that’s more maintainable and performant.

Conclusion

Angular signals are more than just a new feature; they’re a paradigm shift in building reactive Angular apps. By mastering writable, computed, and linked signals, you can simplify your codebase, improve performance, and future-proof your applications.

Here are the key points to remember:

  • Signals provide fine-grained reactivity that optimizes change detection automatically

  • Use computed signals for derived state and effects only for side effects

  • The signal trinity (signals, computed, effects) covers most reactive programming needs

  • Signals complement RxJS rather than replacing it entirely

  • New features like linkedSignal and resources expand signals into complex state management scenarios

  • Performance benefits are significant in large applications with complex component trees

As you embark on your signals journey, start small: convert a simple component to use signals, then gradually apply these patterns to larger parts of your application. Angular signals are your gateway to building more responsive, maintainable, and performant web applications.

Ready to take your Angular skills to the next level? Explore how Syncfusion® Angular components integrate seamlessly with signals for even more power. If you require any assistance, please don’t hesitate to contact us via our support forum. We are always eager to help you!

##Related Blogs

This article was originally published at Syncfusion.com.

0
Subscribe to my newsletter

Read articles from syncfusion directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

syncfusion
syncfusion

Syncfusion provides third-party UI components for React, Vue, Angular, JavaScript, Blazor, .NET MAUI, ASP.NET MVC, Core, WinForms, WPF, UWP and Xamarin.