When Angular was first introduced, it brought with it a reactivity model deeply intertwined with Zones and an event-driven change detection system. As the framework evolved, developers often turned to RxJS to manage complex asynchronous flows, state changes, and UI reactivity. RxJS became the de facto tool for anything remotely reactive in Angular applications. However, as applications scaled, many developers encountered the same challenge: the abundance of boilerplate code, the difficulty of tracing data flows, and the steep learning curve associated with mastering streams, subscriptions, and operators.

Today, Angular enters a new era of reactivity with the introduction of Signals. Designed to be intuitive, explicit, and fine-grained, Signals offer a new reactive primitive built into the core of Angular itself. Signals aren't just a minor addition; they represent a fundamental shift in how you model data and UI state in Angular applications. For the first time, developers have a tool that is both simple enough for local state management and powerful enough to replace large chunks of RxJS boilerplate in the UI layer.

In this article, I'll take a deep dive into Angular Signals, not merely as a new API, but as a new way of thinking about building applications. I'll explore their core primitives, signal(), computed(), and effect(), and understand how they seamlessly fit into Angular's vision for a zoneless, standalone component-driven future. I'll introduce the concept of LinkedSignal, a powerful feature that dramatically improves component-to-component reactivity without the tedious ceremony developers are used to.

This journey isn't about pitting Signals against RxJS in a binary competition. RxJS still has its place for modelling complex event streams, real-time data, and sophisticated asynchronous workflows. Rather, this article will help you understand where Signals excel, where RxJS remains essential, and how the two can coexist in modern Angular applications. Through real-world examples, including building typeahead inputs, form handling, route guards, and data fetching with the new HttpResource, you'll see how Signals simplify common front-end challenges, eliminate boilerplate, and enhance maintainability.

As I proceed, I'll also dive into the performance optimizations under the hood, the new mental models developers must adopt, and strategies for gradually migrating existing Angular applications to embrace this simpler, more predictable reactivity model. I'll examine not only the technical benefits but also the profound impact on developer experience, debugging, testing, and scalability.

By the end of this exploration, it should become clear: Signals are not just another feature. They are the key to a leaner, cleaner, and more intentional Angular. They mark the end of unnecessary RxJS boilerplate in the UI, and the beginning of a new, more elegant way to build Angular applications.

Core Concepts: signal(), computed(), effect()

Angular's new reactive system is built on three essential primitives: signal(), computed(), and effect(). These primitives create a direct and predictable relationship between application state and the user interface, streamlining how changes flow through an Angular application.

A signal() is the starting point. It encapsulates a single piece of state, providing both a getter and setter for its value. When a signal's value is updated, Angular automatically tracks which parts of the application have read from it and triggers updates precisely where needed. This behavior removes the need for explicit subscriptions, manual event listeners, or redundant change detection triggers. A signal can be thought of as a reactive variable, but one that carries with it a deep understanding of its consumers across the application.

Built on top of signals are computed() values, which represent derived data. A computed() function automatically recalculates itself whenever any of the signals it depends on change. Unlike manual recalculations sprinkled throughout component logic, computed() values offer a declarative way to express relationships between different pieces of state. This encourages a design where derived data is defined once and reused consistently, without risk of stale values or unintended inconsistencies. Computed values are lazy by design, updating only when actively used, which leads to highly efficient UI updates.

The final primitive, effect(), introduces controlled side effects into the reactive system. An effect() function runs whenever one of its dependent signals changes, executing logic that falls outside pure state transformations. This is where Angular developers can interact with services, perform DOM manipulations, or trigger external processes in a safe, signal-driven manner. Effects provide a clean boundary between pure reactivity and the inevitable imperative code that all real-world applications require. Effects must be used inside an injection context, such as the constructor.

What makes this system particularly powerful is Angular's runtime dependency tracking. Developers do not need to declare what to watch or remember to clean up listeners. Angular automatically infers dependencies at runtime, ensuring that only the necessary parts of the application re-run when data changes. This eliminates a common class of bugs where stale subscriptions, forgotten listeners, or over-triggered change detection lead to unpredictable behavior and performance issues.

Rather than introducing an entirely new way of programming, these primitives reinforce principles that developers already know: manage state carefully, derive values logically, and trigger side effects intentionally. What changes is the level of mechanical sympathy Angular now provides natively, resulting in simpler, safer, and more performant applications.

To visualize this flow, consider a simple example. Imagine a count signal that feeds into a doubleCount computed value, which in turn triggers a DOM update through an effect(). This automatic dependency wiring ensures that when count changes, everything downstream updates precisely once, without the developer having to orchestrate the process manually.

To visualize how these primitives work together in practice, consider the following simple Angular component example. Here, you create a count signal, derive a doubleCount computed value from it, and set up an effect() that logs whenever the derived value changes.

Component:

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

@Component({...})
export class CounterComponent
{
    // 1. Create a base signal
    const count = signal(0);

    // 2. Create a computed value based on the signal
    const doubleCount = computed(() => count() * 2);

    // 3. Create an effect that reacts to changes
    constructor()
    {
        effect(() => {
            console.log(`Double count is now: ${doubleCount()}`);
        });
    }

    increment()
    {
        // Example mutation
        // Console output: Double count is now: 10
        count.set(5);
    }
}

Template:

<div>
  <h2>Simple Counter Example</h2>
    <p>Count: {{ count() }}</p>
    <p>Double Count: {{ doubleCount() }}</p>
    <button (click)="increment()">Increment</button>
</div>

