Computed
Computed signals create derived values, based on one or more dependency signal values. The derived value is updated in response to changes in the dependency signal values. Computed values are not updated if there was no update to the dependent signals.
Example:
const counter = signal(0);
// creating a computed signal
const isEven = computed(() => counter() % 2 === 0);
// computed properties are signals themselves
const color = computed(() => isEven() ? 'red' : 'blue');
The signature of the computed is:
function computed<GValue>(
computation: IComputationFunction<GValue>,
options?: ICreateComputedOptions<GValue>,
): IReadonlySignal<GValue>
interface IComputationFunction<GValue> {
(): GValue;
}
interface ICreateComputedOptions<GValue> extends ICreateSignalOptions<GValue> {}
The computation function is expected to be side-effect free: it should only access values of the dependent signals (and / or other values being part of the computation) and avoid any mutation operations. In particular, the computation function must not write to other signals
The framework is able to detect loops and signal writes. In such cases, it will report them as errors.
Similarly to the writable signals, computed signals can (optionally) specify the equality function. When provided, the equality function can stop recomputation of the deeper dependency chain if two values are determined to be equal. Example (with the default equality):
const counter = signal(0);
// creating a computed signal
const isEven = computed(() => counter() % 2 === 0);
// computed properties are signals themselves
const color = computed(() => isEven() ? 'red' : 'blue');
// providing a different, even value, to the counter signal means that:
// - isEven must be recomputed (its dependency changed)
// - color don't need to be recomputed (isEven() value stays the same)
counter.set(2);
The algorithm chosen to implement the computed functionality makes strong guarantees about the timing and correctness of computations:
- Computations are lazy: the computation function is not invoked, unless someone is interested in (reads) its value.
- Computations are disposed of automatically: as soon as the computed signal reference is out of scope it is automatically eligible for garbage collection. No explicit cleanup boundaries and / or operations are exposed by the library.
- Computations are glitch-free: the computation function is executed only once in response to dependencies change, and reading synchronously its value won't generate recomputation nor glitches.
Branching in Computations
Computed signals keep track of which signals were read in their computations, in order to know when recomputation is necessary. This dependency set is dynamic, and self-adjusts with each computation. So in the conditional computation:
const greeting = computed(() => showName() ? `Hello, ${name()}!` : 'Hello!');
The greeting
will always be recomputed if the showName
signal changes, but if showName
is false,
the name
signal is not a dependency of the greeting
and will not cause it to recompute.
Errors in Computations
Errors in computed signals are permitted. In such case, the computed signal will enter an "error" state, eventually cascading it to its dependents.
// creates an invalid date
const date = signal(new Date(Number.isNaN));
// creating a computed signal that throw is the date is invalid
const hours = computed(() => {
if (Number.isNaN(date().getTime())) {
throw new Error(`Invalid date`);
} else {
return date().getHours();
}
});
const logHours = () => {
try {
console.log(`${hours()}h`);
} catch (error) {
console.log(error.message);
}
};
logHours(); // logs: Invalid date
date.set(new Date(2023, 1, 10, 8, 42));
logHours(); // logs: 8