Angular Signals in v17 – A revolution?

With Angular’s fast-paced release cycle of a new major release every 6 months, it’s easy to lose track of all the changes in the framework. Especially, because Angular remained mostly stable or some might say even uninteresting in the last years. Thus, if you didn’t have a specific problem, there wasn’t much of a reason to closely monitor each release.

Of course, there were interesting developments such as standalone components, reactive and strict types forms, and the switch to the new Ivy compiler. But at least for me, there were no new features that made me think wow, this could change the way I use Angular or make my life as a front-end developer significantly easier.

That changed when I read about Angular v16 and the new reactivity model with Signals. It kind of looked like the Vue.js reactivity model, which I greatly enjoyed when I used it. This and the Angular team’s statement that with Angular v16 they’re continuing the Angular Momentum with the biggest release since the initial rollout of Angular got me hooked on delving deeper into Signals. In addition, with Signals graduating from developer preview with the recent release of Angular v17, now is the perfect time to start thinking about how you can utilize Signals in your day-to-day work.

At this point, don’t get me wrong. There is nothing wrong in using RxJS and Observables and they absolutely shine in the right use cases. But if you ever had the feeling that easy problems are suddenly complex just because an Observable is involved, this article is for you. So what if there was something you could use without getting a knot in your brain every time? That would be great, wouldn’t it?

If that sparked your interest, keep on reading to find out:

  • Why the current approach to Reactivity has its downsides
  • What Signals are
  • What this means for RxJS and Observables
  • How real-life requirements can be simplified by using Signals
  • Which path Angular has taken with v16 and how it continues it with v17

Reactivity: How we know it

Zone.js For most of its existence, Angular relied on zone.js for change detection and consequently for reactivity in the framework. Although zone.js freed the developer from explicitly declaring which properties should be reactive and which not, it also showed some severe downsides over time. This includes:

  • Serious overhead at application startup, especially for smaller apps with 100kb of zone.js JavaScript that must be loaded before anything else
  • ExpressionChangedAfterItHasBeenCheckedError We’ve all been there
  • Change detection is always run for the whole application, which becomes problematic in larger applications
  • Function calls in templates are discouraged and can lead to serious performance issues
  • ChangeDetection.onPush is hard to use right and thus more of a last resort when performance issues arise

RxJS The second pillar on which Angular’s reactivity is based are Observables and the RxJS library in general, which provides us with reactive extensions for JavaScript. Angular as a framework makes heavy use of Observables and uses them for core components such as the HttpClient or reactive Forms. Especially because of the HttpClient all Angular developers are required to learn RxJS to some extent. And while Observables definitely have their place, it often feels like they are forced on the developer without merit in the current situation. This leads to:

  • A steep learning curve to use the framework
  • Hard to grasp code with memory leaks when Subscriptions aren’t cleaned up
  • Imperative and confusing code inside of .subscribe() blocks
  • Situations where simple problems become complex, just because Observables must be used

As a seasoned Angular developer, most of these points aren’t that much of a deal. But still, why should we overcomplicate things unnecessarily? For example, in a recent project, I had the requirement to introduce simple feature flags into our application. I wanted to have one location to switch features on and off to avoid dealing with a branching concept and to ensure that existing features are not impacted by new code.

For this, I started with a simple implementation that sets the feature flags statically in the backend and serves the feature flags via an API to the Angular frontend. The feature flags were then used in guards and services to disable routes or functionality in templates. So far so simple, but because the feature flags are loaded via an API I had to deal with Observables.

I only wanted to buy a few groceries and not learn how to fly an airplane to get there

Complexity Suddenly there are Subscriptions to be managed, async pipes, unset initial values that cause the feature flags to glitch, RxJS operators to combine feature flag checks with other checks, and so on. In this case, there wasn’t a lot of time to implement this and thus I’ve taken the easy route and put the logic into the template, just to avoid dealing with RxJS operators.

Of course, this isn’t that hard to build if you have a bit of time. But still, in my opinion, it’s not a good sign when a task that would be effortless with the async/await syntax, becomes suddenly complex, just because of Observables being present. Especially if they do not bring any additional benefit, but are forced on the developer just because the HttpClient uses them.

Therefore, I was really intrigued when reading about Angular Signals and their promise of a simpler reactivity model. Perhaps from now on we can choose the right reactivity primitive for the right task, instead of Observables for everything. Let’s see if that’s the case.

Simplified reactivity with Angular Signals

So, at this point, one might say, well, if you use an enterprise-grade framework like Angular, why are you complaining about learning RxJS. Just use something else then. Surely, this is a fair point, but again why should we overcomplicate things, if it could also be straightforward?

This is where the new Angular signals come into the picture. Basically, they allow us to have fine-grained reactivity, but in the same way, also a much simpler model to work with asynchronous properties. The Signal system consists of three core parts:

  • Signals
  • Computed signals
  • Effects

How do they work?

Signals are the core primitive and are data structures that notify all interested consumers when their value changes. In contrast to observables, they always have an initial value and provide us with synchronous access to their current value. Consumers accessing the Signals value are subsequently notified about changes and can then update their value. With signals we can work with asynchronous data, and maintain reactivity, but without the hassle of Subscription management. Furthermore, Angular knows exactly what has changed and doesn’t have to check everything as it has to with async pipes.

Additionally, the value inside of a signal can also be mutated or updated based on its current value. In other words, the current value can be used to derive a new value or a modified value of the existing value, which is then written back to the signal. For example, if you store a list of items, you initially retrieved from an API, you can add or remove items by using the mutate/update methods of signals.

Angular Signals and their consumers

Computed signals are the second primitive, used to derive a new value from one or many Signals and are re-evaluated when one of the Signals changes. With them, multiple signals can be composed to a new Signal or used to build live updating functionality based on other Signals. A simple example would be counting all items in a table component, which should change whenever items are added or removed, or a loading panel that should be shown as long as another signal isn’t populated.

Effects as the third primitive are used to perform side effects when referenced Signals change. They can be a good fit to persist data to local Storage and keep that data in sync with changes or to perform DOM manipulations. However, they should be used with caution as they can also lead to infinite loops and other errors when used incorrectly. For this reason, it is not possible to mutate Signals from within effects, except when explicitly disabling this safeguard.

Furthermore, effects need an injection context so that they can be tied to the lifecycle of a component or service to be cleaned up as soon as the enclosing component is destroyed. To sum it up, effects are there to solve problems you can’t solve with (computed) signals but should be used with care. This is also reflected in the fact that they not graduating from developer preview in the v17 release, while the other signal primitives are.

Light at the end of the tunnel
Are signals the light on the horizon above a sea of observable hardships?

How can developers benefit from Signals?

For developers it’s no longer important to think of stuff like memoizing functions, using pure pipes, or to avoid calling functions in templates like the plague to reduce the load on change detection. It even opens a path to zoneless applications that significantly improve the startup performance of small apps without sacrificing developer productivity.

On top of that, they also reduce the risk of accidental memory leaks or multiple calls to the API if you forget to use the shareReplay or distinctUntilChange operators. In addition, developers no longer have to think about the pitfalls introduced by the OnPush changeDetectionStrategy and can no longer introduce errors by using it without fully understanding it.

Even your future self or future colleague will thank you when they don’t have to know complex RxJS operators or debug hard-to-grasp bugs introduced by faulty RxJS usage. Overall, it frees mental capacity, which can be put to better use, while at the same time improving the performance of an app.

Hence, the simplified reactivity model using signals has the potential to make all current best practices for building a performant Angular app irrelevant, at least as far as change detection is concerned. And that’s a good thing! Because it means that the overall complexity is reduced and the developers can concentrate on what matters the most: implementing features that create added business value.

Real-life use cases simplified with Signals

Now, after all the theory and promises, how do Signals look in practice when used in real-life use cases? For this, we will examine a few non-functional requirements we often need in our projects.

  • Authorization decisions loaded from an API with Route Guards and Checks in components
  • Feature Flags loaded from an API with Route Guards and Checks in components
  • A combination of Authorization and Feature Flag checks
  • A Table with a loading panel and a count of the entries which is secured with Authorization and feature flags

