Over ten years ago now, I remember being on holiday with my parents, with an old laptop that I had gotten from work. My parents had always chosen a beautiful sunny spot, right near the beach. For most teenagers, that would have been ideal. But as a socially awkward, overly tall, geeky kid, I was inside on my laptop.
I remember this time because I was feverishly trying to learn AngularJS. And, like most things when you’re sixteen, almost none of it made sense. I’m just not a studious person, so I can’t break out a book and read about the topic at hand.
No, the way I learn things is to try to make something using them, and fail, over and over again, until I have something that works.
Many years and slightly anti-social family holidays later, I’ve sure grown up a lot. And, what do you know, so has Angular.
Since then, we’ve had the big change to Angular 2 from AngularJS, and now with Angular 20 releasing recently, it’s interesting to reflect on just how Angular has grown up over the years.
There are a bunch of articles covering what’s new in Angular 20, but this isn’t one of those. It’s more of an appreciation of how the framework has evolved over time, how it’s improved, and how the core team has more or less made the right decisions at important times.
Angular has had a long and varied development history, and at times it has felt outright stagnant. Bugs have remained unsolved for a long time. Simple things were needlessly complex. Other issues, like bad performance, caused a measure of developer attrition as people switched to other frameworks. Honestly, I couldn’t tell you what changed between Angular 8 and Angular 14.
But, more recently, the overall direction is good, or even great. It’s never been a better time to be an Angular developer, at a time when it does feel like the framework has a bit of a new lease on life. Of course, there are major changes that occur in each release of Angular, and frequently, these new updates are covered in great detail.
Let’s review the entire history of Angular 2 and up, and reflect on how the framework has matured. I‘ll explain why, in my opinion, the current direction is better than it’s ever been.
When developing a web framework like Angular, React, or Vue, one of the first things you have to ascertain is how state change will be handled within your app.
Within other frameworks like Vue or React, changes to the application state are manually applied via a call to something like setState()
. This gives control to the developer to decide when updates should be applied to the view.
This does introduce a measure of complexity, as deciding when the view should update becomes a hot-button issue. In more complicated views, needlessly pushing updates to our application UI can slow down the app. This lends itself to the existence of many state management solutions for React, like Redux or Zustand. These solutions place our application state in a store, and as the store is updated, the UI of our application changes.
Within Angular, it’s possible to not use a third-party library like Redux to manage application state. Instead, you can just use the core Angular library to manage state throughout your application.
What do I mean? Well, for comparison’s sake, this is a very simple counter app within React. The count variable increases by one when the button is clicked:
function MyButton() { const [count, setCount] = useState(0); function handleClick() { setCount(count + 1); } return ( <button onClick={handleClick}> Clicked {count} times </button> ); }
Let me draw your attention to the second line here.
The useState
function sets up an initial state for our counter app (which is zero), and then provides a setCount
function via destructuring. It’s a little complex, but the point is, whenever you call the method to set the variable value, you’re essentially firing up a little flare to tell React that the variable has changed.
React has no smarts to know if the variable has actually changed or not, so it will carry out the same functionality each time this is called.
Now, compared to Angular:
export CounterComponent{ count = 0; incrementCount(){ count++; } }
And then our template:
<button (click)="incrementCount()">The button has been clicked {{count}} times</button>
We can see that we’re changing the value of the count variable, but we’re not doing anything to notify Angular of the change. Instead of a specific function being run to say “hey, I’ve changed the application state”, Angular uses change detection to try to figure out what has changed, and then apply those changes to the view.
The Angular variation of the above looks simpler to us developers. But if something looks simple, it can be that the real heavy lifting is occurring elsewhere. Angular is bearing the responsibility of checking for changes and updating the view as required. How this change detection works is covered in great detail here.
To demonstrate, let’s give the example of the humble setTimeout
function. On a static HTML page with no JavaScript, executing setTimeout
will execute the vendor-supplied functionality (by Chrome, Firefox, or whoever makes your browser).
But in Angular, at page load, these global window functions are replaced by their patched equivalents. So after completing an HTTP GET
request or hovering over a component, the patched implementation is executed. The original request runs, and then change detection is run afterwards.
Now, obviously, it’s just a terrible idea to redefine common functionality to a new function. If we had the gall to do so and tried to get it through code review in a company project, we’d probably get a long and stern conversation from our senior developer. That is, of course, unless we had a Really Good Reason ™️.
Angular has a really good reason. Without it, views would never update. But, as could be expected, sometimes things happen and variables change, and Angular is not aware of those changes.
Angular can’t account for absolutely 100% of possible events that may trigger a UI update. It’s kind of stuck in a never-ending game of whack-a-mole, trying to capture all browser API’s that could possibly cause a UI change. And then, once the change detection runs, the UI is updated when the values that are bound change.
In some ways, it’s kind of a lose-lose. Existing API’s in the browser get monkey-patched (this is the actual term, which does not inspire confidence). Change detection gets run very frequently, and is not 100% reliable; it can (and does sometimes) miss updates. It’s also naturally a terrible idea, as older versions of Angular rely on these browser callbacks operating in a certain way. It’s not impossible that an older Angular app could be broken by a browser update that affects how these monkey-patched API’s work.
It’s a bit of a hot take, but this approach to checking for UI changes is flawed. Every time something happens that “could” cause a UI change, change detection runs, and Angular sifts through each component looking for changes. If nothing has changed, well, we’ve still wasted clock cycles iterating through something that is unchanged. It’s not a good proposition. Interestingly, its something I wrote about back in 2021.
We’ve established the problems with this approach. And honestly, the chances that something as integral as change detection was reimplemented within Angular seemed unlikely.
Part of the reason why this is a problem is because of the types of data that Angular uses. In other frameworks that have view binding, if you want to use data in your view, you must inherit from a specific interface and then call a function every time your value has updated.
This is also the wrong choice because it leads to a poor developer experience. Devs can forget to inherit from the interface or to call the appropriate function. Whatever happens, the outcome is usually “my view doesn’t update and I don’t know why.” This leads to sifting through code, trying to find the one place where you forgot to call the very special function to notify that a change has, in fact, occurred.
Angular places no such restrictions on what kind of variables we use within the view, so we can use string
or number
or any type of primitive or complex data type. The tsunami-sized wrinkle in this is that these simple primitives are not reactive, and cannot be made reactive.
We can’t set a new value to a string and then get notified that the variable has changed. We can’t, for instance, redefine what occurs when a new string
or number
gets instantiated (thank goodness for that!). So instead, Angular nervously checks when things have happened that could have caused a change, and then runs change detection accordingly.
Fortunately, Angular ships with RxJS, which includes types such as Observable
and Subject
. These types let us define things like Subject<string>
, which do notify when values are changed. However, as someone who has written very reactive applications with lots of forms and complex usages of RxJS, I know it can be cumbersome to use.
A good example of the maturity and good direction that Angular has is that the Angular team could have doubled down on their existing approach. But instead, a combined shift is underway to move away from Zone.js and towards a better reactivity solution by use of signals.
Signals have been added to Angular since v17. With signals, our updated code looks like this:
export CounterComponent{ count = signal(0); incrementCount(){ signal.update(x => x++); } }
We must make a very small change to our template (calling count()
instead of just binding to the count
variable):
<button (click)="incrementCount()">The button has been clicked {{count()}} times</button>
We retain the elegance of our template, but we gain immensely from the fact that our signal has the ability to notify us when it has changed. If all of our variables that can change are signals, then Angular can rely on those notifications for when to update the UI.
Logically, if the only time that data is updated is via a signal, then we could solely rely on that to know when to update the UI. No more Zone.js shenaniganizing, monkey patching, and hoping to catch an event that will appropriately trigger a UI update.
Way back in the AngularJS days, and even before Angular 17, HTML would be conditionally rendered based on HTML directives. ng-if
, and then *ngIf
were the ways to check if a component should be rendered or not.
Angular originally started out as a way to supercharge HTML. I mean, it literally says that on the AngularJS website in 2012:
It’s not a lie to say that we can achieve all of our design goals within HTML. But, just because we can, should we? On one hand, everyone who knows HTML will know how to interact with our thing. But on the other hand, we run the risk of becoming overly verbose.
To demonstrate, let’s imagine that we have an array of items. We want to conditionally render items in the array and show a custom message if the array is empty.
Using the older *ngIf
and *ngFor
, it’s a bit of a hot mess:
<div *ngIf="items.length > 0; else noItems"> <ul> <li *ngFor="let item of items; let i = index"> {{ i + 1 }}. {{ item }} </li> </ul> <div *ngIf="items[0] === 'Admin'"> First item is Admin! </div> </div> <ng-template #noItems> <p>No items found.</p> </ng-template>
Yeah, it’s technically HTML, and would pass a HTML/XML validator. But at what cost? We need a new element every time we want to use a conditional operator. When our array is empty, we have a separate ng-template
container for what to render instead. In this simple example, it’s relatively easy to follow. But in larger projects and templates, it quickly becomes unwieldy.
Again, more work could have been done to somehow work these if
/else
statements and switch case statements into something that was still HTML-ish. But instead, Angular parted with tradition and implemented control flow syntax. What follows isn’t valid HTML, but is far more obvious in what it’s achieving:
@if (items.length > 0) { <ul> @for (item of items; let i = index) { <li>{{ i + 1 }}. {{ item }}</li> } @if (items[0] === 'Admin') { <div>First item is Admin!</div> } </ul> } @else { <p>No items found.</p> }
Suddenly, following what is happening becomes a lot easier. The same can be said of switch cases within Angular:
<div [ngSwitch]="user.role"> <p *ngSwitchCase="'admin'">Welcome, Administrator!</p> <p *ngSwitchCase="'editor'">You can edit content.</p> <p *ngSwitchCase="'viewer'">You have read-only access.</p> <p *ngSwitchDefault>Unknown role.</p> </div>
Again, this is valid HTML, but it’s not super clear how user.role
is related to what is being switched on. The use of text in the switch case could make it easy for typos to be introduced. It’s hard to be exhaustive in our switch case. And, the default switch path is a bit hard to understand as well.
Compared with:
@switch (user.role) { @case ('admin') { <p>Welcome, Administrator!</p> } @case ('editor') { <p>You can edit content.</p> } @case ('viewer') { <p>You have read-only access.</p> } @default { <p>Unknown role.</p> } }
Immediately, following the flow of the code becomes a lot easier. It’s possible for IDE tooling to check that we’ve accounted for every possible value that user.role
can be.
Control flow syntax is a definite developer experience improvement. But, more to the point, it completely flies in the face of ten years of Angular. For example, the RFC on the Angular GitHub discussion board has 66 downvotes at the time of writing. Compared with 207 upvotes and 301 thumbs up, it’s not an inconsequential number of people rejecting the idea.
Even if you introduce a good change to an ecosystem like Angular, some people won’t like it solely because they don’t like change. Maybe they wrote custom tools that depend on having the page as valid HTML, who knows?
The point is, the Angular team went to great lengths to explain the change, and compared it to using other solutions (like using a more HTML-based solution) before implementing it.
The Angular team also shipped migrations that would migrate your code away from *ngIf
and towards control flow syntax, so you could take advantage of it in existing codebases pretty much straight away.
Finally, the original *ngIf
directive has been deprecated as of Angular 20 despite being available since the very early days of Angular. This means there’s a clear direction as to what should be used in the future, for newer developers.
All in all, it’s a class act. We got a thoroughly documented RFC including what changes are coming, good community engagement, automated migrations for existing code, and no ambiguity as to what to use in the future. What else could you ask for?
Angular has been around for nearly a decade. Naturally, it accumulated features and functionality that — while sensical at the time of release — may not make as much sense to continue.
A good example of this is flex-layout within Angular. For a long time, flex-layout was an easy way to use flexbox layouts within your Angular apps. Instead of using CSS, you could use directives like fxFlex
or fxFlexFill
to apply CSS to certain Angular elements. It was published to the Angular namespace on NPM, and had been in beta since it was released 10 years ago.
The general consensus was that, eventually, flex-layout would mature, become stable, and then be integrated into the mainline Angular packages. Until then, developers were confident. Because it was published under the Angular namespace on npm, it felt like a genuine first-party Angular project.
Instead, suddenly, the package was deprecated, and it was only given a year more of LTS support before it would lose support entirely.
Admittedly, communication wasn’t great, and many developers were spooked by how quickly this all seemed to happen. Even I wrote about how I was unhappy about it.
But, in retrospect, I realize now that I was just unhappy about the prospect of having to learn something else. Since then, I learned how to use Tailwind for my styling.
It makes sense that combining CSS functionality with directives in Angular was probably a bad idea after all. If something is wrong with your styling, you can’t just look at the CSS. You have to mentally calculate the impact of your CSS and the inline styles applied by flex-layout.
Moving on from flex-layout saved a generation of developers from getting stuck between CSS and a non-standard flexbox solution, and ultimately, led to higher-quality solutions overall. It’s not easy to mothball something with 300k weekly downloads. But if it benefits the overall quality of the framework, then it’s probably the right thing to do. The Angular team made the right, but difficult, choice.
More improvements to Angular are coming in the near future, and Angular 20 is already shipping an experimental mode for zoneless change detection. Control flow looks slated to expand. All of this, combined with the fact that Angular has everything you need in the box (router, server-side rendering, deferred view loading, etc), shows that we’re just getting started.
Angular is definitely growing up, and it looks like the best years may still be ahead.
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 lets you replay user sessions, eliminating guesswork by showing exactly what users experienced. It captures console logs, errors, network requests, and pixel-perfect DOM recordings—compatible with all frameworks.
With Galileo AI, you can instantly identify and explain user struggles with automated monitoring of your entire product experience.
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.
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowES2025 adds built-in iterator helpers to JavaScript, enabling lazy .map(), .filter(), .take(), and more; ideal for processing large or infinite data streams efficiently.
Explore 15 essential MCP servers for web developers to enhance AI workflows with tools, data, and automation.
Learn how to integrate MediaPipe’s Tasks API into a React app for fast, in-browser object detection using your webcam.
Integrating AI into modern frontend apps can be messy. This tutorial shows how the Vercel AI SDK simplifies it all, with streaming, multimodal input, and generative UI.