- Published on
Angular Signals
- Authors
- Name
- Iraianbu A
Table of Contents
- Introduction
- Characteristics
- Types of Signals
- Writable Signals
- Read-only Signals
- Computed Signals
- Key Benefits
- Effects
- References
Introduction
A signal is a essentially a wrapper around a value that notifies consumers (functions,components or services) when the value changes. This allows for a reactive data flow, where components and services can subscribe to signals and react to changes in the value.
Signals in Angular are a way to manage state and reactive data flows, similar to Observables or BehaviorSubjects. Signals are more lightweight and built directly into the framework, making them easier to use and understand.
Characteristics
- Hold any type of value (Primitive, Object, Array, etc.)
- Exposes a getter function to read the current value.
- Notifies Angular when their value is used, allowing Angular to track dependencies automatically.
Types of Signals
- Writable Signals (Can be updated)
- Read-only Signals (Cannot be updated)
Writable Signals
const count = signal<number>(0)
count.set(1)
count.update((count) => count + 1)
set
- Sets the value of the signal directlyupdate
- Updates the value of the signal using a callback function
Read-only Signals
const count: Signal<number> = signal<number>(0)
console.log(count())
Computed Signals
Computed signals are read-only signals that are derived from other signals.
Computed signals are useful when you need to derive a value from another signal and you want to update the derived value when the original signal changes.
Lazy Evaluation (Computed signals are only evaluated when the value is used)
Memoization (Computed signals are memoized and only re-evaluate when the dependencies change)
Dynamic Computed Signals
const showCount = signal(false)
const count = signal(0)
const conditionalCount = computed(() => {
if (showCount()) {
return `The count is ${count()}.`
} else {
return 'Nothing to see here!'
}
})
When showCount
is false, conditionalCount
returns "Nothing to see here!" without reading count
. Updating count
won't trigger recomputation.
When showCount
is true, conditionalCount
reads count and shows its value. Now updating count will trigger recomputation.
Dependencies can be added or removed dynamically. Setting showCount
back to false removes count
as a dependency.
OnPush
Components
Reading Signals in @Component({
selector: 'app-my-component',
template: `{{ count() }}`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
In OnPush components, Angular automatically tracks signals used in templates and updates the component when signal values change.
Usecases
- Expensive Calculations
- Data Transformation
- Read-only data
Key Benefits
Simple State Management (Provides an easy way to manage state rather than using Observables and Subscriptions)
Reactive Data Flow (Angular automatically tracks dependencies and updates components when the signal value changes)
Type Safety (Signals are typed and provide type safety)
Effects
Effects allows us to perform side effects like API calls, logging, etc. when one or more signals change.
It is a function that run whenever one or more signals change. When we create an effect, Angular tracks the signals used within that effect and re-runs the effect when the signals change.
const count = signal(0)
effect(() => {
console.log(count())
})
Working
Initial execution : Effects always run once when the component is created.
Tracking dependencies : Effects track the signals used within them.
Asynchronous execution : Effects execute asynchronously during Angular's change detection cycle.
Usecases
- Logging data
- Keeping in sync with
window.localStorage
- Adding Custom DOM behavior
effects
when
Avoid Avoid using effect
for state propagation (i.e. when a signal is used to update another signal). This can result in infinte circular dependencies, ExpressionChangedAfterItHasBeenChecked
.
const count = signal(0)
const doubleCount = signal(1)
effect(() => {
doubleCount.set(count() * 2)
})
Injection Context
We can create an effect()
within an injection context like constructor
or inject
.
@Component({...})
export class SignalComponent {
count = signal(0);
constructor() {
effect(() => {
console.log(this.count());
});
}
}
We can assign the effect
to a field.
@Component({...})
export class SignalComponent {
count = signal(0);
private loggingEffect = effect(() => {
console.log(this.count());
});
}
To create an effect outside the constructor, we can pass an Injector
to the effect
function.
@Component({...})
export class EffectiveCounterComponent {
readonly count = signal(0);
private injector = inject(Injector);
initializeLogging(): void {
effect(() => {
console.log(`The count is: ${this.count()}`);
}, {injector: this.injector});
}
}
Destroying Effects
When we create an effect
, it's automatically destroyed when the component is destroyed. Effect return an EffectRef
object which has a destroy
method. We can also manually destroy an effect by passing {manualCleanup: true}
to the effect
function.
@Component({...})
export class SignalComponent {
count = signal(0);
private loggingEffect = effect(() => {
console.log(this.count());
} , {manualCleanup: true});
ngOnDestroy(): void {
this.loggingEffect.destroy();
}
}