In the example above, I create a simple CounterComponent using Angular Signals. The count signal holds the primary state, and doubleCount is derived from it using computed(). An effect() runs inside the constructor to log whenever doubleCount changes.

In the template, notice how accessing the value of a signal simply involves calling it as a function with parentheses: count() and doubleCount(). No async pipes, no subscriptions, no observable unwrapping. Updating the state is just as straightforward: calling increment() mutates the signal, triggering all dependent computations and effects automatically.

LinkedSignal: Inputs, Reusability, and Component Boundaries

Angular Signals have already shown their power in simplifying local state management with signal(), computed(), and effect(). But what happens when you need to connect components across boundaries, handle dynamic inputs, or create reusable reactive logic? Enter LinkedSignal, a lesser-known but transformative feature that ties component inputs to reactive state, slashing the boilerplate traditionally associated with RxJS or manual input tracking. In this section, I'll explore how LinkedSignal streamlines component-to-component reactivity, enhances reusability, and respects Angular's component-driven architecture all while keeping things intuitive and predictable.

What Is a LinkedSignal?

A LinkedSignal is a reactive primitive that binds a component's input (defined as a Signal) to a computed value, automatically updating whenever the input changes. Introduced alongside Angular's Signal API, LinkedSignal (created via linkedSignal()) acts like a computed() value but is specifically designed to work with input Signals. It eliminates the need for lifecycle hooks, manual subscriptions, or RxJS Observables to track input changes, providing a declarative, signal-driven approach to transforming incoming data.

Think of LinkedSignal as a bridge: It connects the external world of component inputs to the internal reactive system of a component, ensuring seamless data flow without the ceremony of traditional Angular approaches. This makes it ideal for scenarios where components need to react to parent-supplied data while maintaining encapsulation and reusability.

Let's start with a practical example to see LinkedSignal in action. Imagine a UserDisplay component that receives a userId input and needs to display a derived value, such as a formatted username. Here's how it looks with LinkedSignal:

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

@Component({
    selector: 'app-user-display',
    template: `
        <h3>User Display</h3>
        <p>User ID: {{ userId() }}</p>
        <p>Formatted Name: {{ formattedName() }}</p>
    `,
    standalone: true,
})
export class UserDisplayComponent {
    userId = input<number>(0); // Input as a Signal

    formattedName = linkedSignal(() => {
        const id = this.userId();
        return id ? `User-${id}` : 'Unknown User';
    });
}

In a parent component, you can pass the userId like this:

import { Component, signal } from '@angular/core';
import { UserDisplayComponent } from './user-display.component';

@Component({
    selector: 'app-parent',
    template: `
        <app-user-display [userId]="userId()"></app-user-display>
        <button (click)="changeUserId()">Change User</button>
    `,
    standalone: true,
    imports: [UserDisplayComponent],
})
export class ParentComponent {
    userId = signal(1);

    changeUserId() {
        this.userId.set(this.userId() + 1);
    }
}

Here's what's happening:

  • The userId input is defined as a Signal using input<number>(0).
  • The formattedName is a LinkedSignal that transforms userId into a formatted string.
  • When the parent updates userId (e.g., via a button click), the userId Signal in the child updates, and formattedName recomputes automatically.
  • The template binds to both userId() and formattedName(), rendering updates without async pipes or subscriptions.

Compare this to the RxJS equivalent, where you'd need a BehaviorSubject, a pipe with map, and lifecycle hooks to manage subscriptions and cleanup. LinkedSignal delivers the same reactivity with a fraction of the code, aligning perfectly with Angular's push toward simplicity.

Reusability: Encapsulating Reactive Logic

One of the standout features of LinkedSignal is its ability to encapsulate reusable reactive logic. By defining LinkedSignal computations in utility functions or services, you can share state transformations across components, reducing duplication and improving maintainability. This is especially valuable in large applications or design systems, where consistent reactivity is critical.

Let's refactor the previous example to make the formatting logic reusable:

import { Signal, linkedSignal } from '@angular/core';

export function formatUserId(userId: Signal<number>): Signal<string> {
    return linkedSignal(() => {
        const id = userId();

        return id ? `User-${id}` : 'Unknown User';
    });
}

Now, any component can use this utility:

import { Component, input } from '@angular/core';
import { formatUserId } from './user.utils';

@Component({
    selector: 'app-user-display',
    template: `
        <h3>User Card</h3>
        <p>{{ formattedName() }}</p>
    `,
    standalone: true,
})
export class UserCardComponent {
    userId = input<number>(0);

    formattedName = formatUserId(this.userId);
}

This approach mirrors the modularity Angular developers love in services or pipes but leverages Signals' reactive nature. It's a clean, reusable way to handle input-driven state without tying logic to a specific component, making it ideal for shared libraries or design systems.

Crossing Component Boundaries

LinkedSignal truly shines when managing reactivity across component boundaries. In traditional Angular, passing reactive data between components often meant using RxJS Observables, which required careful subscription management to avoid memory leaks. LinkedSignal simplifies this by leveraging Signal-based inputs, ensuring reactivity flows naturally from parent to child without manual orchestration.

Consider a dashboard where a parent component passes a selected item's ID to a child component for display:

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

@Component({
    selector: 'app-item-detail',
    template: `
        <h3>Item Detail</h3>
        <p>Item ID: {{ itemId() }}</p>
        <p>Detail: {{ itemDetail() }}</p>
    `,
    standalone: true,
})
export class ItemDetailComponent {
    itemId = input<number>(0);

