LEARN · DEBUGGING GUIDE

Angular Signals Not Updating the Template: Debugging Guide

Angular Signals are supposed to trigger template updates automatically, but when they don't, it's usually a subtle mistake in signal creation, change detection context, or template binding. This guide walks through the exact commands and patterns to diagnose and fix the issue.

IntermediateAngular6 min read

What this usually means

The root cause is almost always that the signal is being mutated rather than replaced, or the signal is created outside an injection context (e.g., in a constructor without proper injection), or the template binding is reading the signal incorrectly (e.g., forgetting the function call parentheses). Angular Signals rely on the signal's equality check to trigger updates; if you mutate an object or array in-place, the signal reference stays the same and Angular skips the update. Another common scenario is using signals in components that are not wrapped in an Angular zone, or using `ChangeDetectionStrategy.OnPush` without triggering markForCheck.

( 01 )Fast diagnosis

The first ten minutes — establish facts before touching code.

  • 1Open browser DevTools and log the signal value: console.log(mySignal()) — check if the value actually changed
  • 2Run `ng.probe($0).componentInstance` in console and call the signal directly to verify the current value
  • 3Check if the signal is created with `signal()` inside the component class (not in a static method or outside injection context)
  • 4Verify template binding uses `mySignal()` (with parentheses) not just `mySignal`
  • 5If using objects/arrays, confirm you are creating a new reference: this.mySignal.set([...this.mySignal(), newItem]) not push()
( 02 )Where to look

The specific files, logs, configs, and dashboards that usually own this bug.

  • searchComponent TypeScript file: signal declaration and update calls
  • searchComponent template HTML: binding syntax (must be `{{ mySignal() }}`)
  • searchBrowser console: Angular DevTools change detection profiler
  • searchAngular zone.js trace: check if async operations run inside NgZone
  • searchPackage.json: angular version <16 signals are not available
  • searchChangeDetectionStrategy setting in component decorator
( 03 )Common root causes

Practical causes, not theory. These are the things you will actually find.

  • warningMutating arrays/objects instead of replacing them (e.g., array.push() instead of [...old, new])
  • warningSignal created outside injection context (e.g., in a static method or constructor parameter without `inject`)
  • warningMissing parentheses in template: `{{ signalName }}` instead of `{{ signalName() }}`
  • warningComponent with ChangeDetectionStrategy.OnPush and no markForCheck call after signal update
  • warningAsync operations running outside Angular zone (e.g., setTimeout without zone wrapper)
  • warningUsing `computed` signals that depend on mutated objects (computed only re-evaluates when its dependencies change reference)
( 04 )Fix patterns

Concrete fix directions. Pick the one that matches your root cause.

  • buildAlways create new references for objects/arrays: `this.items.set([...this.items(), newItem])`
  • buildEnsure signals are initialized in the injection context: use `inject()` in constructor or use `signal()` as class property initializer
  • buildAdd parentheses in template: `{{ mySignal() }}`
  • buildFor OnPush components, call `this.cdr.markForCheck()` after signal updates if needed (though signals should work without it)
  • buildWrap async operations in `NgZone.run()` if they are outside Angular's zone
  • buildUse `effect()` to log signal changes and confirm updates propagate
( 05 )How to verify

A fix you cannot prove is a guess. Close the loop.

  • verifiedAfter fix, change signal value and check template updates immediately
  • verifiedUse Angular DevTools to profile change detection and confirm component updated
  • verifiedAdd a `console.log` inside template binding to see when it re-evaluates
  • verifiedWrite a unit test that asserts the template renders the new signal value
  • verifiedTest with `ChangeDetectionStrategy.Default` to isolate OnPush issues
( 06 )Mistakes to avoid

Things that make this bug worse or harder to find.

  • warningDon't use `JSON.stringify` or deep clone on every signal update—performance killer
  • warningDon't forget that `computed` signals are lazy and only compute when read
  • warningDon't mix RxJS and signals without understanding the interop (e.g., `toSignal` requires proper cleanup)
  • warningDon't assume signals work with zone.js disabled; they rely on internal change detection
  • warningDon't put signal logic in `ngDoCheck`—that defeats the purpose
( 07 )War story

Signal array not updating table on push

Senior Frontend EngineerAngular 17.2, Angular Material Table, Signal-based state management

Timeline

  1. 09:15User reports that adding a row to the table doesn't show the new row until page refresh.
  2. 09:20I check the component: signals are used for the data source.
  3. 09:25I log signal value in console after push: shows the new items array with the new row.
  4. 09:30I check template: using `dataSource()` binding, but the table is using `[dataSource]="dataSource()"` which is correct.
  5. 09:35I look at the code: `this.dataSource.update(arr => { arr.push(newItem); return arr; })`. Mutation!
  6. 09:40I change to `this.dataSource.set([...this.dataSource(), newItem])`.
  7. 09:42Table updates immediately. Root cause: signal reference not changing due to in-place mutation.
  8. 09:45I add a unit test that verifies the signal emits a new reference on update.

