All insights

Architecture

Angular Signals for State Management: Start Without a Store

Most Angular UI state does not need a store. Start with a local signal, promote it when the application actually shares it, and introduce a store only when the architecture earns one.

A glowing state source connected to dependent nodes, illustrating Angular Signals reactivity.

Most Angular UI state does not need a global store. A filter panel, expanded table row, selected tab or modal state often has exactly one consumer: the component rendering it. Moving that state into a service or store before another consumer exists adds architecture without solving a real sharing problem.

That is why I start Angular state management with the smallest primitive that solves the current problem: a local signal. State moves outward only when the application proves that it needs to be shared.

When Angular Signals are enough

Observable-based Angular state spread across services and subscriptions compared with a single component signal.
The same toggle: several files and a subscription lifecycle on one side, one signal on the other.

Consider a filter panel whose open state is used by only one component. A service backed by a BehaviorSubject works, but it introduces another file, an observable, a subscription and teardown logic for a value that never leaves the component.

Local UI state can stay close to the component that owns it.
export class FilterPanelComponent {
  readonly open = signal(false);
  toggle(): void {
    this.open.update((value) => !value);
  }
}

There is no subscription or teardown, and a developer reading the component can immediately see where the state originates. For component-specific UI state, that proximity is valuable.

The Angular state promotion rule

Angular state promotion from a local component signal, to shared service signals, to an application store.
State moves outward only when a second real consumer appears — not in anticipation of one.

The rule I use is simple: promote state only when a second real consumer appears. Do not design around a consumer that might exist next quarter. Wait until another component actually needs the same state.

The progression then becomes predictable: start with a signal inside the component, move shared signals and computed values into a service when multiple consumers appear, and introduce a store when the state becomes genuinely application-wide or workflow-driven.

When to move Signals into a service

A service becomes useful when multiple components need the same source of truth. Derived state can then live beside the source state as computed signals instead of being recreated independently by every consumer.

Shared Signals and computed state keep one definition for every consumer.
@Injectable({ providedIn: 'root' }) export class CaseQueueState {
  private readonly cases = signal<Case[]>([]);
  readonly filter = signal<CaseFilter>('all');
  readonly visibleCases = computed(() => {
    const filter = this.filter(); return filter === 'all' ? this.cases() : this.cases().filter( (caseItem) => caseItem.status === filter );
  });
  load(cases: Case[]): void {
    this.cases.set(cases);
  }
}

When Angular state actually needs a store

A store becomes more useful when state must survive navigation, multiple unrelated areas of the application can update it, complex workflows depend on predictable transitions, or the application benefits from centralized event-driven state management.

This is not an argument against NgRx or other Angular state-management libraries. They solve real problems. The important decision is whether the application currently has those problems.

Signals and zoneless Angular

Angular components with no dependency on a changed signal staying untouched while dependent components update.
Under zoneless change detection, only the components that read the signal do any work.

Signals also fit naturally with zoneless Angular because state changes become explicit. Components depending on a signal can be notified when that signal changes instead of relying on broad asynchronous change-detection triggers.

Angular can run without Zone.js when application state changes are explicit.
bootstrapApplication(AppComponent, {
  providers: [ provideZonelessChangeDetection(), ],
});

The state-management test I use

Before introducing another state-management layer, I ask: who owns this state, who reads it, who writes it, and does it need to survive the component or route that created it?

If there is one owner and one consumer, a component signal is usually enough. If several components need it, promote it to a service. If unrelated parts of the application coordinate through it or the workflow requires centralized transitions, then a store has earned its place.

Starting small is easier to reverse than starting with unnecessary architecture. Angular Signals make that progression inexpensive, which is why local state is now my default rather than something I optimize toward later.

Sources

Where this is applied

Keep reading