In this article, we will explore Angular signals, a powerful feature that has recently been introduced by the Angular team. We'll delve into what signals are, their significance, and the problems they aim to address within the existing Angular ecosystem. Signals serve as straightforward reactive primitives that store values for consumers to read. Essentially, they act as wrappers that provide notifications when the stored value changes. To grasp the functionality of signals, we'll proceed by creating a new Angular application and examining their operation firsthand.
Create a new project by
running the next command
ng new signal-sample
After creating a new project, it would be great to migrate our project to standalone APIs using angular schematics. As of version 15.2.0, Angular offers a schematic to help project authors convert existing projects to the new standalone APIs. The schematic aims to transform as much code as possible automatically, but it may require some manual fixes by the project author.
Run the migration in the order listed below, verifying that your code builds and runs between each step:
- Run ng g @angular/core:standalone and select "Convert all components, directives, and pipes to standalone"
- Run ng g @angular/core:standalone and select "Remove unnecessary NgModule classes"
- Run ng g @angular/core:standalone and select "Bootstrap the project using standalone APIs"
- Run any linting and formatting checks, fix any failures, and commit the result
The last step which is
required to upgrade angular to 16 version:
ng update @angular/cli
@angular/core --next
Now we can start implementation of our first application and after that change it to signals. Open app.component.ts and add the following code there
We also need to update the markup in app.component.html
Run this application via npm
start to check that everything works as expected.
There is a considerable drawback related to the existing application. Let’s put a console log to our totalSum and check how often this getter will be called.
Open the development
console to check the result after clicking on the Change Count button.
As you see, after the
application has started, we have 4 console log calls and after the button click, we
have additional two calls.
Update this code using signal:
These changes required updating the markup as well.
Let’s run our application
and check that everything works as expected without any issues.
As you see, totalSum
shows up only once in the console log and after clicking on Change Count, your changes
will be applied only one time.
Change Detection Today:
Zone.js
Angular assumes that
event handlers have the potential to modify any bound data. Consequently, after
executing event handlers, the framework performs default checks on all
component bindings for changes. However, by leveraging the more powerful OnPush
mode, which utilizes Immutables and Observables, Angular can significantly
reduce the number of components that require checking.
Regardless of whether we
opt for the default behavior or OnPush, Angular needs to be aware of when event
handlers are executed. This poses a challenge because it is the browser, not
the framework, that triggers these event handlers. To address this issue,
Zone.js comes into play. Zone.js extends JavaScript objects like window,
document, and prototypes such as HtmlButtonElement, HtmlInputElement, or
Promise using monkey patching. By modifying these standard constructs, Zone.js
can determine when an event handler has been executed. It then notifies
Angular, indicating that it handles the change detection process.
Let’s see how it works under the hood. NgZone will be created when we bootstrap our application:
bootstrapApplication calls the internal method internalCreateApplication. The responsibility of this internal method is creating NgZone and using monkey patching technics to patch the browser’s native API and bring some additional behavior, interceptors, and hooks to it.
One of the responsibilities of class NgZone is to create a child’s zone exactly for their angular application and it uses special interceptors like onInvokeTask, onInvoke etc. to bring the connections between angular and zone. For example, onInvokeTask callback will be called by Zone to execute macro tasks, microtasks, or events. So, let’s try to understand how it affects our change detection.
Once the task is executed and before leaving the call stack, Angular checks a few things for micro tasks and event tasks. The most interesting here is the function onLeave.
And check the function checkStable (I’ve removed some Angular team comments because they are redundant)
Here when we do not have any pending microtasks !zone.hasPendingMicrotasks and just call onMicrotaskEmpty event handler.
And just the last piece is missing. Who is subscribed to this event?
Probably the name of this service speaks for itself. When either the microtask or event task is completed, we run change detection by calling this.applicationRef.tick(); in the scope of NgZone. tick() method just calls change detection for all our views.
Give it a try to simplify this workflow by using a diagram.
Change Detection
Tomorrow: Signals
Here is a simple
diagram that demonstrates how the signal works.
count = signal(1);
If the consumer is a
template, it can notify Angular about changed bindings. In the terminology of
the Angular team, the signal occurs as a so-called producer. As described
below, there are also other building blocks that can fill this role.
There are two types of Signals.
They can be either writable or read-only.
In our sample we have created two types of signals: count – writable and computed is read-only.
Here is the code of the signal function from angular source code:
In the code snippet
provided, we can see that signal utilizing the WritableSignal<T>
class. This class offers four essential methods: set, update, mutate,
and asReadonly. To modify the value of a writable signal, you have two
options. Firstly, you can use the .set() method to directly assign a new
value. Alternatively, you can employ the .update() operation, which
allows you to compute a new value based on the previous one. In situations
where the signal holds an object, directly mutating the object can be
beneficial.
Here's a sample demonstrating the usage of signals, provided by the Angular team:
Let’s discover read-only signals like computed and effects.
Under the hood, ComputedImpl class has a special method for notifying consumers that the value in signal has potentially changed.
If one of the dependencies changed in the scope of computed() the value will be recomputed.
And here is the information on what UNSET, COMPUTING, and ERRORED are.
If you are already experienced with state management in the context of Angular, working with computed signals should feel quite familiar. A notable advantage of using the `computed()` function is that signals created with it are lazily evaluated and cached for efficiency. Furthermore, it's important to note that computed signals derive their value from other signals. To define a computed signal, you can utilize the `computed()` function and specify a derivation function:
In our sample, we derive from count() signal.
Effects:
The last part of read-only signal is known as an effect(). An effect is an operation that is executed whenever the value of one or more signals undergoes a change. To create an effect, you can employ the effect function:
Effects always run at
least once. When an effect runs, it tracks any signal value reads. Whenever
any of these signal values change, the effect runs again. Similar to computed
signals, effects keep track of their dependencies dynamically, and only track
signals which were read in the most recent execution.
Effects always are executed asynchronously, during the change detection process.
Probably, you are interested in how it works under the hood. The functionality is very simple for understanding there.
First of all, Angular creates a queue for watch and zone and adds the effect to this queue. After that method flush() run all watches from the queue by calling watch.run().
Use-cases for effects
recommended by Angular team:
Effects are rarely needed in most application code, but may be useful in specific circumstances. Here are some examples of situations where an effect might be a good solution:
- Logging data being displayed and when it changes, either for analytics or as a debugging tool
- Keeping data in sync with window.localStorage
- Adding custom DOM behavior that can't be expressed with template syntax
- Performing custom rendering to a <canvas>, charting library, or other third-party UI library
You should avoid using
effects for propagation of state changes. This can result in
ExpressionChangedAfterItHasBeenChecked errors, infinite circular updates, or unnecessary
change detection cycles.
Use RxJs as a workaround
before signals released
Signals will be available only when Angular 16 is released, however, you can use now a similar functionality by using RxJs library based on observables. For example, computed and effect can be replaced by BehaviorSubject + combineLatest function. Because in our case we have only one dependency (count property), let’s check how our code will look with BehaviorSubject.
And our template should look familiar to you as well.
While signals may appear
similar to Angular's long-standing mechanism, RxJS observables, they are
intentionally designed to be simpler and often adequate for many scenarios.
For more complex cases, signals can be seamlessly integrated with observables. The @angular/core/rxjs-interop namespace provides two functions: toObservable, which converts a signal into an observable, and toSignal, which performs the reverse conversion. These functions enable the utilization of signal simplicity alongside the robust capabilities of RxJS.
Signals benefits:
Signals represent a major
advancement in Angular's reactive programming capabilities and change detection
features. Signals are on their way! They will enhance the reactivity and change
detection of our code, making it more responsive. Additionally, they will
simplify the code creation and readability process. And let's not forget, they
bring a lot of excitement and fun! They are new game changers in Angular.
NB! Do not think of signals as a new idea that was implemented by the Angular team. Preact, Solid, and Vue all employ some version of this concept to great success. Angular team has taken the expertise of SolidJS framework. “We've taken a lot of inspiration from these and other reactive frameworks, and we are especially grateful to Ryan Carniato of SolidJS for his willingness to share his expertise and experience in many conversations over the last year. That said, requirements across frameworks differ widely, and we've designed our version of signals to both meet Angular's specific needs and as well as take full advantage of Angular's unique strengths.”
No comments:
Post a Comment