    itemDetail = linkedSignal(() => {
        return `Detail for Item ${this.itemId()}`;
    });
}

@Component({
    selector: 'app-dashboard',
    template: `
        <app-item-detail [itemId]="selectedId"> </app-item-detail>
        <button (click)="selectNext()">
          Next Item
        </button>
    `,
    standalone: true,
    imports: [ItemDetailComponent],
})
export class DashboardComponent {
    selectedId = signal(1);

    selectNext() {this.selectedId.set(this.selectedId() + 1);}
}

In this setup:

  • The parent's selectedId Signal drives the child's itemId input.
  • The child's itemDetail LinkedSignal reacts to itemId changes, computing a derived value.
  • Updates flow automatically when selectedId changes, itemId and itemDetail update, and the template reflects the new state.

This eliminates the need for RxJS Subjects, subscriptions, or lifecycle hooks, creating a clean, predictable data flow. It also respects component encapsulation, as the child's logic remains self-contained and unaware of the parent's implementation. Signals also respect Angular's change detection boundary optimizations automatically, without additional developer configuration.

LinkedSignal vs. RxJS: A Side-by-Side Comparison

To appreciate how LinkedSignal reduces complexity, let's rewrite the dashboard example using RxJS without Signals. This approach relies on traditional Angular patterns with Observables, showcasing the boilerplate that LinkedSignal eliminates.

Listing 1 shows the RxJS version of the ItemDetailComponent and DashboardComponent.

Listing 1: The RxJS version of the ItemDetailComponent and DashboardComponent

import {
    Component,
    Input,
    OnInit,
    OnDestroy
} from '@angular/core';
import {
    BehaviorSubject,
    Subscription
} from 'rxjs';
import { map } from 'rxjs/operators';

@Component({
    selector: 'app-item-detail',
    template: `
        <h3>Item Detail</h3>
        <p>Item ID: {{ itemId() }}</p>
        <p>Detail: {{ itemDetail() }}</p>
    `,
    standalone: true,
})
export class ItemDetailComponent implements OnInit, OnDestroy {
    @Input() set itemId(value: number) {
        this.itemIdSubject.next(value);
    }

    itemId: number;
    itemDetail: string;
    private itemIdSubject = new BehaviorSubject<number>(0);
    private subscription: Subscription;

    ngOnInit() {
        this.subscription = this.itemIdSubject.pipe(
            map(id => `Detail for Item ${id}`)
        ).subscribe(detail => {
            this.itemId = this.itemIdSubject.value;
            this.itemDetail = detail;
        });
    }

    ngOnDestroy() {
        this.subscription.unsubscribe();
    }
}

@Component({
    selector: 'app-dashboard',
    template: `
        <app-item-detail [itemId]="selectedId()"></app-item-detail>
        <button (click)="selectNext()">
            Next Item
        </button>
    `,
    standalone: true,
    imports: [ItemDetailComponent],
})
export class DashboardComponent {

    selectedId = 1;

    selectNext() {
        this.selectedId++;
    }
}

Let's break down the differences:

  • Boilerplate: The RxJS version requires a BehaviorSubject to track itemId changes, a pipe with map to transform the value, and explicit subscription management in ngOnInit and ngOnDestroy. This adds significant overhead compared to the LinkedSignal version, which handles reactivity declaratively.
  • Lifecycle Management: With RxJS, you must manually unsubscribe to prevent memory leaks, whereas LinkedSignal leverages Angular's automatic dependency tracking, eliminating lifecycle hooks.
  • State Handling: The RxJS approach uses a plain number (selectedId) in the parent, requiring manual updates to propagate changes. Signals, on the other hand, ensure reactivity flows naturally from parent to child.
  • Template Simplicity: In the RxJS version, the template binds to plain properties (itemId and itemDetail), but the logic to keep them in sync is hidden in the component. With LinkedSignal, the template binds directly to Signals (itemId() and itemDetail()), making the reactivity explicit and predictable.

LinkedSignal is more than a convenience: It's a step toward Angular's vision of a simpler, more intentional reactivity model. By replacing RxJS-driven input handling with a Signal-based alternative, it reduces boilerplate, eliminates common pitfalls (like forgotten unsubscribes), and aligns with the zoneless, fine-grained change detection Angular is moving toward. It also empowers developers to write reusable, modular code that scales gracefully in large applications.

Compared to RxJS, LinkedSignal is less powerful for complex async workflows (e.g., debouncing or combining streams), but it's unmatched for UI-driven reactivity. As I'll explore in the next section, Signals and RxJS can coexist, with LinkedSignal handling component state and RxJS tackling advanced event streams.

This comparison highlights why Signals, and LinkedSignal in particular, are a game-changer for Angular developers. They eliminate the need for RxJS in many UI-driven scenarios, reducing complexity and making code easier to reason about.

When to use LinkedSignal:

  • Dynamic Inputs: Use LinkedSignal when a component needs to react to input changes in a declarative way.
  • Reusable Logic: Encapsulate state transformations in utilities for shared components or libraries.
  • Component Communication: Simplify parent-child reactivity without RxJS or lifecycle hooks.
  • Simplicity: Opt for LinkedSignal when you want reactive UI updates without the overhead of Observables.

If you need advanced stream manipulation, stick with RxJS or combine it with Signals (e.g., using toSignal() to convert Observables). The beauty of Angular's approach is that you're not forced to choose; you can use the right tool for the job.

Table 1 underscores how LinkedSignal simplifies the developer experience, making reactivity intuitive and reducing the potential for errors like memory leaks or stale data.

