Angular Signals and State Management (2026) - Angular tutorial cover with bold white title lettering on a dark navy grid background.

Quick answer: An Angular signal is a reactive container for a value. Read it by calling it, write it with set or update, and derive new values with computed. Angular re-renders only the parts that read a changed signal, so signals are both simpler and faster than manual change tracking. For most apps a service full of signals is enough state management; reach for NgRx when many features share complex state.The sections below cover the three Angular signals primitives, component state, sharing signals through a service, and the point where Angular signals alone are not enough and you reach for a store.

Quick answer card that answers 'angular signals' in plain language for beginners.
angular signals guide roadmap listing the 4 sections in reading order.

The three core Angular signals tools

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

const count = signal(0);                 // writable
const doubled = computed(() => count() * 2);   // derived, read-only

count.set(5);                            // replace the value
count.update(c => c + 1);                // update from the previous value

effect(() => console.log('count is', count()));  // runs when it changes

Read a signal by calling it: count(), never count. computed creates a value that recalculates automatically and caches until a dependency changes. effect runs side effects such as logging or syncing to localStorage, and Angular’s guidance is to use it sparingly — derived state should be a computed, not an effect.

Component state with Angular signals

In a component, signals replace a lot of manual wiring. Because the template reads the signal, Angular knows exactly which DOM to update.

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

@Component({
  selector: 'app-todos',
  template: `
    <input #box (keyup.enter)="add(box.value); box.value = ''" />
    <p>{{ remaining() }} of {{ todos().length }} left</p>
    @for (todo of todos(); track todo.id) {
      <label><input type="checkbox" [checked]="todo.done"
        (change)="toggle(todo.id)" /> {{ todo.title }}</label>
    }
  `,
})
export class TodosComponent {
  todos = signal<{ id: number; title: string; done: boolean }[]>([]);
  remaining = computed(() => this.todos().filter(t => !t.done).length);

  add(title: string) {
    if (!title.trim()) return;
    this.todos.update(list => [...list, { id: Date.now(), title, done: false }]);
  }

  toggle(id: number) {
    this.todos.update(list =>
      list.map(t => (t.id === id ?

{ ...t, done: !t.done } : t))); } }

Notice the immutability: update returns a new array rather than mutating the old one. Signals compare by reference, so creating a new array is what tells Angular the list changed.

angular signals concept map connecting the related topics this guide covers.

Signals in a service: state you can share

Move the same signals into a root service and you have shared application state with no library. Every component that injects the service sees the same values and updates automatically.

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

@Injectable({ providedIn: 'root' })
export class AuthService {
  private current = signal<User | null>(null);
  readonly user = this.current.asReadonly();
  readonly isLoggedIn = computed(() => this.current() !== null);

  login(user: User) { this.current.set(user); }
  logout() { this.current.set(null); }
}

Expose asReadonly() so consumers can watch the value but cannot change it from outside the service. That single habit keeps state changes auditable.

When you need NgRx

Signals in services cover the large majority of apps. NgRx earns its place when state is genuinely complex: many features read and write the same entities, you need a recorded sequence of every action, or several teams contribute to one state tree. The current NgRx is version 22 and offers two styles.

ApproachShapeBest for
Service with signalsA few signals and computed valuesMost apps; simplest to learn
NgRx SignalStoreSignal-native store with methodsFeature state that several components share
Classic NgRx StoreActions, reducers, effects, selectorsLarge apps needing a full event log and time travel

Start with signals in a service. If you find yourself writing the same update logic in several places, or needing to reason about state over time, that is the signal to introduce NgRx — not before.

Next: testing and debugging Angular apps. To learn signals and state on a real project, Ampersand Academy teaches Angular one-to-one.

angular signals guide coverage grid showing every section covered in this article.

Bridging RxJS and Angular signals for stream interoperability

Enterprise production codebases frequently maintain established RxJS architectures alongside newer reactive state primitives. Refactoring complex applications does not require rewriting functioning event streams into reactive containers overnight. Development teams integrate asynchronous streams into views smoothly by introducing well-defined boundary adapters. When upgrading data processing pipelines, Angular signals deliver clean template rendering without requiring manual subscription tracking or async pipe declarations throughout component templates.

The primary conversion utility ships within the dedicated interop package under core framework modules. Invoking toSignal subscribes to an observable stream and continuously mirrors incoming emissions into a synchronous reactive getter. Because reactive getters require immediate value availability upon initialization, components reading cold or delayed streams can throw errors. Supplying an initial configuration parameter guarantees that child components receive fallback values before remote streams emit initial payloads.

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

