Technology  /  Angular

๐Ÿ…ฐ๏ธ Angular 15 guides ยท updated 2026

Standalone components, signals, RxJS, and dependency injection โ€” building maintainable frontends with the framework enterprises trust.

Angular Signals and State Management: signal, computed, effect, and Stores

Signals are the biggest change in Angularโ€™s history โ€” a rebuilt reactive core that every recent feature (signal inputs, resources, zoneless change detection) stands on. Youโ€™ve used them throughout this series; this guide explains the system itself: how dependency tracking actually works, the three primitivesโ€™ distinct roles, the anti-patterns that mark half-understood signals, and the store pattern that has become modern Angularโ€™s default state management.


The Primitive: A Value That Knows Its Readers

const count = signal(0);
count(); // read: 0
count.set(5); // replace
count.update((c) => c + 1); // transform: 6

A signal is a container with a twist: reading it inside a reactive context registers a dependency. When a template renders {{ count() }}, Angular records โ€œthis view depends on countโ€ โ€” and when count changes, it knows exactly which views, computeds, and effects to update. No diffing the world, no guessing: a live dependency graph, maintained automatically by the act of reading.

Thatโ€™s the entire trick, and itโ€™s worth contrasting with what it replaced. Classic Angular (zone.js) assumed anything might have changed after every event and re-checked everything โ€” the change-detection guide tells that story. Signals invert it: nothing is checked unless a signal it read has changed. Fine-grained, not app-grained.

One rule of hygiene inherited from immutability discipline: update by replacement, not mutation.

items.update((xs) => [...xs, newItem]); // โœ“ new reference โ€” dependents notified
items().push(newItem); // โœ— mutation โ€” signal never knows

Signals compare by reference (configurable via equal); mutating the inner object changes nothing the signal can see. Spread-based updates arenโ€™t style points here โ€” theyโ€™re the notification mechanism.


computed: Derive, Donโ€™t Synchronize

const items = signal<CartItem[]>([]);
const memberDiscount = signal(0.1);
const subtotal = computed(() =>
items().reduce((s, i) => s + i.paisePerUnit * i.qty, 0));
const total = computed(() =>
Math.round(subtotal() * (1 - memberDiscount())));

computed declares a value as a pure function of other signals: lazily evaluated, cached until a dependency changes, chainable into graphs (total depends on subtotal depends on items). The properties that matter:

The design mantra that falls out, and itโ€™s the highest-value sentence in this guide: store the minimum; derive the rest. Given items, everything โ€” counts, totals, filtered views, validity โ€” should be computed. Every derived value you store instead is a synchronization job youโ€™ve assigned to your future self.

linkedSignal covers the edge where derivation and writability collide โ€” โ€œresets when its source changes, but user-adjustable in betweenโ€ (selected item resets when the list reloads; quantity resets when the product changes):

const selected = linkedSignal(() => items()[0] ?? null); // writable, re-derives on items change

effect: The Exit Door, Used Sparingly

constructor() {
effect(() => {
localStorage.setItem('cart', JSON.stringify(this.items()));
});
}

effect runs a side-effecting function whenever its signal dependencies change โ€” the bridge out of the reactive graph to the imperative world: storage, logging, analytics, imperative DOM libraries, title updates. Effects auto-track like computeds, run after change detection, and self-dispose with their context.

The discipline section, because effect misuse is the signals anti-pattern:

// โœ— deriving state in an effect โ€” the cardinal sin
effect(() => this.total.set(this.price() * this.qty()));
// โœ“ that's a computed:
total = computed(() => this.price() * this.qty());

Effect-derived state reintroduces everything computeds eliminated: ordering questions, glitch windows where total is momentarily stale, and write-conflicts. The test before writing any effect: โ€œam I leaving the signal world?โ€ If the answer is โ€œno, Iโ€™m producing another valueโ€ โ€” itโ€™s a computed (or linkedSignal). Real codebases end up with dozens of computeds and a handful of effects; the inverse ratio is the smell. (Guardrail trivia: writing signals inside effects requires opting in precisely because the framework wants you to feel the friction.)


The Store Pattern: App State, Solved Simply

Scale the primitives up and you get modern Angularโ€™s answer to โ€œdo I need NgRx?โ€ โ€” for most apps, a signal store service:

@Injectable({ providedIn: 'root' })
export class OrdersStore {
private api = inject(OrderApi);
// private writable source of truth
private state = signal<{ orders: Order[]; filter: OrderStatus | 'all' }>({
orders: [], filter: 'all',
});
// public read-only projections
readonly filter = computed(() => this.state().filter);
readonly visible = computed(() => {
const { orders, filter } = this.state();
return filter === 'all' ? orders : orders.filter((o) => o.status === filter);
});
readonly pendingCount = computed(() =>
this.state().orders.filter((o) => o.status === 'pending').length);
// named mutations โ€” the only write paths
setFilter(filter: OrderStatus | 'all') {
this.state.update((s) => ({ ...s, filter }));
}
load() {
this.api.list().subscribe((orders) =>
this.state.update((s) => ({ ...s, orders })));
}
markShipped(id: string) {
this.state.update((s) => ({
...s,
orders: s.orders.map((o) => (o.id === id ? { ...o, status: 'shipped' } : o)),
}));
}
}