Signals vs. RxJS: When to Use What

The introduction of Angular Signals raises a natural question for developers who have long relied on RxJS for reactivity: Where do these two powerful systems overlap, and where does each shine? Although Signals and RxJS may appear similar at first glance, especially to those new to reactive programming, they are fundamentally designed for different purposes. Understanding when to reach for Signals versus when to embrace the full power of RxJS is essential for building clean, maintainable, and high-performance Angular applications.

At their core, Signals are about state management. They provide a simple, synchronous, and fine-grained mechanism to model local or UI-driven state within a component or a closely related set of components. Signals excel when you need a value to update predictably in response to user interaction, a parent input, or a computed relationship between other pieces of state. Because Signals are synchronous and immediately reflect their latest value, they feel intuitive for tasks like form control, toggling UI elements, managing user selections, or deriving secondary state based on one or more primary signals.

RxJS, on the other hand, was built to model streams over time. It shines in situations where you are dealing with asynchronous flows, cancellation, complex coordination between multiple sources, debouncing, throttling, retries, backoff strategies, or real-time event streams like WebSocket connections. RxJS offers operators like switchMap, mergeMap, combineLatest, and retryWhen, which are indispensable for sophisticated asynchronous workflows that extend beyond simple UI reactivity. Unlike Signals, observables are inherently lazy and asynchronous, meaning they only emit when subscribed to, and they can model both discrete events and continuous processes over time.

In a typical modern Angular application, a pragmatic division of responsibilities emerges. Signals dominate inside components. They drive the UI, handle local state, manage form values, and compute derived properties for rendering. RxJS remains critical outside components, particularly in services and infrastructure layers, where you interact with external systems like HTTP APIs, WebSockets, authentication providers, and large event-based architectures. This separation of concerns aligns beautifully with Angular's architectural vision: Signals simplify the reactive story at the UI layer, while RxJS remains a powerful tool for managing complex, asynchronous business logic and system interactions.

Consider a simple typeahead search feature. With Signals, you can model the current input value and the loading state of the UI. When the user types, a signal() holds the latest query string, and a computed or effect can trigger a data fetch. However, if you want to debounce the input (e.g., wait 300ms after typing stops) before triggering a search, you'd still lean on RxJS's debounceTime() operator, possibly converting the debounced stream back into a signal using toSignal(). This hybrid approach leverages the strengths of both systems: Signals for clean, declarative state updates, and RxJS for precise timing control over asynchronous user input.

It's important to adopt the right mental model when working with these tools. With Signals, you think in terms of values and direct dependencies. A change to one value immediately flows through all dependent computations and effects. With RxJS, you think in terms of events and transformations over time, where data may be buffered, delayed, canceled, or merged with other streams before reaching its destination. Signals offer immediacy and predictability, and RxJS offers flexibility and control over complex asynchronous flows.

Rather than replacing RxJS, Signals complement it by handling the vast majority of simpler, synchronous UI state management scenarios that previously required verbose observable pipelines. The result is cleaner, more declarative components with less boilerplate, fewer subscriptions to manage, and a clearer separation between UI and service layers. Developers no longer need to force-fit RxJS into every part of their applications, freeing RxJS to focus on the scenarios where its strengths truly matter.

As Angular continues to evolve, the trend is clear: Signals will increasingly be the default choice for most UI-driven reactivity, and RxJS will remain a vital tool for dealing with asynchronous complexity. Mastering both and knowing when to apply each is the key to building robust, scalable, and maintainable Angular applications in the Signals era.

In large-scale Angular applications, state management libraries like NgRx provide a concrete example of where RxJS continues to play a critical role. NgRx's traditional Store, Effects, and Entity modules are deeply based on RxJS streams, and they remain an excellent choice for complex, event-driven architectures requiring time-based event coordination, advanced side effects handling, and scalability across large teams.

That said, even within the NgRx ecosystem, Signals are beginning to reshape the reactive story. The recently introduced SignalStore package offers a new, Signals-first API for state management, blending the fine-grained reactivity of Signals with the robustness of NgRx tooling. SignalStore internally leverages some RxJS capabilities, but developers interact with state primarily through Signals and computed properties, drastically reducing observable boilerplate for common patterns.

This evolution illustrates Angular's broader reactive future: RxJS remains essential for advanced workflows, and Signals dramatically simplify UI and local state management, even inside libraries traditionally built entirely on observables.

Real-World Patterns with Signals

Theory alone cannot capture the transformative simplicity that Angular Signals bring to modern front-end development. Real power comes from seeing how common patterns, often complex and verbose when built with traditional observables, become drastically simpler, more declarative, and more maintainable when rebuilt using Signals. In this section, I'll explore practical, real-world problems that every Angular developer encounters and show how Signals provide clean solutions that reduce boilerplate while enhancing clarity and performance.

A Reactive Typeahead Input

The typeahead pattern, where users type into a search box and see suggestions appear, is a classic front-end challenge. Traditionally, building a robust typeahead involved juggling several concerns: capturing user input, debouncing rapid keystrokes, handling loading states, triggering API calls, and rendering the results reactively. In a pure RxJS approach, this often required chaining multiple operators, carefully managing subscriptions, and dealing with race conditions manually. With Signals, much of this complexity falls away.

Local UI state, such as the current search term, the loading indicator, and the list of search results, can be modelled directly as Signals. As previously explained, a signal() can track the user's input in real-time, a computed() can derive debounced input if needed, and an effect() can handle fetching new data when the debounced input changes.