Authorization Let’s start with the first example. For this, we use Casl.js as an authorization library because it allows us to define and load Authorization rules from a simple JSON data structure and has an easy-to-use interface. Moreover, a JSON structure is a natural fit if you want to define authorization rules only once in the backend and share them with your frontend. But of course, the message from this example stays the same for other libraries and approaches as long as some kind of API calls and thus Observables are involved.

// authorization check with observable-based implementation
export const adminAreaRouteGuard: CanActivateFn = (route, state) => {
  const service = inject(AuthorizationService);
  const router = inject(Router);

  return this.service.canRead('admin-area').pipe(
    map((canAccess) => canAccess ? true : this.router.parseUrl(APP_ROUTES.startpage.link)),
  );
}

// authorization check with signal-based implementation
export const adminAreaRouteGuard: CanActivateFn = (route, state) => {
  const service = inject(AuthorizationService);
  const router = inject(Router);

  return service.canRead('admin-area') ? true : router.parseUrl(APP_ROUTES.startpage.link);
};

With the authorization service from above we can now check if a user is authorized to read, i.e. view, parts of our application and block access to the route if the user is not authorized. As we can see the signal-based implementation is a bit more concise and doesn’t require knowledge about RxJS operators. For this work, I used the toSignal() Operator to convert the Observable from the HttpClient to an easy-to-use Signal.

Feature flags The second example is about feature flags, which we use to enable or disable parts of the application. Subsequently, this allows shipping code that’s not finished yet, without impacting existing use cases. Again, this is a simple true/false check initially. However, as we don’t want to define the flags twice, we first load them from an external provider, in this example our backend, and then must deal with Observables. A simple check for a single feature flag is still easy and looks almost the same as in the authorization example. A bit more challenging is to check if at least one of our feature flags is enabled.

// feature flag check with observable-based implementation
isFeatureEnabled$: Observable<Boolean> = this.isAtLeastOneOfTheFeaturesEnabled(['newFeature', 'saisonalPromotion']);

isAtLeastOneOfTheFeaturesEnabled(names: string[]): Observable<boolean> {
    return this.featureFlags$.pipe(
      map((featureFlags) => featureFlags.filter((feature) => names.includes(feature.name))),
      map((featureFlags) => featureFlags.every((feature) => feature?.isEnabled ?? false)),
    );
}

<app-new-feature *ngIf="isFeatureEnabled$ | async" />

// feature flag check with signal-based implementation
isAtLeastOneOfTheFeaturesEnabled(names: string[]): Signal<boolean> {
    return computed(() =>
      names.some((name) => this.featureFlags()?.find(
        (feature) => feature.name === name)?.enabled ?? false
      )
    );
}

<app-new-feature *ngIf="isAtLeastOneOfTheFeaturesEnabled(
  ['create-blog-posts',
 'enter-blog-post-data'])() />

Again, this is not overly complex and there are not that many differences between the Observable-based and the Signal-based solution. One immediate benefit though is that the signal-based implementation can be dynamically called from the template, while the Observable-based solution should be composed in advance. This doesn’t make a difference in the example from above but would increase the boilerplate code when performing multiple checks. Additionally, one should note, that the Observable-based solution would require even more boilerplate code if the async pipe wouldn’t manage subscribing and unsubscribing of the Observable for us.

As a reminder, if we would call the Observable via a function from the template, the function would be re-evaluated every time the changeDetection is triggered, due to the imprecise tracking of zone.js. This can become problematic, when event listeners run hundreds of times per second, for example when tracking the users’ mouse events. Calling the signal on the other hand can be precisely tracked by Angular, thus the computed signal is only re-evaluated if the input or dependent signals are changed.

Authorization combined with feature flags Often we won’t need authorization or feature flags in isolation, but in combination. We can easily do it in the template by using the async pipe and combining both expressions with an && or || operator. However, this means to put logic into the template, which doesn’t scale well. A better approach is to combine both Observables into a new Observable with RxJS Operators. This could then look somehow like this.

public combinedChecksWithObservables$ = combineLatest([
  this.authService.canRead('AdminArea'),
  this.featureFlagsService.isFeatureEnabled('AdminAreaFeature'),
]).pipe(
  map(([a, b]) => a && b),
);

