Angular v16 signals, everything old is new again

What's the hype?

Jose I Santa Cruz G
ITNEXT

--

Photo by Alex King on Unsplash

Most of you perhaps were too young to remember. <Read with a British narrator voice please> In ye olde AngularJs days, when you had to share some variable value between routes or components, there weren't any stores. The nearest thing to a "shared service" was the rootScope. And we ended putting everything inside it.

What does the rootScope have to do with signals?

Well, my first approach to Angular (v2>) was during the 2017 Jedi Hackster Hackathon, on which I got the 5th place with my Starwars jokes app. When thinking on how to make it appealing, and coming from Ionic v1 and AngularJs, I really missed the rootScope until I found this StackOverflow post which lead to this piece of code.

watch(variable: string): any {
let idx: number = this.keyIndex[variable];
try{
// let watchVar: Observable<any> = this.dataObservables[idx];
// return watchVar;
return this.dataArray[idx].asObservable();
} catch(err) {
throw new Error('Variable ' + variable + ' not present. Try setItem("' + variable + '", this.' + variable + ') first.');
}

}

unWatch(variable: string): void {
console.log('unWatch ', variable);
let idx: number = this.keyIndex[variable];
try{
let dataValue = this.dataArray[idx].value;
this.dataArray[idx].next(dataValue);
this.dataArray[idx].complete();
} catch(err) {
throw new Error('Variable ' + variable + ' not present. Try setItem("' + variable + '", this.' + variable + ') first.');
}
}
this.isJedi$ = this.rootScope.watch('isJedi')     
.takeUntil(this.ngUnsubscribe)
.subscribe(variable => {
this.isJedi = (variable != undefined) ? variable.value : undefined;
console.log('isJedi watcher ', variable);
});

Once again, I could watch (observe/subscribe) for variable's value and when "listening" to the changes I could trigger some actions, such as changing the app font's color, or enabling the Shyriwook translation. On that time I understood nothing about RXJS, or Subscriptions, or BehaviorSubject (and not that nowadays I really understand them fully 😅 ).

Still don't see the relation with signals…

What's a signal anyway?
A signal is a variable that when it changes "things" happens.

signals…

Most examples show a counter (Wow! c'mmon…), and in fact, even though it's like the Pokemon API starter some bootcamps like to use as their best example, it works.

A signal behaves as a single source of truth, it changes and every other place that listens to this signal will also change, and maybe even display the updated value, something happens. Just as traffic lights on an ideal scenario, the light signal changes to green and all cars on the opposite side can pass, if it goes red all cars have to stop.

I made some tests myself, nothing really conclusive on how it will change the way we use Angular. But what I did found is quite interesting:

  1. ChangeDetection : It doesn't matter which ChangeDetectionStrategy you use, when using signals things will happen. This will probably lead to what ( ̶a̶l̶m̶o̶s̶t̶ ̶) everyone is waiting ➡️ ZoneLess Angular.
  2. Stores and state management : When looking for a single source of truth, for example a shopping cart in an eCommerce app, we usually think on a certain kind of store, NgRx, Akita, Elf, or any other state management store library (even any rootScope like solution). This point requires some extra explanations.
  3. Functions in templates : We all learn that calling a function inside an Angular template is a huge "no no no" "do not do it" "avoid at all costs". When calling an Angular signal (eg. hilite=signal<boolean>(false);) from within a template, you have to use the underlying function, and this particular functions do not drive Angular ChangeDetection crazy:
<div class="card"
[ngClass]="{
'hilite': hilite()
}">

The test app

I tried to make a simple example app, not too simple as a counter app, or a name changing example, but simple enough to try to understand signals.

  • An API request to https://jsonplaceholder.typicode.com/users
  • A user card component for displaying users in the list, and also displaying the "current user"
  • A button to hi-light any card
  • A button to set the current user
  • Several versions of the same page (default ChangeDetection, onPush ChangeDetection, and a signals version). Home and No signals are there just to use space, not really useful.

The hardest part was getting the right versions for Angular 16:

  "dependencies": {
"@angular/animations": "^16.0.0-next.5",
"@angular/common": "^16.0.0-next.5",
"@angular/compiler": "^16.0.0-next.5",
"@angular/core": "^16.0.0-next.5",
"@angular/forms": "^16.0.0-next.5",
"@angular/platform-browser": "^16.0.0-next.5",
"@angular/platform-browser-dynamic": "^16.0.0-next.5",
"@angular/router": "^16.0.0-next.5",
"@nrwl/angular": "^15.8.9",
"rxjs": "~7.8.0",
"tslib": "^2.3.0",
"zone.js": "^0.13.0"
},
"devDependencies": {
"@angular-devkit/build-angular": "^16.0.0-next.5",
"@angular-devkit/core": "^16.0.0-next.0",
"@angular-devkit/schematics": "^16.0.0-next.0",
"@angular-eslint/eslint-plugin": "~15.0.0",
"@angular-eslint/eslint-plugin-template": "~15.0.0",
"@angular-eslint/template-parser": "~15.0.0",
"@angular/cli": "^16.0.0-next.5",
"@angular/compiler-cli": "^16.0.0-next.5",
"@angular/language-service": "^16.0.0-next.5",
"@nrwl/cypress": "15.8.9",
"@nrwl/eslint-plugin-nx": "15.8.9",
"@nrwl/jest": "15.8.9",
"@nrwl/js": "15.8.9",
"@nrwl/linter": "15.8.9",
"@nrwl/nx-cloud": "latest",
"@nrwl/workspace": "15.8.9",
"@schematics/angular": "^16.0.0-next.5",
"@types/jest": "^29.4.0",
"@types/node": "16.11.7",
"@typescript-eslint/eslint-plugin": "^5.36.1",
"@typescript-eslint/parser": "^5.36.1",
"cypress": "^12.2.0",
"eslint": "~8.15.0",
"eslint-config-prettier": "8.1.0",
"eslint-plugin-cypress": "^2.10.3",
"jest": "^29.4.1",
"jest-environment-jsdom": "^29.4.1",
"jest-preset-angular": "~13.0.0",
"nx": "15.8.9",
"prettier": "^2.6.2",
"ts-jest": "^29.0.5",
"ts-node": "10.9.1",
"typescript": "~4.9.5"
}

many extra unused dependencies. The important ones are all the ones that look like:
"@angular/*": "^16.0.0-next.*"

Lets focus on the With signals page

import { ChangeDetectionStrategy, Component, inject, OnChanges, OnInit, signal, SimpleChanges, DoCheck } from '@angular/core';
import { CommonModule, NgFor, NgIf } from '@angular/common';
import { UserService } from '../../services/user.service';
import { User } from '../../types/user.interface';
import { UserCardComponentSignaled } from '../../components/user-car-signaled/user-card.component';
import { GLOBAL_SIGNAL_SERVICE } from '../../services/global-signal.service';

@Component({
selector: 'ng16-signals-with-signals',
standalone: true,
imports: [
CommonModule,
UserCardComponentSignaled,
NgFor,
NgIf
],
templateUrl: './with-signals.component.html',
styleUrls: ['./with-signals.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class WithSignalsComponent implements OnInit, OnChanges, DoCheck {
userService = inject(UserService);
globalSignalService = inject(GLOBAL_SIGNAL_SERVICE);
users = signal<Array<User>>([]);
currentUser = this.globalSignalService.getSignal<User>('currentUser');

constructor() { }

ngDoCheck(): void {
console.log('ngDoCheck');
}

ngOnInit(): void {
console.log('ngOnInit');
this.userService.retrieveUsers().subscribe(users => {
this.users.set(users);
});
}

ngOnChanges(changes: SimpleChanges): void {
console.log('ngOnChanges', changes);
}
}

The user list is stored on a signal named users.
The currentUser is retrieved from a so called globalSignalService

import { inject, Injectable, InjectionToken, signal, WritableSignal } from '@angular/core';

@Injectable({
providedIn: 'root'
})
export class GlobalSignalService {

signalMap = new Map<string, WritableSignal<unknown>>();

constructor() { }

getSignal<T>(key: string): WritableSignal<T> {
if (!this.signalMap.has(key)) {
this.signalMap.set(key, signal<T | undefined>(undefined));
}
return this.signalMap.get(key) as WritableSignal<T>;
}

setSignal<T>(key: string, value: T): void {
this.getSignal<T>(key).set(value);
}
}

export let GLOBAL_SIGNAL_SERVICE = new InjectionToken<GlobalSignalService>('GLOBAL_SIGNAL_SERVICE', {
providedIn: 'root',
factory: () => inject(GlobalSignalService)
});
  //...
setCurrent(): void {
console.log('setCurrent', this.user);
this.globalSignalService.setSignal('currentUser', this.user);
}

The GlobalSignalService acts just a SharedService, where all "shared signals" would be stored. And this is why everything old is new again, this is almost the same strategy from AngularJs when using the rootScope, you just use a signal and the magic happens. No Subscriptions, BehaviorSubjects, hot or cold Observables, just use thesignal. This GlobalSignalService is used as our single source of truth for the currentUser, just like a store, but without "all the extra bloat" . — Wait, extra bloat you say? — Yes, I mean stores, actions, reducers, and effects. But…

… signals aren't just plain variables. They also have effects, or can be build upon other signals (computed), or can even be mutated. There's too much reading material, too much to learn yet (just Google for Angular signals and see the more than you can read amount of articles).

Will signals erradicate RxJS?

Hell no! (many would love this happened). But it will make it simpler to solve some problems, for eg changing the app font's color when choosing between Jedi, Sith or Bounty Hunter.

The HttpClient is still based on Observables. RxJS is not evil, it just needs some love, understanding and dedication 😄

Will signals change the way we use Angular

It sure will! Just the way things changed during the transition from AngularJS to Angular v2> , or more recently when standalone components became stable. We'll have to learn to deal with a "not so" new feature.

Change is good, we only have to embrace it.

A good read to keep an eye on is the Angular reactivity space on Github. And if you want to play around, the test project is here: https://github.com/jsanta/ng16-signals-tests

Don't get confused :P

--

--

Writer for

Polyglot senior software engineer, amateur guitar & bass player, geek, husband and dog father. Not precisely in that order.