Listing 2 shows a simple TypeaheadComponent.

Listing 2: A simple TypeaheadComponent

import { Component, effect, signal } from '@angular/core';
import { toObservable, toSignal } from '@angular/core/rxjs-interop';
import { debounceTime } from 'rxjs/operators';

@Component({
    selector: 'app-typeahead',
    standalone: true,
    template: `
      <input
        #searchInput
        type="text"
        [value]="query()"
        (input)="updateQuery(searchInput.value)"
        placeholder="Search..."
      />

      @if(loading()) {
        <p>Loading...</p>
      }

      <ul>
        @for(item of results(); track item) {
          <li>{{ item }}</li>
        }
      </ul>
    `,
})
export class TypeaheadComponent {
    query = signal('');
    loading = signal(false);
    debouncedQuery = toSignal(
        toObservable(this.query).pipe(debounceTime(300)),
        {
            initialValue: '',
        }
    );

    results = signal<string[]>([]);

    constructor() {
        effect(() => {
            const search = this.debouncedQuery();
            if (!search) {
                this.results.set([]);
                return;
            }

            this.loading.set(true);

            const mockResults = [
                'Apple',
                'Banana',
                'Cherry',
                'Date',
                'Elderberry',
                'Fig',
                'Grape',
                'Honeydew',
            ];

            // Simulating a delay to mimic an HTTP request
            setTimeout(() => {
                this.results.set(
                    mockResults.filter((item) =>
                        item.toLowerCase().includes(search.toLowerCase())
                    )
                );
                this.loading.set(false);
            }, 3000);
        });
    }

    updateQuery(value: string) {
        this.query.set(value);
    }
}

In this example:

  • The user's raw input is captured in the query signal.
  • To debounce rapid input changes, toObservable(this.query) converts the signal into an observable, which is then piped through debounceTime(300). The debounced observable is converted back into a signal using toSignal(), resulting in a reactive, debounced query value.
  • An effect() monitors the debounced query. When it changes, the component triggers a new HTTP request to fetch search results.
  • The loading and results states are managed as Signals, ensuring the template updates automatically without async pipes or manual subscription management.

This design leads to a smooth, declarative flow where Signals manage state, effects handle side effects, and the template reflects the latest state naturally. Compared to traditional RxJS-heavy approaches, the code is cleaner, easier to understand, and easier to maintain.

The Typeahead pattern demonstrates a larger principle: by combining Signals for local UI state and selectively using RxJS for stream transformations, Angular developers can build dynamic, highly responsive applications without the ceremony and cognitive overhead of earlier reactive patterns.

Conditional Rendering Using the New Control Flow Syntax

Angular's new control flow syntax, introduced alongside standalone components and Signals, unlocks a far more declarative and composable way to handle conditional rendering in templates. Previously, developers relied heavily on structural directives like *ngIf and *ngFor, which, while powerful, could become verbose and hard to nest cleanly inside more reactive patterns.

Signals integrate naturally with the new control flow syntax, allowing for fine-grained template updates based on reactive state changes without the ceremony of traditional observables or manual event handling. By using Signals as the source of truth for visibility conditions, Angular can optimize change detection and DOM updates, resulting in both cleaner code and better runtime performance.

Consider a simple example: a component that toggles between showing a login form and a dashboard view based on a user's authentication status.

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

@Component({
    selector: 'app-login-dashboard',
    standalone: true,
    template: `
        @if (isAuthenticated()) {
            <h2>Welcome Back!</h2>
            <button (click)="logout()">Logout</button>
        } @else {
            <h2>Please Log In</h2>
            <button (click)="login()">Login</button>
        }
    `
})
export class LoginDashboardComponent {
    isAuthenticated = signal(false);

    login() {
        this.isAuthenticated.set(true);
    }

    logout() {
        this.isAuthenticated.set(false);
    }
}

In this example:

  • The isAuthenticated signal drives the template logic reactively.
  • Angular's @if ... @else control flow blocks switch the rendered content based on the current authentication state.
  • Because isAuthenticated is a signal, Angular automatically knows when to rerender the correct block, without needing any explicit calls to ChangeDetectorRef, manual subscriptions, or async pipes.

The tight integration between control flow syntax and Signals allows developers to think more declaratively about UI states. Instead of wiring conditions manually in the component class, developers express their intent directly in the template, powered by reactive primitives. This approach reduces boilerplate, improves readability, and enables Angular's fine-grained reactivity system to optimize DOM operations at runtime.

By combining Signals with the new control flow syntax, Angular developers can build highly dynamic interfaces that remain clean, predictable, and maintainable even as applications grow in complexity.

Guard Logic with computed() for Route Protection

Routing in Angular often involves protecting certain routes based on application state: whether a user is authenticated, has a certain role, or has completed an onboarding flow. Traditionally, route guards were written imperatively, relying on injected services and manually checking conditions in canActivate() methods, often returning Booleans or observables.

With Signals and computed(), Angular developers can now model guard conditions reactively, creating a clean, declarative way to manage access control based on live application state. Instead of scattering authorization logic across services and guards, Signals allow for centralizing and composing guard conditions in a predictable and testable way.

Here's a practical example: protecting a dashboard route based on whether a user is logged in.

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

import {
    CanActivateFn,
    Router
} from '@angular/router';

@Injectable({
    providedIn: 'root'
})
export class AuthService {
    user = signal<{
        id: number,
        name: string
    } | null>(null);

    isLoggedIn = computed(() => !!this.user());
}

