Introduction
This is a note for advanced usage of signal in Angular 21.
First, let's revisit the basic usage of signal. A signal is a reactive value that can be observed by other components. It is similar to a variable, but it can be observed by other components.
const count = signal(0); // create
count.set(1); // set the value of the signal
count.update((x) => x + 1); // update the value of the signal
count.subscribe((x) => console.log(x)); // xxx This is wrong, signal cannot be subscribed to.
const count$ = toObservable(count);
count$.subscribe((value) => console.log(value)); // This is correct, signal can be subscribed to, but we need to use toObservable to convert
const dependent = linkedSignal(count, (x) => x + 1); // create a dependent signal, which is a signal that is derived from another signal.
To differentiate the signal from the variable, some signal are writeOnly, other are readOnly.
For example,
const count = signal(0); // create a writeOnly signal
const doubleCount = computed(() => count.value * 2); // create a readOnly signal
To think about why some signal are write only, and some are read only, we need to understand the concept of dependency. Usually in frontend development, we need to update the UI when the data changes. So we need to observe the data changes, and update the UI accordingly. And signal is a way to observe the data changes and then update the state.
To observe signal change and then do something. We can use effect().
effect(() => {
console.log(count.value);
});
When we use effect(), we are telling Angular to observe the signal changes, and then do something when the signal changes.
Effect always execute asynchronously, during the change detection process. And effects are rarely needed in most applicaiton code. Here is some examples of when to use effect:
- logging data being displayed and when it changes, either for analytics or debugging tool.
- Keeping data in sync with
window.localStorage - Adding custom DOM behavior that cannot be expressed with template syntax.
- Performing custom rendering to a <canvas> element, or 3rd party UI library.
Managing async data with signals
Now let's talk about how to manage async data with signals.
Signal introduces Resources API to handle async operations.
Resources API provides built-in loading states, error handling, and request management.
Looking at the definition of Resource.
function resource<T, R>(
options: ResourceOptions<T, R> & { defaultValue: NoInfer<T> }
): ResourceRef<T>;
function resource<T, R>(
options: ResourceOptions<T, R>
): ResourceRef<T | undefined>;
// To consume this, for example:
const userResource = resource({
parmas: () => ({ userId: userId() }),
loader: ({ parmas }) => fetchUser(parmas),
});
// Aborting request also can be done
const userResource = resource({
params: () => ({ userId: userId() }),
loader: ({ params, abortSignal }) => fetchUser(params, abortSignal),
});
The resource object also has some properties that are useful to manage the async data.
value- The recent value of the resource. orundefinedif the resource is not loaded yet.hasValue- Whether the resource has a value.isLoading- Whether the resource is loading.error- The error of the resource.abort- Abort the resource.status- The status of the resource.
The status signal provides a specific ResourceStatus that describes the state of the resource using a string constant.