Handling user actions with signals in angular

Alfredo Perez
ngconf
Published in
3 min readMay 18, 2023

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.

Shows how the action stream filters the data by the company selected by the user.

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 toSignalfunction 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:

You can find me on Twitter and on my website.

--

--