export const canActivateDashboard: CanActivateFn = (route, state) => {
    const authService = inject(AuthService);
    const router = inject(Router);

    if (authService.isLoggedIn()) {
        return true;
    } else {
        router.navigate(['/login']);
        return false;
    }
};

In this example:

  • The AuthService uses a signal() to track the current user state.
  • A computed() signal isLoggedIn derives its truthiness based on whether the user is present.
  • The route guard canActivateDashboard reads the computed signal directly at the time of route activation.
  • If the user is not logged in, the guard redirects them to the login page.

This approach is both more declarative and more reactive than traditional service-based guards. If additional rules were needed, such as checking user roles or subscription status, new computed signals could be composed cleanly on top of the existing ones, without breaking the reactive graph or introducing side effects.

Signals make guard logic more transparent, more testable, and better aligned with Angular's direction toward fine-grained, zoneless architectures. They reduce the imperative ceremony around route protection and allow guards to respond naturally to the live, reactive state of the application.

HttpResource: Signal-Based Data Fetching

Data fetching has always been a central part of building Angular applications. Managing HTTP requests, tracking loading states, handling errors, caching responses, and updating the UI have historically required developers to combine several concepts, including HttpClient, observables, subscriptions, and manual state management. Although these tools are powerful, they often lead to verbose patterns, especially in components where fine-grained reactivity is critical.

In Angular 19.2, a new experimental feature called HttpResource was introduced to simplify this problem dramatically. HttpResource is a signal-powered abstraction over data fetching, designed to handle the common lifecycle concerns of remote requests, loading, success, error handling, and caching without requiring developers to manually manage subscriptions or async pipes.

HttpResource is created by calling the httpResource() function from @angular/core. This function takes a fetcher function as input, typically one that uses HttpClient to return an observable or promise. In return, it provides several Signals that automatically track the status of the request:

  • data() A signal holding the successfully fetched data.
  • error() A signal holding any error encountered during fetching.
  • status() A signal indicating the current fetch state (initial, pending, success, or error).
  • isLoading() A Boolean signal that becomes true during a request.
  • reload() A method to manually re-trigger the fetch.

HttpResource fits perfectly into Angular's broader move toward fine-grained, declarative reactivity. Instead of wiring manual subscriptions, managing multiple loading flags, and manually triggering change detection, HttpResource allows developers to work with Signals directly, bringing a new level of simplicity and predictability to data fetching.

Let's revisit our earlier TypeaheadComponent and enhance it using httpResource(), as shown in Listing 3.

Listing 3: Enhanced TypeaheadComponent

import { httpResource } from '@angular/common/http';
import { Component, computed, signal } from '@angular/core';
import { toObservable, toSignal } from '@angular/core/rxjs-interop';
import { debounceTime } from 'rxjs';

@Component({
    selector: 'app-typeahead-with-http-resource',
    imports: [],
    template: `
      <h2>Typeahead with HTTP Resource</h2>

      <input
          #searchInput
          type="text"
          [value]="query()"
          (input)="updateQuery(searchInput.value)"
          placeholder="Search..."
      />

      @if (isLoading()) {
          <p>Loading...</p>
      } @else if (error()) {
          <p>Error loading results.</p>
      } @else {
          <ul>
              @for(item of products() || []; track item) {
                  <li>{{ item }}</li>
              }
          </ul>
      }
    `,
})
export class TypeaheadWithHttpResourceComponent {
    query = signal('');

    debouncedQuery = toSignal(
        toObservable(this.query).pipe(debounceTime(300)),
        {
            initialValue: '',
        }
    );

