DEV Community

Cover image for Exploring Angular’s evolution: Why it’s worth another look
Matt Angelosanto for LogRocket

Posted on • Originally published at blog.logrocket.com

Exploring Angular’s evolution: Why it’s worth another look

Written by Lewis Cianci✏️

Angular has always enjoyed widespread popularity amongst web developers. At the same time, it’s experienced significant — at times even quite jarring — changes. It’s a good time to look back at the history of Angular, where we came from, where we are now, and where it’s headed for the future. We’ll also take a look at a few specific features that may make Angular worth your time, including control flow syntax, Signals, and more.

The good old days: How AngularJS began

AngularJS first debuted in 2010, 13 short years ago. At the time, the web was experiencing something of a renaissance when it came to JavaScript frameworks.

Before the rise of these frameworks, websites would typically serve HTML to users. This meant that whenever the user clicked a button or submitted a form, the data would get posted back to the server.

While this was an entirely functional way of building a website, the experience was a little lackluster compared to using an app on a desktop computer, or even on a mobile phone. Frequently, webpages would flash white between page loads, and webpages didn’t really feel like a cohesive experience.

At that time, the closest tool that existed to produce responsive websites was Adobe Flash. However, even that had its own issues with security and performance.

As JavaScript matured, the rise of the single-page application (SPA) came with it. AngularJS was an early entrant to this, letting developers create beautiful and responsive websites.

This framework improved over time, but so did the growing pains. Scalability was an issue, testing was complex, and other issues arose.

From AngularJS to Angular 2

As the years wore on, the web continued to evolve. In 2013, React was released as another option for web development. While it was radically different from how AngularJS was designed, React offered the benefits of higher performance and cleaner code.

This was one of the reasons why in 2016, only six years after the initial release of AngularJS, Angular 2 — better known to most as just “Angular” — was released. AngularJS was still actively supported at this time and would continue to be so for some time, but Google had shown Angular to be the future of the framework.

In every way, Angular was an improvement over AngularJS. Whereas AngularJS generated HTML for the browser to interpret, Angular simply created the DOM objects directly for the browser. TypeScript brought type safety, and dedicated testing tools like Karma helped developers build well-tested applications.

However, while these changes progressed the framework, they were not well received at the time. Developers were unhappy about the complete lack of an upgrade path from AngularJS to Angular, along with the radically different way that apps would be built.

The reason for this is simple: developers have a lot to remember at any given time. Being told that their existing knowledge would soon become irrelevant and that they would have to learn several new things to stay relevant was not a popular announcement.

Still, the newer iteration of Angular was released, and subsequent versions improved the framework over time. In 2020, Angular changed to a new compiler and runtime that brought many improvements. More recently, Angular 14 introduced strongly typed forms, making data entry via forms less error-prone.

Around the same time, however, Angular deprecated the widely used flex-layout package and completely flubbed the announcement in relation to this.

This was a huge layout library in use throughout many applications, and deprecating this package meant that apps that depended on it would not compile anymore. Angular’s response? “Just use CSS”.

Understandably, people were not impressed. Avid Angular developers who had extensively used flex layouts had to migrate to another layout library like Tailwind in order to upgrade to future versions of Angular.

Teams are usually already pushed to their limits with existing bug fixes and feature requests. Being forced to find time to rewrite your styling in your app because of an upstream decision is never enjoyable.

So, where does that leave Angular today?

Considering Angular’s present and future

Angular is a very competent, high-quality, batteries-included frontend framework that is widely used in personal or corporate settings to produce engaging user experiences. It’s definitely a mature framework, and other first-party components like Angular Material add to its quality.

At the same time, it’s not perfect. The change from AngularJS to Angular 2 was so vast that developers had to start over, and the sudden deprecation of packages like flex-layout has caused some unease within the Angular developer community.

Each time a major change occurs in a framework like Angular, it’s a lot of time spent learning a new way to do things. After all, nobody wants to be stuck holding the bag when the thing they were using gets relegated to the software development dustbin.

