Understanding Angular's ngIf Directive

The Angular ngIf directive fills a gap in HTML's capabilities. While HTML functions as a language in its own right, it lacks conditional logic that developers take for granted in JavaScript.

The ngIf directive essentially brings if-statement functionality to HTML, implementing this missing feature through the special ngIf attribute.

To use ngIf, you supply a condition. Consider these scenarios:

In one case, a container div appears only when the user is authenticated. Inside that container, a button is rendered exclusively when the user has administrator privileges.

Choosing Where to Place ngIf

When working with container elements, you might worry about needing an extra wrapper just to apply ngIf. Fortunately, that's unnecessary.

The ng-container directive serves as an ideal host for ngIf. This approach toggles the visibility of its content without introducing an additional div into the DOM:

ngIf Versus CSS-Based Visibility

While HTML lacks native conditionals, CSS offers mechanisms to hide page sections using the display and visibility properties.

Through JavaScript, you can toggle these CSS attributes to hide elements. However, this approach differs fundamentally from using ngIf.

When ngIf hides an element, that element completely disappears from the page. Chrome Dev Tools inspection reveals no trace of the HTML element in the DOM.

Instead, you'll encounter a distinctive HTML comment where the directive was applied:

<!--bindings={
  "ng-reflect-ng-if": "false"
}-->

This comment exists purely for debugging purposes, indicating where a visible element would have been positioned.

This behavior contrasts sharply with CSS properties like display or visibility. Setting display to none hides the element visually, yet Dev Tools shows the DOM elements still present—just not rendered:

Angular ngIf compared to CSS display and visibility properties

Using visibility: hidden produces similar results. The element becomes invisible but remains in the page structure upon inspection.

Note that visibility: hidden preserves the element's space on the page, whereas display:none removes it entirely. In both CSS approaches, the elements persist in the DOM, consuming resources, unlike ngIf where hidden elements vanish completely.

When building Angular applications, favoring ngIf over CSS-based hiding is generally the recommended approach.

Expression Types Accepted by ngIf

The ngIf directive accepts any valid TypeScript expression, not just booleans. The element's visibility depends on the truthiness evaluation of that expression.

Beyond booleans, you can pass strings, arrays, objects, and other types to ngIf. Here's what happens with various primitive types:

Additional examples show arrays and objects passed to ngIf:

Ultimately, what determines whether an element appears is the truthiness of the expression supplied to ngIf.

Implementing else Logic with ngIf

Just as JavaScript includes else clauses in if statements, Angular provides equivalent functionality for HTML templates.

The ngIf else syntax works like this:

Alongside the courses.length expression, you can specify an else clause referencing a template (here, the noCourses template).

When the expression evaluates to falsy, the noCourses template instantiates exactly where ngIf was applied.

Conversely, when courses.length remains truthy, the noCourses template never appears on the page.

The then-else Capabilities of ngIf

The Angular ngIf directive also supports if-then-else syntax, mirroring JavaScript's conditional structures. Here's an illustration:

In this example, ngIf sits on an ng-container. Based on the truthiness of courses.length, either the coursesList or noCourses template gets instantiated.

Unlike JavaScript, this syntax doesn't accommodate multiple "else if" branches. For such scenarios, ngSwitch provides equivalent functionality.

Combining ngIf with the async Pipe for Observables

When building reactive applications, ngIf frequently serves to deliver observable data into templates.

Pairing ngIf with the async pipe enables Observable consumption in this pattern:

Here, courses$ represents an Observable emitting course object arrays. This Observable might originate from memory or an in-memory store.

Regardless of the data source, the async pipe subscribes to the Observable and exposes emitted values to the template.

The "as" syntax within ngIf applies the async pipe, making Observable values available through the courses local template variable.

A key advantage of using the async pipe directly in templates is automatic unsubscription when the component is destroyed.

Additionally, this consumption pattern automatically updates the component view with the latest Observable data when OnPush change detection is active.

A Problematic Pattern: Overusing ngIf with Observables

While combining ngIf with the async pipe proves convenient, it's easily misapplied in complex interfaces requiring multiple Observable data sources across various page sections.

You may encounter component templates resembling this structure:

This page contains three peer sections at the same HTML tree level:

  • the header requires only the user
  • the body needs everything: courses, lessons, and the user
  • the footer relies on courses and lessons alone

To consume Observable data, we repeat ngIf with the async pipe at multiple page levels.

Both body and footer employ nested ng-container directives to access multiple Observables within a single page section.

The body section necessitates three nesting levels, and components with five or six levels exist solely to consume Observable data.

When Does This ngIf/async Overuse Happen?

This situation emerges in complex components where each Observable's data spreads across the entire page rather than staying in one contained area.

Initially, a component might use each Observable in a specific page region. As requirements evolve, Observable data gets referenced in new locations, causing ng-container directives to proliferate.

Nesting ng-container isn't inherently problematic—it's standard when multiple structural directives like ngIf and ngFor coexist, since each element accepts only one structural directive.

The concern here is the repeated ngIf-with-async combination used solely for data access.

Does This Cause Practical Problems?

Beyond visual clutter and code duplication, this nesting pattern hinders readability and maintainability over time.

One might consider splitting the component into smaller subcomponents. However, the tightly coupled nature of page sections often makes separation impractical.

The Typical Attempted Solution

A common fix involves retrieving all necessary data upfront at the component's top:

This approach nests ngIf directives at the template's beginning to gather all page data in advance.

While the nesting looks unappealing, it eliminates much code repetition. But practical issues emerge with user experience.

Why Upfront Nesting Falls Short

Generally, you want to display content to users as quickly as possible. Data sources have varying fetch times.

Envision that user$ data comes from an in-memory store with immediate availability.