Looks neat, doesn’t it? But being concise isn’t everything and it can be quite challenging to come up with this if you don’t know that the combineLatest operator exists and how it works. Furthermore, this would become a magnitude harder if we had distinct types of Observables that we want to combine. So, let’s compare it with the Signal based solution.

private canReadAdminArea = toSignal(this.authService.canRead('AdminArea'));
private isAdminAreaFeatureEnabled = toSignal(
  this.featureFlagsService.isFeatureEnabled('AdminAreaFeature')
);

public combinedChecksWithSignals = computed(
  () => this.canReadAdminArea() && this.isAdminAreaFeatureEnabled()
);

The signals-based solution requires a bit more code, but it doesn’t require deep knowledge of asynchronous streams and the behavior of operators. You only need to know that there are Signals, Computed Signals that combine multiple signals into one result, and that there is a toSignal operator that converts an Observable to a Signal. But that’s it.

As often, if you already have the Observable-based solution, it looks great, but coming up with the Signal-based solution should be a lot easier for most developers out there. In the end, this saves time which can be used to get the actual job done.

Overview table with authorization, feature flags, and a loading panel To round it up, I’ve created an overview table with a loading panel, authorization, and feature flag checks, and an on-the-fly count of the contained data at the end. It shows a loading panel until the data is loaded from the API, only shows a create button when the user is authorized to do so, and hides a new feature, a count of the contained data at the bottom, as long as the feature flag is disabled.

This should give you a feeling of how an everyday component looks like when being implemented with Signals instead of Observables. The full example is hosted on GitHub in a code repository that accompanies the blog post. It can be run locally and has a mocked API to run as a standalone example.

...
export class BlogPostsTableComponent {
  @Input({ required: true }) vm!: BlogPostsViewModel;

  blogPost: BlogPost = {
    title: 'Freds new blog post',
    author: 'Fred',
    url: 'https://whiteduck.de/freds-new-blog-post',
    date: new Date(),
    keywords: ['Signals demo'],
  };

  isDialogVisible: WritableSignal<boolean> = signal(false);

  submitted: Signal<boolean> = computed(() => !this.isDialogVisible());
  
  areBlogPostsLoading: Signal<boolean> = computed(
    () => this.vm.blogPosts() === undefined
  );

  blogPostCount: Signal<number> = computed(
    () => this.vm.blogPosts()?.length ?? 0
  );

  canUserCreateAndIsBlogPostCountFeatureEnabled = computed(
    () => this.canCreateBlogPosts() && this.isBlogPostCountFeatureEnabled()
  );

  private canCreateBlogPosts: Signal<boolean> = 
    this.authService.canCreate('angular-blog-posts');

  private isBlogPostCountFeatureEnabled: Signal<boolean> = 
    this.featureFlagsService.isFeatureEnabled('blog-post-count');

