Skip to main content

Signal

Fundamentals

A Signal is simply a function returning immediately its value (as opposed to an Observable whose value is only async):

interface IReadonlySignal<GValue> {
(): GValue;

[SIGNAL]: unknown;
}

This function is marked with the SIGNAL symbol so the framework can recognize signals and apply internal optimizations.

Signals are fundamentally read-only: we can ask for the current value and observe change notification.

Read a signal

To read a signal, we simply have to call it like a function:

const value = count();

This function is used to access the current value and record signal read in a reactive context - this is an essential operation that builds the reactive dependencies graph.

Signal reads outside of the reactive context are permitted. This means that non-reactive code (ex.: existing, 3rd party libraries) can always read the signal's value, without being aware of its reactive nature.

Writable signals

When creating a Signal with the signal function (explained later), you'll receive a writable signal:

interface ISignal<GValue> extends IReadonlySignal<GValue> {
set(value: GValue): void;

update(updateFunction: ISignalUpdateFunctionCallback<GValue>): void;

asReadonly(): IReadonlySignal<GValue>;
}

set(...)

This method directly sets the Signal to a new value, and notifies any dependents.

This is useful for changing primitive values or replacing data structures when the new value is independent of the old one.

update(...)

This method updates the value of the Signal based on its current value, and notifies any dependents.

Useful for setting a new value that depends on the old value, such as updating an immutable data structure.

It accepts an updateFunction:

interface ISignalUpdateFunctionCallback<GValue> {
(
value: GValue,
): GValue;
}

update is simply a shorter and convenient manner to combine a signal read and write in one call (count.update(c => c + 1) is similar to count.set(count() + 1)).

Create a Signal

An instance of a writable Signal can be created using the signal creation function:

function signal<GValue>(
initialValue: GValue,
options?: ICreateSignalOptions<GValue>,
): ISignal<GValue>

initialValue is used to set, as its name suggests, the signal's initial value. Because signals are sync, this argument must be provided.

Then, it's possible to provide an optional ICreateSignalOptions argument:

interface ICreateSignalOptions<GValue> {
readonly equal?: IEqualFunction<GValue>;
}

interface IEqualFunction<GValue> {
(
a: GValue,
b: GValue,
): boolean;
}
  • equal: this function is used to compare values passed to the set method. If this equality function determines that 2 values are equal, then the value is simply ignored, and the change propagation is skipped.
  • EQUAL_FUNCTION_STRICT_EQUAL: (default) - compares values using the strict equality ===.
  • EQUAL_FUNCTION_NON_PRIMITIVES_ALWAYS_FALSE: compares primitive values (numbers, strings, etc) using === semantics but treats objects and arrays as "always unequal". This allows Signals to hold non-primitive values (objects, arrays) and still propagate change notification.

Examples

Create a signal and set its value

// create a writable signal
const counter = signal(0);

// set a new signal value, completely replacing the current one
counter.set(5);

// update signal's value based on the current one
counter.update(currentValue => currentValue + 1);

Create a signal with an immutable value

const names = signal<readonly string[]>(['Alice']);

// "update" is convenient to modify immutable data structures
names.update(names => ([
...names,
'Bob',
]));

Create a signal with a mutable array as value

// note that we MUST use a different `equal` function if we want to be able to mutate non-primitive values. Else the signal won't update.
const names = signal<string[]>(['Alice'], { equal: EQUAL_FUNCTION_NON_PRIMITIVES_ALWAYS_FALSE });

names.update(names => {
names.push('Bob');
return names;
});