Meanwhile, courses$ arrives via a rapid REST API call, while lessons$ comes from a slower API request.

Ideally, you'd show user$ immediately, then courses data upon arrival, and finally lessons when ready.

However, the ngIf nesting blocks this incremental rendering. With the current approach nesting ngIf three times, the entire component only appears once all data is available.

This solution thus proves visually unattractive and practically detrimental to user experience.

What alternatives exist for improvement?

The Single Data Observable pattern

For straightforward screens, the complications outlined earlier may not pose a significant challenge.

However, as your screen grows in complexity and you notice that the heavy use of ngIf/async nesting is creating maintenance headaches and degrading the user experience, it might be time to refactor your component to adopt the single data observable pattern.

With this approach, the component template takes on a much cleaner structure:

Notice that there is no longer any nesting, and the repeated use of the ngIf/async pair is gone. All the data required by the template is accessed at the top of the component through a single data$ Observable, meaning the async pipe is applied only once.

This Observable holds all the data the component's template needs during its entire lifecycle, which is why the pattern is named as it is.

How does this pattern address the earlier issues?

In addition to eliminating unnecessary nesting and making the template far more readable and logical, this pattern also resolves the UI concerns discussed previously.

In fact, you can construct the data$ Observable in any way that suits the desired user experience.

For instance, if the user$ data is already accessible, you can configure the Observable to emit an initial value with the user property populated while the other properties hold default values:

This lets the user$ data display right away while the remaining data is still loading. When the courses API request finishes, you can then emit a second value for the data$ Observable that includes the courses:

As a result, you can show the user even more content, perhaps already concealing the global loading indicator while a more localized spinner continues to run.

Eventually, when the lessons arrive from the server, you can emit a final value of the data$ Observable that contains everything:

How to build the Single Data Observable?

The data$ Observable can be assembled using any combination of RxJs operators that fit the UI's specific demands.

But in most cases, the go-to method for constructing this Observable is via RxJs combineLatest. Here is a typical example of building the data$ Observable:

Let’s walk through the logic step by step. First, we set up three separate Observables (user$, courses$, and lessons$).

The initial user$ Observable comes from an in-memory store and has no default emission, which makes sense since there is no default user profile.

However, the other two Observables, which originate from backend services, have logical defaults (the empty array), established via the startWith operator.

This means each of those Observables first emits the empty array [], and only later emits the result from the backend call.

Finally, we combine all three Observables using combineLatest, which produces a result Observable that emits tuple values.

These tuples contain the values emitted by each source Observable in sequence, specifically [user, courses, lessons]. We then use the map operator to transform this tuple into an object of type ExampleData.

With just a few commonly used RxJs operators, we have successfully created the exact data$ Observable needed, making the template much simpler and easier to maintain, at the cost of a small amount of additional code.

If you are not familiar with combineLatest

When using this pattern, it's important to remember a key characteristic of combineLatest: it will not emit its first tuple until all of its source Observables have emitted at least one value.

This is exactly why adding default values to courses$ and lessons$ is crucial; otherwise, you would not be able to provide the user and courses data to the template as soon as it becomes available.

The data$ Observable we defined will wait for a user to be emitted, and once that happens, it will keep emitting values whenever any of the user$, courses$, or lessons$ Observables emit new values.

After the first combined tuple, any additional emissions from user$, courses$, or lessons$ will trigger a new result tuple.

When is the Single Data Observable pattern worth using?

It’s probably not wise to apply this pattern universally, but rather only in screens that rely on multiple observable sources, where it’s challenging to predict upfront where in the page those observables will be needed as the UI evolves.

If the screen is expected to be complex and handle several observables, adopting the pattern from the start is a good idea. Otherwise, refactoring into it later is quite straightforward.

For developers building UIs in a reactive style, this pattern makes larger and more intricate components significantly easier to manage.

On the flip side, it’s excessive for simpler screens, so its usage should be evaluated on a case-by-case basis.

What happens under the hood with ngIf?

To conclude our exploration of the ngIf directive, let’s examine the peculiar * syntax used with *ngIf and what it actually signifies.

The * syntax indicates that ngIf is a structural directive, meaning it alters the page’s structure.

When Angular encounters the *, the template compiler processes the template from its initial form:

Angular then de-sugars the *ngIf syntax into the following structure:

As shown, under the surface, the *ngIf directive is just a standard Angular attribute that targets the ngIf property.

The * syntax simply indicates that the content of the element where the directive is applied is treated as an ng-template, which may or may not be inserted into the page depending on whether the structural directive decides to instantiate it.

It’s worth noting that this de-sugaring process applies to all structural directives (like *ngFor, etc.), not just ngIf.

Summary

As we’ve seen, the ngIf directive functions like the missing if-then-else feature of HTML.

With it, you can easily add or remove elements from the page based on the truthiness of a JavaScript expression.

When an element is removed using ngIf, it is removed entirely—it’s not merely hidden via CSS.

If you adopt a reactive style for your applications, ngIf is frequently paired with the async pipe to consume observable data.

For components with numerous data sources, this can result in extensive ngIf nesting and repetitive code in the template just to access data, along with startup UI issues.

If you encounter these problems, consider refactoring your component to use the single data observable pattern, where only one data observable is supplied to the view.

This approach dramatically simplifies the template and enhances the component’s initial load experience. The combineLatest example mentioned is common, but you are free to use any other operator combination to craft exactly the data observable the view requires.

I hope this deep dive has been informative. If you want to learn more about all the available Angular Core directives, we recommend the Angular Core Deep Dive course.

Additionally, if you have questions or feedback, please leave a comment below and I will respond.

To stay updated on future posts about Angular, consider subscribing to our newsletter:

And if you’re just starting out with Angular, check out the Angular for Beginners Course:

Angular ngIf: Complete Guide — figure 2