    #searchResource = httpResource<{
        products: { id: number; title: string }[];
    }>(() => {
        if (!this.debouncedQuery()) {
            // Skip the request if the query is empty
            return undefined;
        }

        const query = encodeURIComponent(this.debouncedQuery());
        const url = `…/search?q=${query}`;
        return url;
    });

    isLoading = computed(() => this.#searchResource.isLoading());
    error = computed(() => this.#searchResource.error());
    data = this.#searchResource.value;

    products = computed(() => {
        return (
            this.data()?.products.map((product) => product.title) || []
        );
    });

    updateQuery(value: string) {
        this.query.set(value);
    }
}

In this version:

  • User input is captured via the query signal.
  • The input is debounced using toObservable and toSignal.
  • The additional check for the debounceQuery Signal ensures that the request is not executed until that Signal has been set.
  • httpResource() watches the debounced query and automatically triggers a new HTTP request whenever it changes.
  • The template uses Signals exposed by HttpResource (isLoading(), error(), value()) to declaratively render the appropriate UI.

There is no need for subscriptions, manual loading flags, or defensive async pipes. State changes propagate naturally through Signals, making the component smaller, clearer, and easier to maintain.

HttpResource also introduces built-in caching. If the same parameters are used again, Angular avoids re-sending the request unless explicitly instructed to reload(). Error handling is first-class as well: Developers can easily respond to fetch failures without extra boilerplate or duplicated error checks in the template.

In short, HttpClient is still the right choice when:

  • You're making API calls inside a service layer.
  • You need full control over request/response pipelines.
  • You're composing complex RxJS streams (e.g., retries, joins, batching).

HttpResource is ideal when:

  • You want to fetch data directly inside a component.
  • You need automatic loading/error handling.
  • You want the data-fetching state to drive the UI declaratively.
  • You want minimal boilerplate and maximum clarity.

HttpResource enables a new mental model for front-end developers: Treat remote data as live state, just like local variables, and let the framework handle the complexities of the HTTP lifecycle reactively and predictably.

Gradual Migration Strategies

One of the key strengths of Angular's Signals system is that it was designed with gradual adoption in mind. Unlike some previous architectural shifts that required all-or-nothing rewrites, Angular Signals allow teams to introduce fine-grained reactivity incrementally, alongside existing codebases built with RxJS, @Input()/@Output(), and traditional change detection mechanisms. This flexibility ensures that projects of any size, complexity, or legacy burden can benefit from Signals without introducing instability or major technical debt.

Signals can coexist peacefully with existing patterns because Angular provides deliberate interop bridges between the old and the new. Developers can convert an observable into a signal using toSignal() or expose a signal as an observable using toObservable(). Similarly, traditional @Input() bindings can be upgraded to input() to create linked signals reactively, without breaking component APIs. Effects and computeds can live side-by-side with older imperative event handlers, making migration paths organic rather than disruptive.

A pragmatic migration strategy often begins at the component level. Instead of trying to refactor entire services or state management layers immediately, developers can first refactor simple, isolated components that primarily deal with local UI state. Components with form controls, visibility toggles, tab switching logic, or basic computed displays are perfect candidates. These migrations tend to be straightforward: replace class properties with signal(), replace derived getters with computed(), and replace imperative methods like markForCheck() with reactive effect() blocks.

After developers learn to manage local state using Signals, the next step is to connect service-layer observables to signals. By using the toSignal() function, you can convert an RxJS stream from a service into a reactive signal within your component. This keeps your component signal-based and removes the need to handle observables directly. As a result, template bindings become much simpler: Instead of using async pipes and checking for null values, you can bind directly to signal values using the {{ signal() }} syntax.

As adoption grows, projects can begin to upgrade critical interaction points like route guards, loading states, and even reactive forms to signal-driven versions. New features and modules can be built natively with Signals, and legacy areas of the application continue using observables and services as before. This incremental opt-in model ensures that teams never need to pause feature development simply to adopt Signals; they can phase in improvements naturally over time.

One of the most effective ways to begin migrating to Signals is by targeting simple, low-risk parts of the application first. Local state variables such as counters, toggles, form field values, or loading flags can be easily rewritten as signal() instances with minimal code changes. The accompanied code contains several examples of these using Signals. Derived state, often expressed today as getters or manual recalculations inside methods, can be naturally upgraded to computed() signals, offering cleaner declarative data flows. Child components that currently use @Input() properties can transition to using input(), automatically gaining reactive linked signals without breaking their external API. In templates, structural directives like *ngIf and *ngFor can be rewritten using the new @if and @for control flow syntax, unlocking finer-grained rendering optimizations driven by signals rather than coarse view checks. By starting with these accessible, low-complexity migrations, teams can quickly build confidence with Signals while laying the groundwork for broader adoption across larger, more critical parts of the codebase.

It's important, however, to be thoughtful during migration. Signals and RxJS are powerful tools but solve slightly different problems. Replacing observables purely for the sake of using Signals can lead to unnecessary complexity, especially in asynchronous, event-driven workflows where RxJS still excels. A good rule of thumb is to use Signals for local synchronous UI state and RxJS for asynchronous operations, system events, and service communications.

A good rule of thumb is to use Signals for local synchronous UI state and RxJS for asynchronous operations, system events, and service communications.

Tooling and developer ergonomics are improving as well. The Angular Language Service, component-level DevTools inspections, and community libraries are rapidly evolving to offer better support for Signals, making it easier to spot reactive flows, debug dependency graphs, and optimize fine-grained reactivity across the stack.

Ultimately, the gradual migration path for Signals is a significant achievement for Angular's ecosystem. It empowers teams to adopt the future of reactive programming on their own terms, balancing innovation with stability. By starting small, iterating carefully, and applying the right tool for each job, developers can transform Angular applications to be faster, cleaner, more declarative, and more maintainable, without ever facing a “rewrite or die” dilemma.

Signals and Performance: What Changes Under the Hood?

As with any powerful abstraction, Signals unlock new possibilities for building reactive applications, but they also introduce new pitfalls for developers unfamiliar with the model. Being aware of common mistakes early on can save teams from unnecessary complexity, performance regressions, and confusing bugs. Fortunately, most of these mistakes stem from incorrect mental models inherited from working with RxJS or traditional Angular state management.

One of the most frequent mistakes is misusing effect() for derived state. New users often reach for effect() whenever they want to compute something reactively. However, effect() is intended for triggering side effects, such as making service calls, interacting with the DOM, or dispatching actions, not for computing new values. When you need to derive a new value from one or more signals, you should always prefer computed(). This distinction ensures that your reactive graph remains pure and declarative, while side effects are isolated and intentional.

Another common trap is forgetting that Signals are synchronous. Unlike observables, Signals update immediately when their dependencies change. There is no concept of asynchronous emission inside a Signal's getter. Developers sometimes mistakenly expect Signals to behave like observables, thinking that downstream updates will be deferred or batched. This misunderstanding can lead to confusing bugs where state changes happen earlier than anticipated. When working with Signals, always assume synchronous propagation unless you intentionally introduce asynchronous behavior via RxJS operators at the service layer.

Over-engineering is another frequent issue. Signals offer a natural, lightweight way to model local and derived state, but in some cases, developers wrap simple values or create unnecessary layers of indirection “just to be reactive.” Not every piece of local data needs to be a signal; sometimes a plain field or a straightforward method is sufficient. Remember: Signals shine most when you are modeling dynamic, shared, or computed state that benefits from fine-grained reactivity.

Developers may also recreate observables unnecessarily when working with hybrid codebases. Although toObservable() is a powerful bridge, it should be used thoughtfully. Signals are optimized for direct, synchronous dependency tracking inside components. Rewrapping them into observables inside templates or unnecessary service layers often reintroduces the very complexity that Signals were designed to remove.

Finally, a subtle but important consideration is managing resource lifecycles when combining Signals with other Angular constructs. For example, when creating effects inside services that outlive their components, it's crucial to ensure that side effects and subscriptions are properly disposed of to avoid potential memory leaks. Although Angular automatically handles cleanup for effects created inside component trees, long-lived singleton services require more care. The Angular team has acknowledged the need for formal lifecycle management hooks for Signals and Effects, and this is currently under active discussion for future releases. Until such APIs are available, developers must be cautious when wiring Signals and Effects inside services or globally scoped objects.

Signals are a powerful addition to Angular's toolkit, but they demand a slight shift in mindset toward simpler, more synchronous, more declarative design. By being aware of these common mistakes early on, developers can leverage Signals to their fullest potential, building applications that are cleaner, faster, easier to reason about, and easier to scale.

Conclusion: A Simpler, More Predictable Angular

Angular's evolution has always been about balancing power with pragmatism. The introduction of Signals marks one of the most profound shifts in how developers model state, data flows, and UI reactivity within Angular applications. Rather than layering more complexity onto an already mature framework, Signals simplify. They allow developers to reason about application state synchronously, declaratively, and with an unprecedented level of fine-grained control.

Signals unlock a new reactive mental model, one that aligns with how modern users expect web applications to behave: fast, responsive, and intelligent. They remove the need for heavy-handed change detection strategies, reduce boilerplate around inputs, outputs, and subscriptions, and offer a composable, ergonomic way to build complex interfaces without drowning in streams and lifecycles.

At the same time, Signals respect Angular's commitment to stability. They do not force existing projects into painful rewrites. They provide bridges, gradual migration paths, and tooling to ensure that teams can adopt this next-generation model on their own terms. Whether you are building small UI interactions, enhancing form management, optimizing rendering, or rethinking global state management strategies, Signals open the door to simpler, more predictable, and more maintainable Angular applications.

As Angular moves toward a zone-less future, with faster initial render times, smarter hydration strategies, and enhanced concurrency, Signals will be the foundation. They are not merely another API to learn: They represent a rethinking of Angular's core reactive story. Teams that embrace Signals today will find themselves best positioned to build the fast, scalable, and resilient applications that tomorrow's web demands.

The future of Angular is reactive, declarative, and powered by Signals. It has already begun.

Although this article focused on introducing Signals and their role in modern Angular development, there are even more powerful patterns emerging. In future installments, I'll explore advanced topics such as integrating Signals with Reactive Forms, building localization workflows with reactive primitives, designing custom Signal utilities, and evolving large-scale state management strategies. Signals aren't just a feature. They're a foundation, and their possibilities are only beginning.

As it matures beyond experimental status, HttpResource promises to be a major part of building more responsive, maintainable, and performant Angular applications.

Table 1: Comparison: LinkedSignal vs. RxJS

Aspect LinkedSignal RxJS
State Management selectedId is a Signal, inherently reactive. selectedId is a plain number, requiring manual updates to propagate changes.
Input Handling itemId is an input Signal, automatically reactive with linkedSignal(). itemId uses a BehaviorSubject, requiring a setter to update the Subject manually.
Derived Values itemDetail is computed declaratively with linkedSignal(). itemDetail requires a pipe with map and a subscription to compute the derived value.
Lifecycle Hooks None needed, Angular handles reactivity automatically. Requires ngOnInit and ngOnDestroy to manage subscriptions and prevent memory leaks.
Template Binding Binds directly to Signals (itemId(), itemDetail()), explicit and reactive. Binds to plain properties, with reactivity logic hidden in the component.
Code Complexity Minimal: ~10 lines of logic, declarative. Higher: ~20 lines of logic, imperative, with manual subscription management.


Table 2: Signals, RxJS, and NgRx: Choosing the Right Tool for Reactive State Management

Aspect Signals RxJS NgRx Store NgRx SignalStore
Purpose Fine-grained local UI state, synchronous Asynchronous event streams, complex flows Global app-wide state management Global state with Signals-first API
Best for UI bindings, forms, derived state, component inputs HTTP polling, debouncing, cancellations, websockets Large applications, cross-feature communication Large applications, but with simpler Signal-driven APIs
Learning Curve Low Medium to High High (needs understanding of actions, reducers, effects) Medium (simpler API, but still NgRx patterns)
Boilerplate Minimal Moderate High Low
Zone.js dependency None None None None
Example Operators N/A (synchronous) debounceTime, switchMap, mergeMap combineReducers, createEffect createComputed, createSignalSelector
Ideal Usage Inside components In services, APIs, complex side effects Cross-app global state, enterprise apps Same, but with Signals as the first-class citizen


Table 3: A comparison between HttpClient and HttpResource

Aspect HttpClient HttpResource
State Management Manual (loading flags, error handling) Automatic via Signals (isLoading(), data(), error(), status())
Subscriptions Required (.subscribe()) Not needed, data is pushed into Signals
Caching Manual or via interceptors Built-in default caching
Template Binding async pipe often required Direct binding with signal() calls
Boilerplate Moderate to high Very low
Best For Bulk API calls, service layers, complex flows UI-driven data fetching, fine-grained component reactivity