Reactive programming has become a central paradigm in modern frontend development, with frameworks like Angular relying heavily on observables, event emitters and state management patterns. But with the introduction of Signals, Angular has unlocked a simpler, more intuitive way to manage local state reactively.
In this article, we’ll take you from the basics to advanced use cases of Signals in Angular, explaining how they work, why they’re different from observables and how you can use them to build high-performance Angular applications.
What are Angular Signals?
Angular Signals are reactive values that update automatically when their dependencies change. Think of them as reactive variables: they’re lightweight, synchronous and easy to work with, making them ideal for managing local component state. Unlike observables, which are typically used to handle streams of asynchronous data, signals are synchronous and work perfectly with Angular’s change detection.
In Angular, a signal represents a value that can change over time, and other signals can depend on it. This creates a natural flow of reactive state management within your application, where updates are propagated automatically.
Example:
import { signal } from '@angular/core';
// Define a signal
const count = signal(0);
// Update signal value
count.set(1);
// Read the signal value
console.log(count()); // Outputs: 1
How Signals Differ from Observables
Although both observables and signals are reactive programming constructs, they differ significantly in their purpose and behavior.
Feature | Signals | Observables |
---|---|---|
Push vs Pull | Pull-based (get when needed) | Push-based (values are emitted) |
Synchronous | Synchronous and immediate | Can be asynchronous |
Subscriptions | Not required | Required |
Use Case | Local state | Streams, async operations |
While observables are ideal for streams of events (e.g., HTTP requests, WebSocket messages), signals shine when dealing with local, synchronous state that needs to be accessed and updated within your Angular components.
Setting Up Signals in Angular
To start using signals in Angular, make sure you are working with Angular 16 or above, as this is when they became fully integrated into Angular Core. Below is a basic example of how to use signals in an Angular component:
import { Component, signal } from '@angular/core';
@Component({
selector: 'app-counter',
template: `
<button (click)="increment()">Increment</button>
<p>Count: {{ count() }}</p>
`
})
export class CounterComponent {
count = signal(0);
increment() {
this.count.set(this.count() + 1);
}
}
Advanced Concepts: Derived and Computed Signals
Angular allows you to create derived signals (signals that depend on other signals) which automatically update when their dependencies change. This makes it easy to handle complex state dependencies.
Derived Signals
Derived signals are perfect for managing calculated values that depend on other signals.
Here’s a simple example of creating a derived signal:
const baseCount = signal(10);
const doubleCount = computed(() => baseCount() * 2);
console.log(doubleCount()); // Outputs: 20
In this example, doubleCount
will always return twice the value of baseCount
. If baseCount
changes, doubleCount
will update automatically.
Computed Signals
You can also create computed signals, which act similarly to derived signals but are used when you need to perform more complex calculations.
const a = signal(5);
const b = signal(3);
const sum = computed(() => a() + b());
console.log(sum()); // Outputs: 8
Whenever a
or b
changes, the sum
signal will recalculate automatically.
Integrating Signals with Angular Change Detection
One of the key advantages of signals is their deep integration with Angular’s change detection mechanism. Signals trigger updates to the view automatically, even when using OnPush change detection. This significantly improves performance without the need for manual change detection calls.
Example with OnPush:
@Component({
selector: 'app-signal-example',
template: `<p>{{ fullName() }}</p>`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class SignalExampleComponent {
firstName = signal('Soumaya');
lastName = signal('Erradi');
fullName = computed(() => `${this.firstName()} ${this.lastName()}`);
}
Even with OnPush, the view will update whenever firstName
or lastName
changes because of the reactive nature of signals.
Signals in Complex Applications
For more complex scenarios, such as managing the state of an entire application, signals can simplify state management by providing clear and declarative dependencies between different parts of the state. For instance, in a shopping cart application, you can use signals to manage cart items and automatically calculate the total price based on the items added.
Example: Shopping Cart with Signals
@Injectable({ providedIn: 'root' })
export class CartService {
private items = signal<CartItem[]>([]);
addItem(item: CartItem) {
this.items.set([...this.items(), item]);
}
getTotalPrice = computed(() =>
this.items().reduce((total, item) => total + item.price, 0)
);
}
The getTotalPrice
signal will automatically update whenever items are added to or removed from the cart.
Best Practices for Signals
When using signals in Angular applications, follow these best practices for optimal performance and maintainability:
- Use signals for local, synchronous state: Signals are ideal for managing component state or any synchronous data dependencies within a component.
- Keep derived signals simple: While derived signals are useful, keep your dependencies minimal to avoid overly complex signal chains that are difficult to debug.
- Leverage automatic updates: Don’t overcomplicate your signal-based components by manually tracking state updates. Let Angular handle it for you.
- Combine Signals and Observables: In some cases, you might still need observables for managing asynchronous data. In such cases, use signals for synchronous state and observables for streams.
Conclusion
Angular Signals introduce a fresh approach to managing reactive state in Angular applications. They offer a lightweight, synchronous and declarative way to manage state, making your applications easier to reason about and maintain. As you build out more complex applications, signals can reduce the need for boilerplate code, eliminate the need for manual subscriptions and streamline your change detection.
With this foundational knowledge, you're ready to start building more reactive and performant Angular applications using signals.
Happy coding!
Top comments (6)
Easy to understand 😀, thanks for writing one 🙏
But the computed signals use computed method as in docs is it 🤔
const count: WritableSignal<number> = signal(0);
const doubleCount: Signal<number> = computed(() => count() * 2);
Using computed like this is key for creating signals that depend on other signals, allowing Angular to handle dependencies and updates automatically.
Thanks, apologies but to me I feel that this post seems to be articulated by AI ... well it is perfectly alright but hopefully after reviewed by you or any human before 🚀😊
The step-by-step guide from basics to advanced use cases is incredibly helpful.
You should also talk about how to use signals along with effect()
The effect function is used with signals to trigger side effects based on changes in signal values.
Great article , I’ve been using signals for awhile now and they are awesome!