So, how should we feel towards Angular? If we’re on another framework like Svelte or React, should we consider it? The answer to that depends heavily on your particular need and application.

However, it’s important to note that the framework itself has shown drastic improvements in recent times. Let’s dive into what those improvements are and why Angular developers are excited about what the future holds for this framework.

Control flow syntax

When designing applications, one of the core tasks you can perform is to show or hide certain controls conditionally depending on a variable’s state.

In a simple use case, you can handle this task using the ngIf directive. Basically, this directive determines whether or not to render the component by evaluating a condition to be either true or false.

A good example of this is whether to show a loading indicator or not. In earlier versions of Angular, you’d wind up with something like this:

<div class="text-center" *ngIf="!dataLoaded">
  <p>Loading data...</p>
</div>
<div class="text-center" *ngIf="dataLoaded && cat">
 <p>Your ideal cat is a {{cat.idealType}}</p>
</div>
Enter fullscreen mode Exit fullscreen mode

However, in reality, server responses can be more complex than just success or failure. For example, a request can succeed but can return empty data. If you’re not careful, your markup could become cluttered with ngIf statements for every possible scenario, making it harder to manage and understand.

Another option is to use ng-template to control what is displayed and when, like so:

<ng-container *ngIf="server.response===200; else serverFail">
  The request suceeded!
</ng-container>