Components inject and read โ€” store.visible() in templates, store.setFilter(...) on clicks โ€” and every consumer updates reactively, forever. The structure is doing real work: private writable signal (nobody outside can mutate state โ€” encapsulation), public computeds (consumers canโ€™t hold stale copies), named mutation methods (every state change has a greppable name and one implementation โ€” poor-manโ€™s actions). This shape โ€” sometimes formalized via the NgRx SignalStore library, often hand-rolled exactly as above โ€” has become the community default. Reach for full NgRx (actions/reducers/devtools) when you genuinely need its event log and middleware: large teams, audit-grade traceability, complex effect orchestration.

Where async fits: resources for read-flows keyed on state, subscribe-and-update (as in load) for commands โ€” the reads-declarative, writes-imperative split operating inside the store.


How the Tracking Actually Works (The Interview Cut)

The mechanism is simple enough to hold entirely, and holding it dissolves several mysteries. When a reactive consumer (a template, a computed, an effect) runs, the framework sets a global โ€œcurrently evaluatingโ€ marker; every signal() read during that run checks the marker and records the edge consumer โ†’ this signal. When a signal is written, it walks its recorded consumers and marks them dirty; templates schedule re-render, computeds mark themselves stale (recomputing lazily on next read), effects queue.

Consequences that now explain themselves:

Interviewers increasingly probe signals; narrating reader-registers-dependency, writer-notifies-readers, computeds-pull-lazily โ€” and knowing why handler reads donโ€™t track โ€” places you well past tutorial familiarity.

Frequently Asked Questions

Signals vs RxJS โ€” which do I learn, which wins? Both, for different axes: signals model state (current values, synchronous reads, no subscription management); RxJS models events over time (streams, debouncing, cancellation, the next guide). Modern code: signals by default, RxJS where time/coordination is intrinsic, toSignal/toObservable as the bridges. The โ€œwarโ€ framing is obsolete โ€” the framework itself uses both.

Why call syntax โ€” count() โ€” instead of plain properties? The call is the dependency-tracking hook (and you know functions-as-values). Property magic (proxies) hides reads; the call makes every reactive read visible and greppable. You stop noticing within a week.

Do signals replace component @Inputs, lifecycle, etc.? They already did โ€” signal inputs, queries, resources are signal-based; lifecycle hooks shrank because derivations subsume them. This guide is the theory behind the seriesโ€™ practice.

How granular should signals be โ€” one big state object or many small signals? Either works (the store above uses one object + projections; many small signals is equally valid). Optimize for update ergonomics: state that changes together lives together; independent state in independent signals avoids spurious recomputation of unrelated computeds.

Can I use signals outside components โ€” plain classes, utilities? Yes โ€” signals are a standalone reactive system (only effect needs an injection context, or an explicit injector). Stores, directives, route guards: all signal-friendly.


Migrating a Component to Signals: A Checklist

Since most working developers meet signals mid-codebase rather than greenfield, the incremental recipe: (1) State fields โ†’ signal() โ€” every mutable property templates read; mutations become .set/.update with spread-replacement. (2) Derived fields โ†’ computed() โ€” anything currently recomputed in getters, methods, or sync-blocks; delete the synchronization sites as you go (the deletion is the payoff). (3) @Input() โ†’ input() โ€” usually mechanical (ng generate @angular/core:signal-input-migration automates the bulk); ngOnChanges bodies become computeds/effects. (4) Subscriptions feeding fields โ†’ toSignal or resources. (5) Only then consider OnPush/zoneless for the component โ€” with everything signal-driven, itโ€™s a flag flip rather than a bug hunt. Components migrate independently โ€” signal and non-signal components interoperate freely โ€” so the strategy is opportunistic: migrate what you touch, prioritize the components profiling implicates, and leave stable legacy alone. The one migration to not defer: new code starts signal-first, because every zone-reliant component written today is tomorrowโ€™s checklist item.

A closing calibration on what signals donโ€™t solve: theyโ€™re a reactivity primitive, not a data-management framework. Cache invalidation policy, optimistic updates with rollback, offline sync, undo/redo โ€” these remain design problems you solve on top of signals (or adopt a library for), and pretending the primitive answers them is how โ€œsignals will fix our stateโ€ projects disappoint. What signals do guarantee โ€” derived values that canโ€™t go stale, precise updates, and a readable dependency graph โ€” removes the accidental complexity so the essential complexity gets your full attention. Thatโ€™s the honest pitch, and itโ€™s plenty.

Self-Check

  1. cart.items().push(item) โ€” the badge doesnโ€™t update. Mechanism, and fix?
  2. Convert to signals: a component stores filteredList, updating it in three places whenever list or query changes.
  3. effect(() => { if (this.user()) this.greeting.set('Hi ' + this.user().name); }) โ€” critique and rewrite.
  4. In the OrdersStore, why is state private with computeds public โ€” name the two failure modes it prevents.
  5. A โ€œselected orderโ€ should reset to null whenever the orders list reloads, but be user-settable otherwise. Which primitive?

(Answers: mutation doesnโ€™t change the reference, so no notification โ€” update with spread. filteredList = computed(() => filter(this.list(), this.query())) โ€” delete all three sync sites. State-derivation-in-effect: greeting = computed(() => this.user() ? 'Hi ' + this.user().name : ''). Outside mutation bypassing invariants, and consumers caching stale snapshots โ€” both structurally impossible here. linkedSignal(() => null, ...) keyed on the orders signal.)

Next: RxJS Essentials โ€” the observables youโ€™ll actually meet in Angular, the five operators that matter, and the signal-observable bridges.