> This might change around Angular 17.2, though that's not set in stone.
> Still, that shouldn't stop us from getting a head start on learning about signals and their benefits now, gearing up for what's just around the corner.
What's Inside
Here's a rundown of the topics we'll tackle:- What exactly are Signals?
- How do we access a signal's value?
- How can we change a signal's value?
- The update() Signal API
- Why bother with signals instead of plain primitive values?
- The computed() Signal API
- How do we subscribe to a signal?
- Is it possible to read a signal from a computed signal without linking them?
- What's the biggest trap when working with computed signals?
- Are signal dependencies set only by the first call to the compute function?
- Handling signals with array and object values
- Customizing the signal equality check
- Tracking signal changes with the effect() API
- How do Signals tie into change detection?
- Dealing with error NG0600: Writing to signals is not allowed
- Setting signals from within an effect, if necessary
- The default way effects are cleaned up
- Manually cleaning up effects
- Running cleanup tasks when an effect is torn down
- Working with read-only signals
- Sharing a signal across multiple components
- Building reactive data services with signals
- Signals and OnPush components
- Is it okay to create signals outside of components, stores, or services?
- How do Signals stack up against RxJs?
- Final thoughts
- Angular @if: Complete Guide
- Angular @for: Complete Guide
- Angular @switch: Complete Guide
- Angular @defer: Complete Guide
- Angular Signal Inputs: Complete Guide
- Angular linkedSignal: The Missing Link in Signal-Based Reactivity
Defining Signals
In simple terms, a signal is a reactive primitive that holds a value, letting us modify it in a deliberate way and keep tabs on its changes over time. Signals aren't unique to Angular. They've been around in various forms in other frameworks for quite a while. To get a grip on signals, let's look at a basic example that doesn't use them yet, and then we'll rework it with the Signals API. Our starting point is a straightforward Angular component featuring a counter variable:@Component(
selector: "app",
template: `
<h1>Current value of the counter {{counter}}</h1>
<button (click)="increment()">Increment</button>
`)
export class AppComponent {
counter: number = 0;
increment() {
this.counter++;
}
}
This component is about as simple as it gets.
It shows a counter value and includes a button to bump it up. It relies on the default change detection mechanism in Angular.
That means after every event—like clicking the increment button—the {{counter}} expression and any other expressions on the page get checked for changes.
As you might guess, attempting to figure out what needs updating this way can be pretty inefficient.
For this component, though, the approach is actually required, because our state is stored in a mutable plain JavaScript member variable called counter.
When an event fires, virtually anything on the page could have influenced that data.
Plus, clicking that increment button could have easily caused shifts anywhere else on the page, not just inside this component.
Think about a scenario where we call a shared service that impacts multiple parts of the page.
With default change detection, Angular has no way to know precisely what changed, so it can't assume anything and must check everything!
Since there's no guarantee about what might have changed, the entire component tree and all expressions on every component need to be scanned.
There's simply no alternative with default change detection.
Enter signals!
Here's the same example, but now rewritten with the Signals API:
@Component(
selector: "app",
template: `
<h1>Current value of the counter {{counter()}}</h1>
<button (click)="increment()">Increment</button>
`)
export class AppComponent {
counter = signal(0);
constructor() {
console.log(`counter value: ${this.counter()}`)
}
increment() {
console.log(`Updating counter...`)
this.counter.set(this.counter() + 1);
}
}
The signal-based version doesn't look all that different at first glance.
The key change is that we're now using the signal() API to wrap our counter value, rather than a plain counter variable.
This signal holds the counter's value, starting at zero.
What we see is that the signal acts as a container for the value we want to monitor.
Accessing a Signal's Value
Even though the signal wraps the value, we can retrieve it anytime by simply calling the signal like a function, with no arguments. Look at the code in the constructor of our signal-based AppComponent: constructor() {
console.log(`counter value: ${this.counter()}`)
}
By invoking counter(), we get the value stored in the signal—in this case, zero, the initial value.
Modifying a Signal's Value
There are a few ways to change a signal's value. Here, we're using theset() API in our counter increment function:
increment() {
console.log(`Updating counter...`)
this.counter.set(this.counter() + 1);
}
The set() API lets us assign any value to the signal, provided it matches the type of the initial value.
For our counter signal, that means we can only set numbers, since it started at zero.
The update Signal API
Alongside theset() API, there's also the update() API.
Let's swap it in for our counter increment function:
increment() {
console.log(`Updating counter...`)
this.counter.update(counter => counter + 1);
}
The update API takes a function that receives the signal's current value and returns the new desired value.
Both approaches to incrementing are functionally identical and perform equally well.
Why Choose Signals Over Primitive Values?
We've established that a signal is essentially a lightweight wrapper for a value. So what's the real benefit? The key advantage is the ability to get notified when the signal's value shifts, allowing us to react to the new data. That's not possible with a plain counter variable. When you use a plain value, there's no mechanism to alert you to changes. With Signals? It's a breeze! That's precisely why signals are worth using.The computed() Signal API
Signals can be built upon other signals. When one signal changes, any signals derived from it automatically update as well.
For instance, suppose we want a derived counter that's ten times the original counter's value.
Using the computed() API, we can create such a derived signal:
@Component(
selector: "app",
template: `
<h3>Counter value {{counter()}}</h3>
<h3>10x counter: {{derivedCounter()}}</h3>
<button (click)="increment()">Increment</button>
`)
export class AppComponent {
counter = signal(0);
derivedCounter = computed(() => {
return this.counter() * 10;
})
increment() {
console.log(`Updating counter...`)
this.counter.set(this.counter() + 1);
}
}
The computed API takes one or more source signals and generates a new signal based on them.
When the source (our counter signal) changes, the computed signal derivedCounter updates immediately.
So, after clicking Increment, the counter becomes 1 and derivedCounter hits 10, then 2 and 20, then 3 and 30, and so on.
Subscribing to a Signal
Notice that derivedCounter didn't explicitly subscribe to the counter signal in any way. All it did was call the source signal usingcounter() within its compute function.
And that was enough to connect the two!
From then on, whenever the counter gets a new value, the derived signal follows suit automatically.
It feels a bit like magic, so let's untangle what's happening:
- When we create a computed signal, the function we pass to
computed()runs at least once to establish the initial value of the derived signal. - As the compute function executes, Angular tracks which other signals are being accessed.
- Angular detects that calculating derivedCounter's value triggers the signal getter
counter(). - This tells Angular there's a dependency, so every time the counter gets a new value, the derived
derivedCounterupdates as well.
Reading a Signal from a Computed Signal Without Linking Them
In some advanced cases, you might want to peek at a signal's value from another computed signal without forming a dependency. This should be a rare occurrence, but if it's ever necessary, here's how:@Component(
selector: "app",
template: `
<h3>Counter value {{counter()}}</h3>
<h3>10x counter: {{derivedCounter()}}</h3>
`)
export class AppComponent {
counter = signal(0);
derivedCounter = computed(() => {
return untracked(this.counter) * 10;
})
}
The untracked API allows us to access the counter signal's value without creating a dependency between counter and derivedCounter.
Keep in mind, untracked is an advanced feature that should seldom be called for.
Using it frequently is a sign that something's off with your approach.
The key pitfall when creating computed signals
Correctly calculating derived values requires a specific approach, and there's an important detail that often trips people up.
Angular's dependency tracking works by observing which signals are read during the execution of a computed function. If a signal isn't read, Angular won't register a dependency on it.
This becomes particularly tricky when you introduce conditional branching inside the computation logic.
Consider this example that illustrates the problem:
@Component(
selector: "app",
template: `
<h3>Counter value {{counter()}}</h3>
<h3>Derived counter: {{derivedCounter()}}</h3>
<button (click)="increment()">Increment</button>
<button (click)="multiplier = 10">
Set multiplier to 10
</button>
`)
export class AppComponent {
counter = signal(0);
multiplier: number = 0;
derivedCounter = computed(() => {
if (this.multiplier < 10) {
return 0
}
else {
return this.counter() * this.multiplier;
}
})
increment() {
console.log(`Updating counter...`)
this.counter.set(this.counter() + 1);
}
}
Notice the conditional logic inside the compute function in the example above.
We're reading from the counter() source signal, but only under specific circumstances.
The intention here is to dynamically adjust the multiplier based on user interaction, such as clicking the "Set multiplier to 10" button.
However, this approach fails to work as intended!
While the counter itself increments correctly when you run this code, the expression {{derivedCounter()}} remains stuck at zero, even after clicking the button.
The root cause is that during the initial computation, there are no calls to counter().
The call to counter() sits inside the else branch, which never executes on the first pass.
Consequently, Angular never establishes a dependency link between the counter signal and the derivedCounter signal.
From Angular's perspective, these are two entirely separate, unrelated signals.
That's why updating the counter has no effect on derivedCounter.
The takeaway here is that we need to be thoughtful about how we define computed signals.
Whenever a derived signal relies on a source signal, we must ensure the source signal is invoked on every single call to the compute function.
If we don't, we break the dependency chain between them.
That said, this doesn't mean we can't use any conditional logic within a compute function at all.
Here's a version that handles this correctly:
@Component(
selector: "app",
template: `
<h3>Counter value {{counter()}}</h3>
<h3>Derived counter: {{derivedCounter()}}</h3>
<button (click)="increment()">Increment</button>
<button (click)="multiplier = 10">
Set multiplier to 10
</button>
`)
export class AppComponent {
counter = signal(0);
multiplier: number = 0;
derivedCounter = computed(() => {
if (this.counter() == 0) {
return 0
}
else {
return this.counter() * this.multiplier;
}
})
increment() {
console.log(`Updating counter...`)
this.counter.set(this.counter() + 1);
}
}
In this revised code, the compute function now calls this.counter() unconditionally on each invocation.
Angular can now properly detect the dependency between the two signals, making everything work as expected.
The multiplier will now be correctly applied after the first click on "Set multiplier to 10".
How are signal dependencies determined over time?
Not at all — dependencies for a derived signal are re-assessed dynamically each time its value is recalculated.
Each time the computed function executes, Angular rebuilds the list of source signals it depends on.
This means dependencies are fluid and can change throughout the signal's lifetime, rather than being locked in at creation.
This further emphasizes why we must be cautious with conditional logic when defining derived signals.
Working with signals containing arrays and objects
Up to this point, we've focused on signals with primitive values, such as numbers.
What happens though when a signal holds an array or an object?
For the most part, arrays and objects work similarly to primitives, but there are a couple of important nuances to be aware of.
Let's look at a signal holding an array and another holding an object:
@Component(
selector: "app",
template: `
<h3>List value: {{list()}}</h3>
<h3>Object title: {{object().title}}</h3>
`)
export class AppComponent {
list = signal([
"Hello",
"World"
]);
object = signal({
id: 1,
title: "Angular For Beginners"
});
constructor() {
this.list().push("Again");
this.object().title = "overwriting title";
}
}
These signals behave normally — you can read their values by simply calling them as functions.
However, the crucial difference from primitives is that nothing stops you from mutating the array directly with methods like push(), or from changing an object's properties directly.
In this particular example, the rendered output would be:
- "Hello", "World", "Again" for the list
- "overwriting title" for the object title
Now, this is definitely not the intended way to work with Signals!
Instead, signal values should always be modified using the set() or update() APIs.
Doing so ensures that any derived signals get a chance to recalculate, and the view updates accordingly.
Mutating the signal value directly bypasses the entire signal mechanism, opening the door to a host of potential bugs.
It's critical to remember: never mutate signal values directly — always go through the Signals API.
This warning is necessary because the Signals API currently provides no safeguards against this kind of misuse, such as automatically freezing array or object values.
Customizing signal equality checks
When it comes to object- or array-based signals, it's worth noting that the default equality check uses "===" (strict equality).
This check matters because a signal only emits a new value when it differs from the previous one.
If the incoming value is deemed equal to the existing one by this check, Angular will not emit a new value.
This serves as a performance optimization, avoiding unnecessary re-renders when the same value is emitted repeatedly.
However, the default "===" comparison checks for reference equality. This means it cannot recognize two distinct array or object instances as being functionally equivalent.
To handle such cases, we can override the equality function with a custom implementation.
Let's start with a basic example using the default equality check on an object signal.
We then create a derived signal from it:
@Component(
selector: "app",
template: `
<h3>Object title: {{title()}}</h3>
<button (click)="updateObject()">Update</button>
`)
export class AppComponent {
object = signal({
id: 1,
title: "Angular For Beginners"
});
title = computed(() => {
console.log(`Calling computed() function...`)
const course = this.object();
return course.title;
})
updateObject() {
// We are setting the signal with the exact same
// object to see if the derived title signal will
// be recalculated or not
this.object.set({
id: 1,
title: "Angular For Beginners"
});
}
}
If you were to click the Update button several times, you'd see repeated log output in the console:
Calling computed() function...
Calling computed() function...
Calling computed() function...
Calling computed() function...
etc.
The reason is that "===" can't tell that the new object value passed to the signal is functionally the same as the current one.
As a result, the signal treats them as different values, triggering a recalculation of any dependent computed signals.
To prevent this, we supply a custom equality function when creating the signal:
object = signal(
{
id: 1,
title: "Angular For Beginners",
},
{
equal: (a, b) => {
return a.id === b.id && a.title == b.title;
},
}
);
With this custom equality check, we're now doing a deep comparison based on the object's property values.
The derived signal will only compute once, regardless of how many times you click the Update button:
Calling computed() function...
It's worth noting that providing such custom equality functions is generally not recommended for most scenarios.
In typical applications, the default equality check works perfectly fine, and a custom check rarely makes a noticeable difference.
Implementing a manual equality check can introduce maintainability problems and subtle bugs — for instance, if you add a property to the object and forget to update the comparison logic.
We've included this topic for completeness, for those rare situations where you genuinely need it. For the vast majority of use cases, the default behavior is perfectly adequate.
Reacting to signal changes with the effect() API
The computed() API highlights one of signals' most appealing traits: the ability to detect and react to changes.
That's essentially what computed() does, doesn't it?
It perceives a change in a source signal and recalculates a derived value in response.
But what if our goal isn't to compute a new signal value, but simply to be notified of a change for some other purpose?
Suppose you need to detect when one or more signals change value in order to execute a side effect unrelated to other signals.
Common examples include:
- logging signal values using a logging library
- persisting signal data to localStorage or a cookie
- silently saving signal values to a database in the background
- and more
All these scenarios are made possible with the effect() API:
//The effect will be re-run whenever any
// of the signals that it uses changes value.
effect(() => {
// We just have to use the source signals
// somewhere inside this effect
const currentCount = this.counter();
const derivedCounter = this.derivedCounter();
console.log(`current values: ${currentCount}
${derivedCounter}`);
});
This effect logs a statement to the console every time either the counter or derivedCounter signal emits a new value.
It's important to note that the effect function runs at least once when it's first declared.
This initial execution is what establishes the effect's initial set of dependencies.
As with computed(), an effect's signal dependencies are re-evaluated dynamically on each subsequent run.
Signals and the change detection system
The connection here might be starting to become clear...
Signals give us a convenient way to monitor changes in application data.
Now imagine storing all of your application's data within signals.
First, it's worth noting that doing so wouldn't drastically complicate your application code.
The Signals API and its underlying principles are simple enough that a codebase built entirely around signals would remain highly readable.
That being said, it wouldn't be quite as straightforward as using plain JavaScript member variables for the same purposes.
So what's the real benefit then?
Why would one want to adopt a signal-centric approach for data management?
The payoff is that signals make it trivial to detect when application data changes, enabling automatic updates to any dependent pieces.
Now consider the scenario where Angular's change detection is directly integrated with your app's signals, and Angular is aware of which components and template expressions use each signal.
With that knowledge, Angular would know precisely which data changed and exactly which components and expressions need to be re-evaluated in response.
The necessity to scan and check the entire component tree — inherent to default change detection — would disappear entirely!
If we provide Angular with the assurance that all application data lives inside signals, Angular gains all the information it needs to implement the most efficient change detection and rendering strategy possible.
Angular would be able to update the view with the newest data in the most optimal manner.
And that, fundamentally, is the core performance advantage of using signals!
Merely encapsulating data in a signal allows Angular to deliver optimal performance in terms of DOM updates.
At this point, you should have a solid understanding of how signals work and why they're beneficial.
The next question is: how do we put them into practice effectively?
Automatic effect cleanup behavior
An effect is essentially a function that executes in response to signal value changes.
Like any function, it can capture and reference other variables in the application via a closure.
This implies that effects, much like any other function, carry a risk of causing accidental memory leaks.
To address this, Angular automatically handles the cleanup of effects based on their creation context.
For instance, an effect created inside a component will be automatically cleaned up when that component is destroyed. The same applies to effects created within directives, and so forth.
Manually managing effect lifecycle
There might be rare situations where you'd prefer to manage the cleanup of an effect yourself.
Such cases should be uncommon, though.
If you find yourself manually cleaning up effects throughout your entire application, something is probably amiss.
Nevertheless, when needed, an effect() can be explicitly destroyed by calling destroy on the EffectRef instance returned when the effect is first created.
In these situations, you'll likely want to disable the automatic cleanup by setting the manualCleanup option:
@Component({...})
export class CounterComponent {
count = signal(0);
constructor() {
const effectRef = effect(() => {
console.log(`current value: ${this.count()}`);
},
{
manualCleanup: true
});
// we can manually destroy the effect
// at any time
effectRef.destroy();
}
}
The manualCleanup flag turns off the default cleanup mechanism, giving you complete control over when the effect is terminated.
Calling effectRef.destroy() will instantly destroy the effect, preventing it from running in any future scheduled executions and clearing any external variable references, which helps prevent memory leaks.
Managing cleanup tasks when effects are torn down
Removing an effect function from memory alone may not always be enough to handle the full scope of cleanup work.
There are situations where a cleanup operation, such as terminating a network connection or releasing other held resources, should happen at the moment an effect is removed.
For these scenarios, an effect accepts an onCleanup callback:
@Component({...})
export class CounterComponent {
count = signal(0);
constructor() {
effect((onCleanup) => {
console.log(`current value: ${this.count()}`);
onCleanup(() => {
console.log("Perform cleanup action here");
});
});
}
}
This callback gets executed during the cleanup phase.
It provides a place to handle a variety of cleanup tasks, for instance:
- unsubscribing from an observable
- closing a connection to a network or database
- clearing timers chosen via setTimeout or setInterval
- and similar operations
Now we'll move on to some other signal-related ideas, followed by typical patterns you’ll likely find useful when working with signals.
Signals with read-only access
We've already worked with read-only signals, perhaps without realizing it.
These signals cannot have their value modified from the outside. In many ways, they mirror the behavior of a JavaScript const declaration.
Readonly signals can be accessed to read their value but can't be changed using the set or update methods. Read-only signals do not have any built-in mechanism that would prevent deep mutation of their value. - Angular repo
Two main sources allow you to obtain a read-only signal:
computed()signal.asReadonly()
Consider what occurs when we attempt to alter a derived signal's value:
@Component(
selector: "app",
template: `
<h3>Counter value {{counter()}}</h3>
<h3>Derived counter: {{derivedCounter()}}</h3>
`)
export class AppComponent {
counter = signal(0);
derivedCounter = computed(() => this.counter() * 10)
constructor() {
// this works as expected
this.counter.set(5);
// this throws a compilation error
this.derivedCounter.set(50);
}
}
Notice that, while we can update the counter signal—a standard writable one—attempts to change derivedCounter simply do not work.
The set() and update() methods are simply not available on it.
This reveals the read-only nature of the derivedCounter signal.
Creating a read-only signal from a writable one is straightforward:
@Component(
selector: "app",
template: `
<h3>Counter value {{counter()}}</h3>
`)
export class AppComponent {
counter = signal(0);
constructor() {
const readOnlyCounter = this.counter.asReadonly();
// this throws a compilation error
readOnlyCounter.set(5);
}
}
However, a writable signal cannot be derived from a read-only one; no API supports that direction. Instead, you would need to declare a fresh signal, likely using the read-only signal's current value.
Sharing a signal across several components
Let's shift the discussion to some common patterns for using signals throughout your app.
If a signal's scope is limited to just one component, turning it into a simple member variable is the best approach, as we have done.
But how do you handle a situation where the same signal data is required by different, distant locations in your application?
It is perfectly possible to create a single signal and reference it from many components.
Whenever that signal's value changes, every component relying on it will refresh.
// main.ts
import { signal } from "@angular/core";
export const count = signal(0);
As the example shows, placing the signal in its own file allows any component to import it.
To illustrate this, we can create two components that both leverage the count signal.
// app.component.ts
import { Component } from "@angular/core";
import { count } from "./main";
@Component({
selector: "app",
template: `
<div>
<p>Counter: {{ count() }}</p>
<button (click)="increment()">Increment from HundredIncrComponent</button>
</div>
`,
})
export class HundredIncrComponent {
count = count;
increment() {
this.count.update((value) => value + 100);
}
}
In this code, the count signal was imported and used. Any other component could import it the same way.
For some simpler use cases, this might be all that is required.
Still, for many apps, I believe this straightforward method won't cut it.
Think of it like exposing a global mutable variable in JavaScript.
Without any restriction, any part of the code could change it, or—in signal terms—publish a new value via the set() method.
Generally, this is something to avoid, much like you'd avoid a global mutable variable.
The goal should be to seal off direct access to the signal, ensuring that all modifications happen through a controlled interface.
Building reactive services using signals
The most straightforward approach for sharing a writable signal across components involves wrapping it in a service, as shown here:
@Injectable({
providedIn: "root",
})
export class CounterService {
// this is the private writeable signal
private counterSignal = signal(0);
// this is the public read-only signal
readonly counter = this.counterSignal.asReadonly();
constructor() {
// inject any dependencies you need here
}
// anyone needing to modify the signal
// needs to do so in a controlled way
incrementCounter() {
this.counterSignal.update((val) => val + 1);
}
}
Those familiar with Observable Data Services and BehaviorSubject with RxJs will see the similarity.
This pattern, though, tends to be more intuitive and involves fewer advanced ideas to grasp.
Notice how the writable counterSignal is deliberately kept private within the service. Others can read its current value through the read-only counter member.
To modify the counter, you must use the dedicated public method incrementCounter, which gives you a single, controlled entry point.
With this setup, you can easily incorporate any validations or business rules directly into that method. For instance, if a rule says the count can't exceed 100, you can enforce it in this one spot, no need to scatter the logic all over your app.
Maintenance becomes simpler as well.
Finding which parts of the app increment the counter is just an IDE search for incrementCounter away.
This kind of analysis would be impossible with a freely accessible signal, which exposes direct access.
By the way, any dependencies the signal requires can be injected through the service's constructor, like normal.
Encapsulation is the key principle at work, greatly improving maintainability.
Rather than letting any part of the app assign new values arbitrarily, we funnel those changes through a controlled path.
This pattern is far better than merely handing out a writable signal when data is needed in multiple components.
Working with OnPush change detection and signals
OnPush components normally only re-render when their input properties point to new references or when an async pipe receives a new value from an Observable. They won't update just because an input object's properties have been mutated.
The good news is that OnPush now works hand-in-hand with signals.
When a component uses a signal, Angular notes that dependency. Any subsequent update to that signal triggers a re-render of the component.
This applies to components using the OnPush strategy; they too will refresh when a signal they depend on gets a new value:
@Component({
selector: "counter",
template: `
<h1>Counter</h1>
<p>Count: {{ count() }}</p>
<button (click)="increment()">Increment</button>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CounterComponent {
count = signal(0);
increment() {
this.count.update((value) => value + 1);
}
}
In this mock-up, clicking the "Increment" button should cause the component to render anew. This demonstrates that signals and OnPush are wired together.
Such integration removes the necessity to inject ChangeDetectorRef and call its markForCheck method to refresh an OnPush component.
See what this looks like without signals:
@Component({
selector: "app",
standalone: true,
template: ` Number: {{ num }} `,
changeDetection: ChangeDetectionStrategy.OnPush,
})
class ExampleComponent {
num = 1;
private cdr = inject(ChangeDetectorRef);
ngOnInit() {
setInterval(() => {
this.num = this.num + 1;
this.cdr.markForCheck();
}, 1000);
}
}
That approach is noticeably more complex for achieving the same outcome. The signals version is significantly more straightforward.
Signals: usable beyond components, stores, and services?
Yes, absolutely! Signals aren't tied to any particular context. You can create them anywhere, at any time.
We showed that earlier in our examples. This flexibility is the beauty of Signals. Still, you'll typically want to enclose a signal within a service to manage access, as we discussed.
Signals vs. RxJs
Signals don't aim to take the place of RxJs in every possible way. But they do present a simpler, more straightforward option for certain tasks where RxJs was frequently necessary.
Consider a use case like propagating data changes across many parts of your app. For that, signals serve as a clear and easy substitute for an RxJS BehaviorSubject.
We appreciate you reading this piece. Should you wish to know when we release similar content, signing up for our newsletter is the way to go:
Subscribers also get the latest news on the Angular ecosystem.
For an intensive look at other Angular Core features like signals, recommend the Angular Core Deep Dive Course:
Summary
This guide has walked you through the Angular Signals API and the details of this fresh reactive primitive.
The core idea to take away is that by consolidating your app's state in signals, Angular can precisely determine which sections of the UI require updating.
Here are the main goals that Signals aim to accomplish:
- provide a reactive primitive that is simpler to grasp, making it easier to build your apps in a reactive fashion.
- cut down on needless component re-renders for views that aren't affected by the change.
- prevent wasted change-detection cycles for components whose data hasn't changed.
While the API is easy to use, you should be aware of some common missteps:
- when defining effects or computed signals, avoid reading source signals only inside conditional Blocks; this can lead to subtle bugs.
- steer clear of mutating the value of array or object signals directly.
- don't get carried away with custom equality functions or manual effect cleanup. Default behavior serves well in most situations.
Give Signals a try in your next project and see the ease for yourself!