@Component({
  selector: 'app-metric-panel',
  template: `<p>Status: {{ status() }}</p>`,
})
export class MetricPanelComponent {
  private http = inject(HttpClient);
  status = toSignal(
    this.http.get<string>('/api/status'),
    { initialValue: 'pending' }
  );
}

import { Component, inject } from '@angular/core'; import { toSignal } from '@angular/core/rxjs-interop'; import { HttpClient } from '@angular/common/http';@Component({ selector: 'app-metric-panel', template: `<p>Status: {{ status() }}</p>`, }) export class MetricPanelComponent { private http = inject(HttpClient); status = toSignal( this.http.get<string>('/api/status'), { initialValue: 'pending' } ); }

Execution context dictates where these conversion adapters can safely initialize. Calling toSignal outside an active injection context causes immediate runtime failures unless developers pass an explicit environment injector reference. Furthermore, when unhandled source streams emit failure notifications, Angular signals intercept those errors and rethrow them during template read evaluation. Configuring safe fallback parameters avoids unhandled boundary crashes during intermittent network disruptions.

Converting Observables with toSignal

The complementary workflow surfaces when downstream libraries expect continuous event streams rather than synchronous getters. Calling toObservable constructs a stream that broadcasts updates whenever tracked dependencies produce fresh outputs. Unlike synchronous getters, this bridge utility schedules notifications across microtask boundaries. Rapid internal mutations occurring within identical execution frames collapse into consolidated emissions, maintaining high performance across high-frequency interfaces.

Managing subscription lifecycles represents another vital consideration when coordinating stream conversions. Bridges instantiated within UI components automatically register teardown routines with local view destruction hooks. In contrast, services operating at root application levels require explicit lifetime constraints to prevent abandoned listeners. Understanding these boundary relationships enables developers to combine functional stream operators with modern reactive primitives without creating hidden resource leaks.

Careful evaluation helps teams decide when to maintain streams versus when to transition to synchronous primitives:

  • Use streams for asynchronous coordination involving debounce timing, cancelable switches, or windowing operators.
  • Use reactive getters for direct UI presentation, derived view calculations, and synchronous component inputs.
  • Employ bridge converters exclusively at interface boundaries rather than alternating abstractions within single classes.

Fine-tuning reactivity with Angular signals equality checks

Default state notification logic checks value modifications through strict referential comparisons. The framework applies standard identity checks to evaluate whether downstream consumers require recalculation. However, applications processing continuous websocket messages, financial tickers, or real-time geolocation coordinates often receive freshly instantiated object wrappers containing unchanged data. Under these demanding conditions, Angular signals accept custom equality predicates to eliminate redundant re-rendering cycles.

A custom equality predicate executes synchronously whenever mutation methods supply replacement payloads to writable state containers. Supplying an equality comparison function lets engineering teams implement structural equality checks tailored to specific domain models. When architecting enterprise data models, configure Angular signals with targeted field comparisons instead of deep recursive traversals. Targeted comparisons verify critical entity keys while conserving processor overhead during high-frequency data refreshes.

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

interface GeoCoordinate {
  latitude: number;
  longitude: number;
}

export const activeLocation = signal<GeoCoordinate>(
  { latitude: 51.5074, longitude: -0.1278 },
  {
    equal: (prev, curr) =>
      prev.latitude === curr.latitude &&
      prev.longitude === curr.longitude,
  }
);

import { signal } from '@angular/core';interface GeoCoordinate { latitude: number; longitude: number; }export const activeLocation = signal<GeoCoordinate>( { latitude: 51.5074, longitude: -0.1278 }, { equal: (prev, curr) => prev.latitude === curr.latitude && prev.longitude === curr.longitude, } );

Halting duplicate notifications early protects computationally heavy derivation graphs. If a derived computation parses extensive collections, generates geometric boundaries, or filters tabular projections, suppressing upstream emissions shields application responsiveness. Because custom comparators execute prior to observer dispatch, stable payloads never wake dependent computed nodes or queue unnecessary browser layout updates.

A critical pitfall involves mutating existing properties in place before pushing state updates. If code modifies internal object fields directly and passes the mutated reference into update handlers, the previous and current arguments reference the exact same memory address. The comparison evaluates to true, mistaking changes for identity matches and halting downstream notification. When working with Angular signals, always enforce strict data immutability across all update routines.

Applying custom comparator predicates yields distinct architectural benefits across data-heavy applications:

  • Suppresses unnecessary template re-rendering when receiving identical backend polling payloads.
  • Prevents expensive recalculation of downstream computed formulas when secondary properties stay unchanged.
  • Enforces disciplined immutability boundaries across service layers and feature modules.

Preventing circular runs with untracked signal operations

