Have you ever adopted the Angular OnPush Change Detection strategy in your project, only to encounter puzzling bugs that drove you back to the default mechanism? This article walks through several common scenarios where OnPush appears to malfunction, explains why, and shows how to resolve them. In practice, OnPush is far more intuitive than it seems and works seamlessly with a wide variety of component architectures.
For a deeper dive into the default change detection algorithm, check out this complementary guide: How does Angular Change Detection Really Work ?.
Scenario 1 - The Baseline (Default Change Detection)
Let's examine a straightforward newsletter component that currently relies on the default change detection strategy. It lives inside a parent component named HomeComponent:
Here, we provide the User object as an input. The User type is a straightforward custom interface:
Notice that the Home component passes a reference to a hard-coded user object directly to the newsletter child.
A "Change User Name" button is also present, which directly mutates the user data.
Initial Newsletter Component Implementation
The newsletter component is a purely presentational element. It accepts inputs, renders them in its template, and emits an @Output event upon subscription:
We can see that the component receives the user object via an @Input() and displays the corresponding first name.
Default Change Detection and Object Mutability
When we run this example and click "Change User Name", the expected behavior occurs:
- Initially, the newsletter displays "Hello Alice", matching the value set in the
Homecomponent. - After the button click, the UI updates to show "Hello Bob", reflecting the direct value assigned in the
changeUserNamemethod.
This functionality relies on the default change detection mechanism, which is fully compatible with direct object mutation.
Angular compares the result of {{user?.firstName}} before and after the click event. When a difference is detected, the template is refreshed with the new value.
But what happens when we switch to OnPush?
OnPush Change Detection and Direct Object Mutability
Let's modify the newsletter component to use OnPush:
After clicking the same button, the displayed text remains "Hello Alice". The application now produces incorrect results—the view no longer reflects the underlying model.
Why Leaves OnPush in the Dark?
This is an expected failure, but it leads to more subtle situations ahead. The root cause is twofold:
- We mutated the existing user object directly.
- OnPush relies on comparing input references, not internal properties.
- Since we didn't introduce a new object reference, the OnPush change detector was never triggered.
Avoiding Direct Mutation with OnPush
If you adjust changeUserName() to generate a new object instance instead of mutating the existing one, everything works as intended:
With this corrected version and OnPush, the UI now shows "Hello Bob" after the click.
To sidestep this problem, either refrain from mutating objects directly or employ an immutability library to freeze the view-model data passed down to components.
Up to this point, OnPush behaves as expected. However, there are more layers to this mechanism that often leads to confusion.
OnPush Change Detection and Event Handlers
Are there alternative signals that could trigger a re-render? Notice that the newsletter component includes a button with its own click handler.
If we click the "Subscribe" button, the template updates to display "Hi Bob". This happens because triggering an event handler inside the component itself also forces the OnPush change detector to run, regardless of whether any inputs changed.
This is our first clue that OnPush is about more than just inspecting property bindings.
What other scenarios can activate OnPush?
Scenario 2 - OnPush and Observables
Now, let's imagine that the user data isn't hard-coded in the parent component. To make this more realistic, we assume this data resides in a centralized UserService that loads data at startup and exposes it to the rest of the application via dependency injection.
Here's what that service might look like—simplified, with an actual implementation typically querying a backend:
The UserService Implementation
Let's break down what this service accomplishes:
- It's a global singleton because we register it in the root module.
- The user data is exposed via a public observable called
user$, which other parts of the app can subscribe to. - This observable is created by converting a private
SubjectusingasObservable. - The service emits data through this private subject, inaccessible to outsiders.
- By hiding the subject,
UserServicemaintains control over when new user data is pushed to the rest of the app. - The service immediately emits an
ANONYMOUS_USERobject, while also providing a publicloadUsermethod. - Typically,
loadUserwould invoke a backend request. Here, for demonstration, it simply emits a new user object.
Receiving User Data Elsewhere
Let's inject this service into the Home component and see how to use it:
Here's what we've changed:
- The
UserServiceis now injected into the class. - The template consumes the
userService.user$observable directly using the async pipe.
Scenario 2 Results
What happens when we click "Change User Name" now? Does the newsletter update?
With this implementation, which consumes the observable directly, everything still functions correctly.
The text on screen updates to "Hello Bob".
The Reason It Still Works
This is because a new user object instance is emitted through the observable. From the child component's perspective, it receives a fresh reference, so everything functions seamlessly.
Scenario 3 - Passing Observables as @Inputs() to an OnPush Component
Now, let's alter the design slightly. Instead of subscribing to user$ in the Home component, we want to pass the observable itself down the component tree:
Everything else in Home remains the same, but now we pass a reference to user$ to the newsletter.
This reference remains constant regardless of how many values are emitted.
Manipulating the Observable in the Child
Here's the updated newsletter component:
The component now receives an Observable as an input and subscribes to it using the async pipe.
Scenario 3 Results - Pragmatic OnPush
What happens in this case? The input property user$ hasn't changed—it still points to the same observable object.
Previously, we assumed nothing would update since the input reference is stable:
But that's not the case—Scenario 3 works perfectly!
Since the async pipe subscribes to user$, Angular knows that the template depends on its emissions.
Therefore, in version 3, clicking "Change User Name" triggers the newsletter template to update correctly.
Let's explore further architectural patterns to test the limits of OnPush.
Scenario 4 - Deeply Nested Smart Components
For this iteration, we decide to nest the newsletter component much deeper within the component hierarchy. This tree includes third-party libraries without source access, adding complexity.
We want the newsletter to fetch its own data from services rather than relying on inputs, to avoid:
- Passing data down through multiple layers that don't need it, adding unwanted boilerplate.
- Manually routing
Output()events back up, duplicating logic in each intermediary component.
This event-bubbling approach is awkward and signals a need for redesign.
Let's rework the newsletter as a deeply nested smart component:
The New Home Component Structure
This component can now be injected anywhere within the Home subtree and still function. The parent component simplifies to:
This pattern often demonstrates the benefit of injecting services deep into the tree rather than prop drilling data and events. Dependency injection makes this approach highly viable.
However, there's a critical flaw with this version:
This newsletter component doesn't work with OnPush!
Why Is OnPush Unresponsive Here?
Because it manually subscribes to user$ in ngOnInit, this implementation only functions with the default change detection strategy—not OnPush.
Does this mean OnPush prohibits deeply injected services? No. We just need to subscribe to any constructor-injected observables directly within the template using the async pipe:
Scenario 4 Resolved
With the async pipe subscription in place, this version now works flawlessly with OnPush!
The reason is that the async pipe registers the observable with the framework, allowing the OnPush change detector to be notified on each emission.
Previously, without the async pipe, the framework had no awareness that the observable's values were being rendered.
New Features from Angular 4
Notice two modern Angular conveniences in that solution:
- The
ngIf'as' syntax (introduced in Angular 4) assigns the async pipe's result to the template variableuser. - The
ngIf'else' branch shows a loading indicator until data becomes available.
Conclusions
With measured precautions in component design, OnPush operates seamlessly across diverse component styles—whether data comes directly as inputs, through observable inputs, or exclusively via constructor services.
An OnPush change detector is triggered by factors beyond just input reference changes, such as:
- Activating a component event handler.
- Emitting a new value from an observable subscribed to via the async pipe.
By consistently using the async pipe in templates for any observable subscriptions, you reap several benefits:
- Significant reductions in change detection issues while using OnPush.
- Simplify future migration from default to OnPush if necessary.
- Recognizing that immutable data and input reference checks aren't the only path to high performance; a reactive approach also leverages OnPush effectively.
If you're keen to explore advanced Angular core features, the Angular Core Deep Dive course offers thorough coverage, including detailed sections on change detection.
And for newcomers to Angular, there's the Angular for Beginners Course:
Related Angular Articles
If this post proved useful, these additional resources may also pique your interest:
- Angular Router - How To Build a Navigation Menu with Bootstrap 4 and Nested Routes
- Angular Router - Extended Guided Tour, Avoid Common Pitfalls
- Angular Components - The Fundamentals
- How to build Angular apps using Observable Data Services - Pitfalls to avoid
- Introduction to Angular Forms - Template Driven vs Model Driven
- Angular ngFor - Learn all Features including trackBy, why is it not only for Arrays?
- Angular Universal In Practice - How to build SEO Friendly Single Page Apps with Angular
- How does Angular Change Detection Really Work?
