Signals

About

guide/signals

A signal is a wrapper around a value that notifies interested consumers when that value changes. (Values may be primitive or complex data structures)

Signals gives Angular a new way of handling data changes and updating the UI, instead of just Change Detection.

You read a signal's value by calling its getter function, which allows Angular to track where the signal is used.

Best Practices

  • Avoid using effect() for propagation of state changes.

    • This might lead to infinite circular updates, or unnecessary change detection cycles.

    • Angular will forbid you from change signal states inside effect().

  • Don't mutate signal values when updating them, always give them new values.

Writable Signals

signal()

Can be created with signal(value).

You may update their values with:

  • set(newValue) to just set a new value.

  • update((oldValue) => newValue), to access the old value.

<!-- To Output a signal, just execute it's getter -->
<p>{{ counter() }}</p>
@Component({ ... })
export class Component {
    counter: WritableSignal<number> = signal(0);
    // Or
    counter = signal<number>(0);

    actions = signal<string[]>([]);

    show() {
        console.log(this.counter());
    }

    increment() {
        this.counter.update((oldValue: number) => oldValue + 1);

        // NEVER mutate Signal values, instead always give it a new value
        this.actions.update((oldActions) => [...oldActions, 'new Value']);
    }

    decrement() {
        this.counter.update((oldValue: number) => oldValue - 1);
    }
}

Computed Signals

computed()

Are readonly signals that derive their value from other signals.

Created with computed(() => this.otherSignal() + 1).

Computed signals depend on other signals, so computed signals are only updated when their dependee signals update.

Lazy and Memoized

Dynamic dependencies

const showCount = signal(false);
const count = signal(0);
const conditionalCount = computed(() => {
    if (showCount()) {
        return `The count is ${count()}.`;
    } else {
        return "Nothing to see here!";
    }
});

In this example:

  • When showCount is false, updating count will not trigger a recomputation of the computed signal.

    • So only showCount change will trigger.

  • But when showCount is true, count signal can be read, and now the computed signal will recalculate itself on both showCount and count updates.

  • Just remeber that the computed signal won't recalculate immediatly after the dependee signals update, but only when conditionalCount is read again.

Effects

effect()

Effects are operations that runs whenever on or more signal values change.

  • They also have dependee signals, which are the signals used inside them.

  • When these dependee signals values change, the effect re-runs.

Create them with effect(() => {}).

Effect always run at least once.

Effects also keep track of their dependencies dynamically.

So they will only track signals that were read during the last execution.

Effects always execute asynchronously, during the change detection process.

Effects are good for:

  • Logging data.

  • Keeping data in sync with window.localStorage.

  • Performing custom rendering to a <canvas>, charting library, or other third party UI library.

Injection Context

By deafult, you can only create effect() within an Injection Context.

Effects are destroyed automatically when their Components, Directives, Services, etc are destroyed.

export class Component {
    constructor() {
        effect(() => { ... });
    }
}

You can create effect outside of a constructor or assign specific names to it by:

export class Component {
    // Give a descriptive name to the effect
    private cEffect = effect(() => { ... });
    
    // To create an Effect outside of the constructor, get the Injector dependency
    constructor(private injector: Injector) {}
    
    log() {
        effect(() => { ... }, { injector: this.injector });
    }
}

Avoid signal tracking

To avoid re-run of effects when signal values are updated, wrap the signals with untracked().

export class Component {
    counter = signal(0);
    constructor() {
        effect(() => {
            console.log(`Counter is ${ untracked(counter) }`);
        });
    }
}

untracked is also useful when an effect needs to invoke some external code which shouldn't be treated as a dependency.

effect(() => {
    const user = currentUser();
    untracked(() => {
        // If the `loggingService` reads signals, they won't be counted as dependencies of this effect.
        this.loggingService.log(`User set to ${user}`);
    });
});

Effect cleanup functions

Effect might start long-running operations, which you should cancel if the effect is destroyed or runs again before the first operation finished.

When creating effects, you can optionally pass an onCleanup function as its first parameter. And this onCleanup accepts a callback function that is invoked before the next run of the effect begins, or when the effect is destroyed.

effect((onCleanup) => {
    const user = currentUser();
    const timer = setTimeout(() => {
        console.log(`1 second ago, the user became ${user}`);
    }, 1000);
    onCleanup(() => {
        clearTimeout(timer);
    });
});

Last updated