Automatic dependency tracking represents one of the most productive characteristics of reactive development. Reading a reactive value inside a computation automatically registers that container as a dependency. However, complex real-world state machines frequently require inspecting auxiliary state without establishing permanent subscription links. In non-trivial state machines using Angular signals, reading secondary values inside derived formulas can introduce unintended dependency edges that degrade runtime efficiency.

The untracked helper isolates specific reads from active tracking frames. Wrapping any signal invocation inside an untracked closure queries the underlying value synchronously while preventing surrounding derivations from recording it as an invalidation trigger. When instrumenting telemetry loggers with Angular signals, developers inspect current user identity or session timestamps without re-executing logger routines whenever background session counters increment.

A dangerous pitfall occurs when side effects write to reactive containers that simultaneously feed the triggering computation. Uncontrolled recursive cycles quickly exhaust call stack allocations and throw framework execution errors. Isolating non-driving inputs through untracked execution prevents cyclical dependency loops. In production Angular signals deployments, explicit boundary demarcation separates primary event triggers from passive contextual metadata reads.

Consider a filtered search panel where user keystrokes update search parameters while a secondary signal controls page size settings. If developers intend for API queries to initiate strictly when query text changes, reading page size inside an untracked wrapper prevents page size toggles from executing remote requests prematurely. The query computation runs strictly when text inputs mutate, reading pagination parameters passively.

Auditing dependency chains should form a standard part of performance reviews in growing enterprise codebases. Inserting debugger statements within derived calculations clarifies which values trigger invalidations across complex component trees. By intentionally shielding contextual reads with untracked invocations, software engineers maintain predictable dependency graphs that evaluate strictly upon intentional state transitions.

Best practices for managing dependency tracking boundaries include the following architectural patterns:

  • Wrap analytic and diagnostic logging reads inside untracked blocks to prevent re-execution.
  • Isolate configuration lookups that should not trigger recomputation when global settings mutate.
  • Break circular dependency chains where an effect reads and writes related state containers.

Declarative asynchronous data handling with modern resources

Enterprise software depends heavily on asynchronous backend communication, yet initial reactive primitives focused primarily on managing synchronous in-memory values. Orchestrating network calls historically required manually synchronizing loading booleans, error containers, and response payloads across services. With contemporary additions to Angular signals, developers utilize declarative resource abstractions to bind asynchronous network promises and stream responses directly to reactive templates.

The resource and rxResource APIs bridge asynchronous network communication with synchronous reactive state. A resource monitors dynamic request parameters; whenever bound parameters change, the underlying loader executes automatically to fetch updated content. Integrating asynchronous queries into Angular signals reduces boilerplate service logic, replacing imperative fetch invocations with self-managing declarative reactive endpoints.

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

@Component({
  selector: 'app-user-profile',
  template: `
    <p>Status: {{ userResource.status() }}</p>
    @if (userResource.value(); as user) {
      <h3>{{ user.name }}</h3>
    }
  `,
})
export class UserProfileComponent {
  userId = signal(101);
  userResource = resource({
    request: () => ({ id: this.userId() }),
    loader: async ({ request }) => {
      const res = await fetch(`/api/users/${request.id}`);
      return res.json();
    },
  });
}

import { Component, signal } from '@angular/core'; import { resource } from '@angular/core';@Component({ selector: 'app-user-profile', template: ` <p>Status: {{ userResource.status() }}</p> @if (userResource.value(); as user) { <h3>{{ user.name }}</h3> } `, }) export class UserProfileComponent { userId = signal(101); userResource = resource({ request: () => ({ id: this.userId() }), loader: async ({ request }) => { const res = await fetch(`/api/users/${request.id}`); return res.json(); }, }); }

A resource instance encapsulates several complementary reactive values, including value, status, isLoading, and error. User interface templates bind directly to these signals, rendering adaptive loading skeletons and error banners without custom component wiring. In-flight requests automatically abort when bound parameters change prior to promise completion, preventing out-of-order responses from corrupting display states.

Managing concurrent request races previously required crafting intricate RxJS operator sequences with cancellation operators. When leveraging asynchronous loaders under Angular signals, stale response cancellation occurs under the hood. If a user rapidly navigates between customer record views, earlier in-flight requests abort automatically, preventing out-of-order response arrivals from corrupting current screen state.

Additionally, resource primitives expose imperative reloading hooks through dedicated reload methods. When background synchronization events demand immediate cache invalidation without parameter changes, calling reload triggers fresh network retrieval. This combination of declarative tracking and controlled execution produces robust asynchronous architectures across complex enterprise applications.

Key architectural considerations when implementing declarative resources include:

  • Ensure loader functions return clean deserialized types matching template view models.
  • Handle network failure states gracefully by rendering dedicated fallback templates when error signals activate.
  • Use request parameters to encapsulate all dynamic variables required by the remote endpoint.

