If your Angular template not updating when variable changes, the cause is often change detection triggers, mutated data, or async code running outside Angular.
When the screen won’t refresh, it’s tempting to blame the binding. Most of the time, the binding is fine. The real issue is that Angular never gets the signal to run a check, or it runs a check and sees no new reference to render.
This guide helps you prove what’s happening, then fix it with patterns you can reuse in real Angular apps.
Why An Angular Template Can Stay Stuck
Angular updates the DOM during change detection. Change detection runs when Angular thinks something happened that might change the view, like an input reference change, a template event, or an async source that Angular tracks.
If your code changes a value but Angular does not schedule a check, the UI can look frozen. A click that suddenly “wakes” the view is a strong hint that your update happened earlier than the next render.
Fast Triage Signals
- Log The Value — If the console shows the new value while the template shows the old one, you’re dealing with a render trigger, not a write failure.
- Check Strategy — If the component uses
OnPush, Angular will skip checks unless it sees the right triggers. - Check Object Writes — If you mutate an object or array in place, Angular can miss it in
OnPushpaths.
Map Symptoms To Causes
Most “stuck template” bugs fall into a small set of buckets. Use the table to map what you see to a likely cause and a first move.
| Symptom | Likely Cause | First Fix To Try |
|---|---|---|
| Updates appear after a click | Async work not triggering a check | Bind with async pipe or call markForCheck |
| OnPush child stays stale | Mutated object or array input | Write a new reference (copy, map, spread) |
| Pipe output won’t change | Pure pipe + mutated input | Return a new value; treat impure pipes as last resort |
| View never refreshes | Detector detached or destroyed | Reattach; stop async writes on destroy |
Angular Template Not Updating When Variable Changes With OnPush
When a component uses ChangeDetectionStrategy.OnPush, Angular checks it less often. It can cut work, but your code must line up with the triggers Angular listens for.
With OnPush, Angular checks a component when an input reference changes, when an event happens in the view, or when the component is marked dirty for a later pass.
Fix Input Updates That Mutate In Place
Mutation is the top reason an OnPush child does not rerender. If a parent passes an object, then later changes a field on that same object, the reference stays the same. Angular sees “same input,” so it skips the child.
- Replace The Reference — Create a new object or array when you update state so the input reference changes.
- Keep Inputs Narrow — Pass the exact primitive a child needs when you can, not a large bag of data.
- Make Mutation Loud — If you must mutate, do it in one place and pair it with a clear refresh trigger.
// Parent component
updateName(nextName: string) {
this.user = { ...this.user, name: nextName };
}
Fix Nested Writes And In-Place Sorting
Nested objects can hide mutation. You change settings.ui.theme, but only the last field changes, not the parent references the template depends on. In-place sorting is another trap since sort() mutates arrays.
- Copy Each Level You Change — Update the field, plus each parent object along the path.
- Sort A Copy — Use
[...items].sort(...)so you keep a new reference. - Add trackBy — For lists, use a stable id so Angular can keep rows aligned across renders.
Fix Updates Lost To Object And Array Mutation
Even without OnPush, mutation can lead to confusing UI. A derived value in a pipe may stay stale, or a list may not reflect edits the way you expect.
Angular’s pipes guide notes that detecting changes inside arrays or objects requires an impure pipe, and it warns that impure pipes can carry a heavy cost if used carelessly.
When Pure Pipes Hide Changes
If you pass a single array into a pure pipe and mutate that array, the pipe may not run again. The template then keeps the old pipe output while the data changed.
- Return New Arrays — Build a new array when adding, removing, or sorting items.
- Return New Objects — If the pipe depends on object fields, pass a new object reference.
- Move Logic Into Code — Compute the derived value in the component so it updates with your state.
When Lists Look Stale Or Jump
List rendering can also mask the real issue. If identity tracking is off, a row can look like it ignored a change, when it’s really reusing a DOM node with old state.
- Use A Stable trackBy — Return an id from
trackByso items keep their identity. - Avoid Index Identity — Index breaks when you insert, delete, or sort.
- Keep Row State External — Store edit state by id, not by array position.
Make Async Updates Trigger A Template Refresh
Async code is a frequent cause of stale templates. Your variable changes, you can log it, yet the UI stays stale. That points to Angular not scheduling a change detection pass right after the async work finished.
Angular’s zoneless guide lists the notifications Angular relies on to decide when to run change detection, including ChangeDetectorRef.markForCheck (called by AsyncPipe) and updating a signal that the template reads.
Make Signals Drive The Template
If you’re using signals, the template must read the signal for Angular to know it should refresh. A signal that changes but never gets read in the template is invisible to rendering.
- Read The Signal In The Template — Bind
{{ count() }}or use the signal in a template expression. - Keep Derived Values As Computed — Use computed signals for values the UI shows so updates flow from one place.
- Bridge Streams Carefully — If you convert an observable to a signal, confirm the signal is the one the template reads.
This signals-first pattern pairs well with AsyncPipe. Both make the template the consumer, which keeps refresh triggers predictable.
Prefer AsyncPipe Over Manual Subscription
If you subscribe in code and assign into a field, you own both the subscription and the UI refresh story. If you bind an observable in the template with | async, Angular handles subscribe and unsubscribe, and it marks the component to be checked when a new value arrives.
- Bind Streams In The Template — Expose
user$, then render it with{{ user$ | async }}. - Keep One Source Of Truth — Avoid mixing manual assigns and template binds for the same value.
- End Subscriptions On Destroy — Use
takeUntil(or similar) so late emissions don’t hit a dead view.
Angular’s AsyncPipe docs state that when a new value is emitted, the pipe marks the component to be checked for changes and cleans up on destroy.
Catch Updates From Timers And Third-Party Callbacks
Timers, custom events, and third-party callbacks can run outside Angular’s tracked execution. When that happens, your state changes, yet Angular does not run a check.
- Run Through NgZone — Wrap the state update in
ngZone.run(() => ...)so Angular sees it. - Update A Template-Read Signal — Signals refresh the view when the template reads the signal.
- Mark For Check After Writes — In
OnPush, callmarkForCheckwhen no other trigger exists.
// Callback from a non-Angular library
constructor(private ngZone: NgZone) {}
onExternalEvent(payload: string) {
this.ngZone.run(() => {
this.status = payload;
});
}
Use ChangeDetectorRef When You Truly Need It
Manual change detection is a tool, not the first move. Still, there are times where it’s the cleanest fix: an OnPush component updated by a service callback, a detached view, or code paths that you cannot route through Angular’s usual triggers.
Angular’s ChangeDetectorRef docs describe markForCheck as a way to ensure a component is checked even if inputs and view events did not trigger a pass.
Know The Two Main Methods
- markForCheck — Marks the view as dirty so it gets checked on the next change detection pass.
- detectChanges — Runs change detection for the view and its children right away.
markForCheck fits most UI update issues with OnPush. detectChanges is better when you need an immediate repaint in the same turn, or when the view is detached and you’re controlling checks on your own.
Keep Manual Calls Small
- Mark After Assign — Write the new value, then call
markForCheckonce. - Guard Destroy — Stop async streams on destroy so you don’t update a dead view.
- Let AsyncPipe Do The Work — AsyncPipe marks for check when a new value arrives.
private cdr = inject(ChangeDetectorRef);
saveComplete() {
this.saving = false;
this.cdr.markForCheck();
}
A Debug Checklist You Can Run In Ten Minutes
If you want to stop guessing, run a tight checklist. Each step rules out a big class of causes. You’ll end with a short, boring fix, which is the goal.
Step-By-Step Checks
- Verify The Write — Log the value right after assignment and confirm it matches what you expect.
- Confirm The Binding — Check the template reads that same field, not a similar name or a getter with side effects.
- Check OnPush — If the component is
OnPush, treat mutation as a suspect. - Swap In A New Reference — Replace a mutated object or array with a copied one and see if the UI updates.
- Bind Streams With AsyncPipe — If the value comes from a stream, render it with
| async. - Mark For Check — If no Angular trigger exists, call
markForCheckafter the write. - Check Pipes — Pure pipes run on reference changes; confirm the pipe sees a new input.
- Search For Detach — Look for
detach()and manual view control that might skip checks.
A Minimal Repro Test
Create a tiny component that binds to the failing field and nothing else. If the value updates there, the issue is higher up, like inputs, pipes, or list identity. If it fails there too, the issue is a trigger problem.
- Remove Indirection — Bind the raw field instead of a computed method.
- Remove Pipes — Temporarily print the value with no pipe.
- Move The Write — Trigger the write from a button click inside the component to confirm the binding works.
One last reminder for search intent: angular template not updating when variable changes is rarely “Angular is broken.” It’s a trigger, a reference, or a pipe.
If you’re still stuck, re-check for hidden mutation, then verify which updates happen during async work. That path solves nearly every angular template not updating when variable changes report.
See Angular docs on zoneless change detection.