The ticket came in as 'Table not updating after adding row'. I reproduced it: the REST call returned success, the array in memory had the new item, but the Material Table showed the old rows. I opened DevTools and logged the signal value after the push — it showed the updated array. That's when I suspected a reference issue.

I checked the template binding: `[dataSource]="dataSource()"` looked fine. Then I looked at the update code: `this.dataSource.update(arr => { arr.push(newItem); return arr; })`. The signal's `update` method returns the same array reference. Angular's change detection uses `===` to compare old and new values; since the reference didn't change, it skipped the template update.

The fix was simple: replace the mutation with a new array: `this.dataSource.set([...this.dataSource(), newItem])`. I also added a lint rule to forbid array mutations on signals. The lesson: signals are about immutable data flows. If you mutate, you break the contract.

Root cause

Signal update returning the same array reference after in-place mutation via push.

The fix

Replace with `this.dataSource.set([...this.dataSource(), newItem])` to create a new array reference.

The lesson

Always return a new reference when updating signals holding objects or arrays; mutate only with caution.

( 08 )How Angular Signals Trigger Change Detection

Angular Signals use a reactive graph that tracks dependencies. When a signal's value changes (determined by `===` equality), it notifies its consumers (template bindings, computed signals, effects). The template binding is a consumer that Angular's change detection checks during the next change detection cycle. However, if the signal value is the same reference (e.g., mutated array), the equality check passes and no notification is sent.

This is different from RxJS where you can emit the same object. Signals rely on immutability. The `set` method triggers update only if the new value is different from the old one according to the equality function (default `Object.is`). The `mutate` method exists but is for advanced use; it forces a notification even if reference doesn't change, but it's easy to misuse.

( 09 )Common Pitfall: Signals in OnPush Components

Components with `ChangeDetectionStrategy.OnPush` only check for changes when their input references change, or when an event handler runs. Signals are designed to work with OnPush because they trigger change detection automatically when they update. However, if you update a signal in a microtask (e.g., `setTimeout`), it might not trigger change detection if the component is not in the Angular zone. Always ensure async operations are inside `NgZone.run` or use `ApplicationRef.tick`.

Another edge case: if you have a `computed` signal that depends on a mutable object, the computed won't re-evaluate because the dependency didn't change reference. This leads to stale computed values even though the underlying data changed.

( 10 )Debugging with Angular DevTools and Profiler

Open Angular DevTools in Chrome, go to the 'Profiler' tab, record a session, and perform the action that should update the signal. The profiler shows which components are checked. If your component is not in the list, change detection didn't run. If it is checked but the template didn't update, the binding might be wrong or the signal value didn't change.

You can also use the 'Component Tree' tab to inspect the signal value directly: select the component, then in the properties panel you can see the signal's current value. This helps distinguish between a signal that hasn't updated vs a template that isn't binding.

( 11 )Testing Signals and Template Updates

In unit tests, use `TestBed.createComponent` and then call `fixture.detectChanges()` after signal updates. However, signals are synchronous, so `detectChanges` should reflect the new value. If it doesn't, check that the signal is indeed updated before `detectChanges`. You can also use `component.signalName()` in the test to assert the value.

For integration tests, simulate user interaction and then check the DOM: `expect(fixture.nativeElement.textContent).toContain('new value')`. This catches template binding mistakes like missing parentheses.

Frequently asked questions

Why does my signal update in console but not in the template?

Most likely you are mutating an object or array in-place, so the signal reference stays the same. Angular's equality check sees the same reference and skips the template update. Use `set` with a new reference. Also check template binding syntax: must be `{{ mySignal() }}` with parentheses.

Do I need to call `markForCheck` when using signals with OnPush?

No, signals automatically mark the component for check when they update. However, if you update a signal outside Angular's zone (e.g., in a `setTimeout` without `NgZone.run`), you might need to call `changeDetectorRef.markForCheck()` or wrap the update in `NgZone.run`.

Can I use `signal.mutate` to update arrays in place?

Yes, `mutate` forces a notification even if the reference doesn't change. But use it sparingly; it's easy to accidentally mutate shared state. Prefer creating new references for clarity and predictability.

My computed signal is not updating when a dependency changes.

Computed signals only re-evaluate when their dependency signals change reference. If the dependency is a signal that holds an array and you mutate the array in-place, the dependency doesn't change reference, so the computed won't re-run. Always replace the array with a new reference.

How do I debug if a signal update is even firing?

Use an `effect` to log changes: `effect(() => console.log('signal changed:', mySignal()));`. This will run whenever the signal changes. If it doesn't log, the signal isn't being updated or the effect is not in the correct injection context.