Replacing decorators with signal inputs and two-way models

Component communication traditionally relied on property decorators such as @Input and @Output, combined with lifecycle hooks like ngOnChanges to detect parameter mutations. Modern component architecture transitions completely toward signal-based input functions. Adopting Angular signals for component boundaries replaces brittle lifecycle hooks with typed reactive inputs that integrate directly into downstream computed values.

The input function constructs a read-only reactive container driven by parent template bindings. Required inputs enforce compile-time verification, eliminating unexpected runtime exceptions caused by omitted component inputs. When architecting component libraries with Angular signals, transforming raw input values through built-in transform functions cleans incoming data before consumer logic executes.

Two-way synchronization using model inputs

Bidirectional state synchronization formerly demanded coordinated pairs of input properties and output event emitters following strict naming conventions. The model primitive unifies two-way binding into a single mutable signal. When parent components bind two-way values, child components update the model signal directly, emitting change notifications to parent structures automatically without verbose event wiring.

Migrating away from legacy lifecycle handlers simplifies state derivations. Instead of intercepting input changes inside ngOnChanges and storing intermediate values in local variables, developers construct computed signals that derive display states directly from signal inputs. In systems modernizing with Angular signals, this shift eliminates timing bugs caused by asynchronous lifecycle hooks executing out of sequence.

Signal-based outputs complement this model through the output function. Unlike legacy event emitter implementations, signal outputs execute without internal RxJS dependencies, reducing bundle footprints across standalone component trees. The resulting component interfaces communicate reliably, maintain strict type boundaries, and expose clean reactive interfaces to consuming parent templates.

Adopting modern input primitives provides notable architectural advantages across enterprise applications:

  • Eliminates boilerplate change detection lifecycle hooks and manual defensive assertions.
  • Enables derived properties to update automatically whenever input bindings produce new values.
  • Guarantees strict type safety and compile-time presence checks for required properties.

Managing cleanup routines and effect execution boundaries

Signal effects provide powerful synchronization hooks for logging, manual DOM manipulation, and external third-party integration, but unmonitored side effects pose genuine memory leak risks. In large-scale enterprise deployments of Angular signals, uncontrolled effect instantiations can retain view references long after components detach from active document trees.

By default, effects created within component constructor contexts bind their lifecycles directly to surrounding view destroy notifications. The framework cleans up registered effects automatically when host views unmount. However, when developers initialize Angular signals effects inside dynamic service methods or deferred helper scopes, passing explicit cleanup logic becomes mandatory to avoid lingering listeners.

The onCleanup parameter passed to effect callback functions provides a direct mechanism to tear down transient resources. Whenever an effect schedules re-execution or its host context unmounts, registered cleanup callbacks run first. When synchronizing Angular signals with third-party charts or canvas contexts, cleanup hooks dispose previous graphic instances before new drawing passes initialize.

Injecting DestroyRef offers programmatic registration of teardown hooks across arbitrary service boundaries. Combining DestroyRef with signal subscribers guarantees that long-lived singleton services dismantle subscriptions whenever transient consuming components undergo destruction. This strategy prevents detached views from occupying heap memory or handling orphaned notification dispatches.

Developers should also configure the manualCleanup option with caution. Setting manualCleanup: true instructs the framework to bypass automatic destruction bindings, delegating teardown responsibility entirely to application code. When designing mission-critical services around Angular signals, prefer framework-managed lifecycles and reserve manual cleanup overrides for persistent cross-cutting background utilities.

Disciplined cleanup management guarantees high application responsiveness across prolonged user sessions. Profiling memory allocation patterns in developer tools confirms that disposing signal dependencies promptly prevents memory bloat. Adhering to these reactive best practices equips development teams to build scalable, resilient web applications that fully exploit the speed and precision of modern reactive architecture.

Frequently asked questions

What is a signal in Angular?

An Angular signal is a reactive container for a value. You read it by calling it and write it with set or update, and Angular re-renders the parts of the view that read it when it changes.

When should I use computed instead of effect?

Use computed for values derived from other signals, because it caches and recalculates automatically. Use effect only for side effects such as logging or syncing to an external API.

Do I need NgRx with Angular signals?

No. For most applications, signals in a shared service are enough state management. NgRx is worth adding for large apps with complex shared state or a need to audit every change.

Why does my signal update not refresh the view?

You probably mutated the value instead of replacing it. Signals compare by reference, so call set or update with a new array or object to trigger a re-render.

What is asReadonly on a signal?

It exposes a read-only view of a writable signal, so components can watch the value but only the service that owns the signal can change it.

Last updated on · Written by