<ng-template #serverFail>
  <ng-container *ngIf="server.response===401; else otherError">
    Not authorised :(
  </ng-container>

  <ng-template #otherError>
    Somethinge else happened...
  </ng-template>
</ng-template>
Enter fullscreen mode Exit fullscreen mode

This method works, but it’s not easy to read or understand, especially compared to more intuitive if...else statements. In this simple example, it’s not too difficult to correlate the contents of the ngIf statements to the appropriate identifiers in the file, but this can become quite fatiguing in more complex situations.

Fortunately, Angular is introducing control flow statements to bring order to these kinds of situations. Consider the below instead:

@if (server.response === 200) {
  The request suceeded!
} @else if (server.response === 401) {
  The user is unauthorised :(
} @else {
  Something else happened, response was {{server.response}}
}
Enter fullscreen mode Exit fullscreen mode

Straight away, it’s much easier to understand the logical flow of this template. It’s clear what will be rendered depending on how the variables are evaluated. The reduction in the amount of boilerplate code as compared to the use of ng-template is clear.

These benefits also extend to other areas, like switch statements. Previously, switch statements might have looked like this:

<div [ngSwitch]="server.response">
  <ng-container *ngSwitchCase="200"> <!-- Case 200 -->
    The request succeeded!
  </ng-container>

  <ng-container *ngSwitchCase="401"> <!-- Case 401 -->
    Not authorized :(
  </ng-container>

  <ng-container *ngSwitchDefault> <!-- Default case -->
    Something else happened...
  </ng-container>
</div>
Enter fullscreen mode Exit fullscreen mode

What’s happening here is a little bit muddled. We’re switching on the server.response value, and then using the ngSwitchCase to conditionally render a ng-container. Longer switch statements would require a new ng-container for every conditionally rendered element.

With control flow syntax, this code instead becomes the following:

@switch (server.response) {
  @case (200) {
    The request succeeded!
  }
  @case (401) {
    Not authorized :(
  }
  @default {
    Something else happened...
  }
}
Enter fullscreen mode Exit fullscreen mode

Again, the improvements that come with control flow syntax are clear. We can easily see what would be rendered depending on the evaluation of the server.response variable.

These benefits also apply to for loops within Angular. Whereas previously you would use *ngFor as a directive, control flow makes for loops a lot easier to implement. For example, imagine that we have an itemArray containing an array of items. We can iterate through that array like so:

<ul>
  @for (item of itemArray; track item; let i = $index, let f = $first; let l = $last, let ev = $even, let o = $odd; let co = $count) {
    <li>{{item}}
      <ul>
        <li>Current index is {{i}}</li>
        <li>Is this the first item in the array: {{f}}</li>
        <li>Is this the last item in the array: {{l}}</li>
        <li>Is the index for this item even: {{ev}}</li>
        <li>Is the index for this item odd: {{o}}</li>
        <li>Total count of items in array: {{co}}</li>
      </ul>
    </li>
  } @empty {
    <li>No items in the array</li>
  }
</ul>
Enter fullscreen mode Exit fullscreen mode

At first blush, it looks pretty amazing. But what’s actually going on here? The main thing we’re interested in is the variables called out in the for line. Let’s break those down:

  • item of itemArray — The main array we are iterating through, and what to refer to each item within the array as
  • track item — The property used to track each object. Normally, this could be the ID of a particular item in a list. Note that while trackBy was optional, track item is now required, as it can help improve performance in larger lists
  • let i = $index — Lets us use the i variable to get the index of the current item
  • let f = $first — Checks if the item is first in the list
  • let ev = $even — Checks if the item’s index is even
  • let o = $odd — Checks if the item’s index is odd
  • let co = $count — The count of the items in the array

Finally, @empty is an optional case defining what to do if the list is empty.

What I find interesting here is the availability of so many variables within the micro-syntax. This makes it easy to know if the current item is an odd or even item, or even if it’s the first or last item.

You could easily achieve this with the modulo operator as well — for example, by using something like index % 2 == 0 to check if an item is even. However, it feels like the Angular team is striving for ease of use here, which is something I personally appreciate.

Deferring component loads

When it comes to shipping your web app, your bundle size matters quite a bit. If your app is functionally amazing, but using it requires your users to wait several seconds while megabytes of data are transferred in, then it’s going to lead to poor user experience.

Additionally, every time you ship an update for your app, people will have to bring in a whole new JavaScript bundle. This will further negatively affect user experience.

That’s why it makes sense to split your app up into smaller bundles and load those bundles when required. If we wanted to do that from scratch, it would be quite the undertaking. Fortunately, in Angular 17, we can implement this almost effortlessly.

For example, if we wanted to render our app-lazy-component when our variable changed to true, we could do the following:

   @defer (when isVisible) {
      <app-lazy-component />
    }
    @loading {
      Hold on while our lazy component gets ready...
    } @placeholder {
      The component isn't loaded yet.
    } @error {
      Whoops, the component blew up!
    }
Enter fullscreen mode Exit fullscreen mode

Unlike an ngIf directive, once the isVisible variable changes to true, the component is loaded and will stay in the DOM even if it changes back to false.

There are also a few other cool things we can see in this example, like the easy ability to display loading, placeholder, and error results. We can also trigger components to load after an event or after a specified length of time.

Better still, we can specify a minimum amount of time that a placeholder should be displayed before the new component gets rendered. This prevents a flash from happening when the new component is loaded if it’s ready sooner than expected.

Introducing Signals

Angular 16 introduced Signals to help deal with asynchronous data and how to display it within an Angular app. But asynchronous data has been around since the initial release of Angular, so why is Signals being introduced at this late stage in the game?

Up until now, to deal with data that updated over time, you may have used something like an Observable, Subject, or BehaviorSubject within Angular. These still-excellent tools from RxJS can help you write responsive applications and will continue to be usable in Angular via the async pipe.

Now, Signals exists as a simple way to define and update data that may change over time. Within our component, setting up a signal is as simple as this:

changesOverTime = signal(1);
Enter fullscreen mode Exit fullscreen mode

Then, to read the value of the signal, we just use it in the template like so:

{{changesOverTime()}}
Enter fullscreen mode Exit fullscreen mode

But, wait a minute! While it’s always been possible to call functions from within Angular templates, it’s always been discouraged.

This is because calling synchronous or blocking functions from our templates can cause our app to lock up for seconds at a time. Depending on Angular’s change detection as well, a function can be called multiple times until a view finally settles.

But still, we’re wary. After all, we’ve been told to not use functions for so long! So, let’s make ourselves at peace with this new rule by exploring why we can use these safely.

In a nutshell, there are three functions available to Signals within Angular:

  • set() — Replaces the value of the signal with a new value
  • update() — Uses the current value of the signal to set a new value (e.g., adding a number to an existing number)
  • mutate() — Modifies the value of a signal (e.g., removing or adding an item to an array)

Calling any of these methods will notify every place in the code that uses these Signals when the values are changed. Because they are not observables, you don’t need to subscribe or unsubscribe to them, which is a nice bonus over traditional RxJS.

Signals can also take the value of other Signals and use them to calculate new values, like so:

firstValue = signal(1);
secondValue = signal(2);

computedValueSignal = computed(() => this.firstValue() + this.secondValue());

updateSignal()
  {
    this.firstValue.set(5);
    this.secondValue.update(value => value + 1);
  }
Enter fullscreen mode Exit fullscreen mode

Then we can use {{computedValueSignal()}} in our template to display the value.

Okay, but now we’re really suspicious! Not only are we calling functions within our template, but we’re also completing an operation within the computed value. Surely this will introduce jank into our app when the change detection cycle runs, won’t it?

The answer is that it could, but it won’t be anywhere as bad as you expect it to be. The reason why is a good one.

Computed signals are only updated when two conditions are met: first, dependent signals are updated; second, the value of a computed signal is read. Better still, even though it’s a computed signal, the new value is memoized or cached when it is calculated.

Obviously, this is still not the place for long-running operations.

However, if you do mistakenly call a long-running blocking operation, you’ll only incur that penalty when the dependencies are updated and the new value is read. Angular won’t repeatedly recalculate the computed value on every call or view update, which is a nice benefit.

Standalone components

Besides the great new features we’ve already discussed, standalone components are another valuable improvement to Angular. In previous versions of Angular, every new component required an NgComponent, which was then used by other parts of the application.

While this worked, it introduced quite a bit of boilerplate and required every component within Angular to be imported and then exported through an NgComponent. It also affected tree-shaking and bundle size.

Standalone components allow for components, pipes, and directives within Angular to be declared without requiring a dedicated NgComponent. This makes them easier to create and also improves tree-shaking outcomes.

Overall, it’s not as much of a big change to the framework as control flow syntax, or signals, but it does show an overall move toward a better framework.

Is Angular worth another look?

So, where does that leave us? On one hand, Angular has introduced some difficult changes in the past that have been hard to get behind. However, it’s hard to ignore that it’s improving rapidly.

React developers still outpace Angular developers in terms of technologies being used, but a lot of great work is being put into Angular at the moment. Angular also ships a lot of fantastic other tooling that works more or less straight out of the box, like routing and dependency injection.

This makes Angular a more complex and heavier framework, but it also means you don’t have to choose something from a list of options. Additionally, it means that all the required bits for Angular, like routing or Angular Material, are all first-party and enjoy a consistent level of support.

Frameworks like Svelte or Vue are far lighter than Angular and will always be useful for a particular purpose. But if you’ve looked past Angular once in your life already, it might be worth looking again. You may get more than you bargained for, and these improvements we’ve covered could be the start of many more.


Experience your Angular apps exactly how a user does

Debugging Angular applications can be difficult, especially when users experience issues that are difficult to reproduce. If you’re interested in monitoring and tracking Angular state and actions for all of your users in production, try LogRocket.

LogRocket Signup

LogRocket is like a DVR for web and mobile apps, recording literally everything that happens on your site including network requests, JavaScript errors, and much more. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred.

The LogRocket NgRx plugin logs Angular state and actions to the LogRocket console, giving you context around what led to an error, and what state the application was in when an issue occurred.

Modernize how you debug your Angular apps — start monitoring for free.

Top comments (0)