  createBlogPost(): void {
    this.vm.blogPosts.update((blogPosts) => {
      if (blogPosts) { blogPosts = [...blogPosts, this.blogPost]; } 
      else { blogPosts = [this.blogPost]; }
      return blogPosts;
    });

    this.isDialogVisible.set(false);
  }
...
Let’s rewrite everything and say goodbye to RxJS?

So away with Observables, RxJS, and all its complexity?

Definitely not. Even if you might be tempted, especially as a new developer, to simply skip learning RxJS, this is not a wise idea. RxJS is a fantastic library for specific tasks, such as implementing event listeners, working with asynchronous data or event streams, and needing fine-grained control over the time of execution or scheduling of the next event in a stream. That’s where RxJS shines and where it feels great to use.

On the other hand, Observables are not a great fit for representing asynchronous values in templates, which leads to all the ramifications outlined in the reactivity section above. That’s why Angular Signals are introduced by the Angular team, instead of using Observables as the reactive primitive.

RxJS Interop Furthermore, there is a lot of existing code and libraries relying on RxJS and a lot of built-up knowledge around it. For this reason alone, RxJS will stay around and that’s why interoperability with it is crucial. Luckily, the Angular developers have a similar opinion on the subject and are introducing the @angular/core/rxjs-interop package which converts Signals to Observables and vice versa in an effortless way.

The new takeUntilDestroyed Operator even further simplifies our lives as developers and makes cleaning up subscriptions and working with Observables much more straightforward. In particular, simplifying the use of observables in components also means that developers are encouraged to use observables where it makes sense and benefits them.

Thus, on the one hand, this shows that RxJS is not expected to go away and on the other hand allows developers to freely choose the best tool for the task at hand.

Sounds promising, so can I use Signals today?

Now that we’ve learned that Signals are great for everyday tasks in templates and how RxJS still fits into the picture, you might be wondering if you should be using Angular Signals, and if you should start today or not. Especially now that Angular Signals has come out of the Developer Preview, or whether you should wait until they are more widely used in Angular. As often, it depends. In order to make an informed decision, we should take a look at what is still missing from the overall picture of Angular Signals.

Signal-based components One limitation that I quickly come across when playing with signals is the (current) lack of signal-based components and component inputs. This means sharing signals between parent and child components is not a pleasant experience now. It’s fairly simple to work around this by using a service to share state between components, but for simple use cases, it’s just an additional boilerplate and has some pitfalls. Nevertheless, it’s only an inconvenience and shouldn’t discourage the use of signals.

Lifecycle hooks To complement Signals Angular will introduce new lifecycle hooks in signal-based components and remove the lifecycle hooks that are now replaced by Signals and their derivates. With the new afterNextRender, afterRender, and afterRenderEffect there will be new lifecycle hooks available, which are focused on executing code after rendering operations were performed. On the other hand, ngOnChanges, ngDoCheck, ngAfterViewInit, ngAfterContentInit, ngAfterViewChecked, ngAfterContentChecked will be removed from Signal-based components. So if you rely heavily on the hooks that will be removed or plan to migrate components, you should probably wait for the stable version of the Signal-based components to avoid migrating twice.

Forms Furthermore, Signals support is also missing from Reactive Forms and the ngModel syntax. While you can work around this by using the toSignal operator or implement an explicit two-way binding instead of using the banana-in-a-box syntax, signals support is not there yet. As with components, you should wait for a release of signal-based forms before migrating existing forms.

Generally, there is no blocking issue to using Signals and they can used without waiting for Angular to add Signals support to all building blocks. However, if you are planning to migrate or rewrite parts of your application and use Signals, it may be a good idea to wait for the release of signal-based components to avoid migrating to an outdated component variant.

Where does Angulars roadmap lead to?

Is that all Angular v16 improved?

Signals are undoubtedly the most remarkable part of v16, but the version offers even more:

You can find a more detailed overview of all the new parts introduced in v16 in the Angular team’s release blog post.

Where is Angular headed with v17?

Generally speaking, Angular is heading to a more simplified learning curve with a reduced initial overhead. Both which contribute to the success of React and Vue.js and might Angular make a more interesting choice for developers. Combined with a lower bundle size by making zone.js optional, better performance, and reduced complexity through reduced usage of Observables, Angular is on the right path to stay relevant for new developers and projects.

The recent release of Angular v17 reflects this. With a simplified control flow syntax, improved runtime performance through the new control flow syntax, and deferable views, which are combined with revised documentation, the v17 release stays on the path Angular chose with v16. Correspondingly, these changes should further help to make Angular more approachable for developers and a better fit for small and medium-sized applications.

Conclusion

Now, are Angular Signals a revolution or not? In my opinion, it greatly depends on your point of view. If you are focused on creating business value and feel like Observables get in your way from time to time, Angular Signals will feel like a revolution. If you are a fan of concise code and familiar with all the RxJs Operators, it’s more of a nice addition. But nevertheless, which of both you are, Signals allow you to use the best tool for the job and that’s a great thing.

Finally, let’s answer the question of whether signals are a revolution or not. Combined with all the other changes that were introduced with v16 and are now being continued with v17, it definitely feels like a revolution to me, which will certainly make my life as a developer easier and more fun.

To answer this question yourself, I recommend you visit the accompanying Signals Sample Repository on GitHub. It includes a full sample application built with Signals and featuring the real-life uses cases from the blog post. It can be run standalone without an api as it includes simulated API data. Have fun exploring Signals on your own!

Thank you for reading my blog post, I hope you enjoyed it and can now make an educated choice if Signals are for you or not. If you liked it, feel free to check out my other Angular blog posts: