Handling user actions with signals in angular
Now that we have signals with Angular 16, the question is how we handle asynchronous data streams with user interactions.
You might be familiar with the idea of an action stream, which Deborah Kurata, if I'm not mistaken, invented, and these action streams were created to represent user interactions.
To recap, the main idea was to create an Observable
from a previously defined BehaviorSubject
that was later updated based on user interactions.
selectedCompany = new BehaviorSubjectstring>('');
selectedCompanyAction$ = this.selectedCompany.asObservable();
selectCompany(value: Event)
const newValue = (value.target as HTMLSelectElement).value;
this.selectedCompany.next(newValue);
}
Thanks to creating a new observable from the user interaction, you can now react to changes, load more data, or mix with other asynchronous streams:
public filteredUsers$ = combineLatest([
this.manageUsersFacade.users$,
this.selectedCompanyAction$,
]).pipe(
map([users, company]) =>
company === ''? users: users.filter((user) => user.company === company)
)
);
The pattern of creating action streams was very effective. It helped to improve change detection since everything could be treated as a stream and you could rely on the async pipe to handle data and avoid subscribing to observables.
So, how do we do it with signals?
A signal is a wrapper around a value that can notify interested consumers when that value changes. Signals can contain any value, from simple primitives to complex data structures.
The first part is to transform the data stream into a signal. To do this, you can convert the observable using the toSignal
function or,selectSignal
if you are using NgRx.
const usersSignal = toSignal(this.usersApiService.list());
users = this.store.selectSignal(selectAllUsers)
Then, to create the “Action Signal," we have to declare it with an initial value and modify it once the user makes a change:
selectedCompany = signal<string>('');
selectCompany(value: Event){
const newValue = (value.target as HTMLSelectElement).value;
this.selectedCompany.set(newValue);
}
The final step is to replace the combineLatest
that we used when both streams were observables. To do this, we are going to use thecomputed
method that reads values from both streams and creates a new signal whenever one of them changes. Here is how it will look:
// 👇 This creates the new Signal
public filteredUsers = computed(() => {
// 👇 Signal with the data from the API request
const users = this.manageUsersFacade.users();
// 👇 "Action Signal" that reacts to user interactions
const company = this.selectedCompany();
return company === ''
? users
: users.filter((user) => user.company === company);
});
TL;DR
You can declare “Action Signals” that can be updated whenever there is a user interaction and use compute
to create new signals from multiple signals while still enjoying the benefits of having more performant code without observables and subscriptions.
Here is an example: