ComputedValue
ComputedValue<T> automatically updates when its dependencies change. It's derived from other StatePort instances.
Interface Definition
interface ComputedValue<T> extends StatePort<T> {
/**
* Dependencies of this computed value.
* Used by framework adapters to subscribe and recompute.
*/
dependencies: ReadonlyArray<StatePort<any>>;
// StatePort interface
get(): T;
set(value: T | ((prev: T) => T)): void; // Always throws (read-only)
subscribe?(callback: (value: T) => void): () => void;
}
Creating Computed Values
Use the createComputed function with a builder pattern:
function createComputed<Ds extends readonly StatePort<any>[]>(
...dependencies: Ds
): ComputedComputeBuilder<Ds>;
Builder Pattern
const computed = createComputed(dep1, dep2, ...)
.as((value1, value2, ...) => computedValue)
.for(owner);
Steps:
createComputed(...dependencies)- Declare dependencies.as(compute)- Define the computation function.for(owner)- Provide a Disposable owner for cleanup
Function Signature
createComputed<Ds extends readonly StatePort<any>[]>(
...dependencies: Ds
): {
as<T>(
compute: (...values: {
[K in keyof Ds]: ReturnType<Ds[K]["get"]>
}) => T
): {
for(owner: Disposable): ComputedValue<T>;
};
}
Type Parameters:
Ds- Tuple type of dependency StatePorts (inferred from arguments)
Parameters:
...dependencies: Ds- One or more StatePort dependencies
Returns: Builder object with .as() method
Computation Function
The .as() method accepts a function that receives dependency values and returns the computed value:
.as<T>(
compute: (...values: {
[K in keyof Ds]: ReturnType<Ds[K]["get"]>
}) => T
)
Type Parameters:
T- The type of the computed value (inferred from return type)
Parameters:
compute- Function that receives dependency values and returns computed value- Receives values in the same order as dependencies
- Type is inferred:
ReturnType<Ds[K]["get"]>for each dependency
Returns: Builder object with .for() method
Owner (Disposable)
The .for() method requires a Disposable owner for cleanup:
.for(owner: Disposable): ComputedValue<T>
Parameters:
owner: Disposable- Object with[Symbol.dispose]()method
Returns: ComputedValue<T> instance
Common Owners:
DisposableResourceclasses (usingthis)- Objects created with
makeDisposableObject - Custom objects implementing
Disposable
Methods
get(): T
Returns the current computed value. The computation function is called if dependencies have changed.
Returns: The current computed value of type T
Memoization: The result is memoized. The computation only runs when dependencies change (checked using Object.is()).
Example:
const a = new ReactiveValue(2);
const b = new ReactiveValue(3);
const sum = createComputed(a, b)
.as((x, y) => x + y)
.for(owner);
console.log(sum.get()); // 5 (computed: 2 + 3)
a.set(5);
console.log(sum.get()); // 8 (recomputed: 5 + 3)
console.log(sum.get()); // 8 (cached, no recomputation)
set(value: T | ((prev: T) => T)): void
Always throws an error. Computed values are read-only.
sum.set(10); // Error: "Computed values are read-only"
subscribe(callback: (value: T) => void): () => void
Subscribes to computed value changes. Returns an unsubscribe function.
Parameters:
callback: (value: T) => void- Function called when the computed value changes
Returns: An unsubscribe function
Behavior:
- Notifications are deduplicated using
queueMicrotask - If multiple dependencies change in the same tick, only one notification is sent
- The callback receives the latest computed value after all dependencies have updated
Example:
const a = new ReactiveValue(2);
const b = new ReactiveValue(3);
const sum = createComputed(a, b)
.as((x, y) => x + y)
.for(owner);
let notificationCount = 0;
sum.subscribe?.((value) => {
notificationCount++;
console.log('Sum:', value);
});
// Change both dependencies in the same tick
a.set(10);
b.set(20);
// After microtask: notificationCount is 1 (deduplicated)
// Logs: "Sum: 30" (latest value after both changes)
dependencies: ReadonlyArray<StatePort<any>>
Read-only array of dependency StatePorts. Framework adapters use this to subscribe to dependencies.
Example:
const computed = createComputed(a, b, c)
.as((x, y, z) => x + y + z)
.for(owner);
console.log(computed.dependencies); // [a, b, c]
Memoization
Computed values are memoized by default:
- The computation function only runs when dependencies change
- Dependencies are compared using
Object.is() - The cached result is returned if dependencies haven't changed
Example:
let computationCount = 0;
const computed = createComputed(a, b)
.as((x, y) => {
computationCount++;
return x + y;
})
.for(owner);
computed.get(); // computationCount = 1
computed.get(); // computationCount = 1 (cached)
a.set(5); // dependency changed
computed.get(); // computationCount = 2 (recomputed)
computed.get(); // computationCount = 2 (cached)
Notification Deduplication
When multiple dependencies change in the same execution context, notifications are deduplicated:
- Uses
queueMicrotaskto batch notifications - Only one notification is sent per subscriber, even if multiple dependencies change
- The callback receives the latest computed value after all changes
Example:
const a = new ReactiveValue(1);
const b = new ReactiveValue(2);
const sum = createComputed(a, b)
.as((x, y) => x + y)
.for(owner);
let callCount = 0;
sum.subscribe?.(() => {
callCount++;
});
// Multiple dependency changes in the same tick
a.set(10);
b.set(20);
a.set(15);
// After microtask: callCount is 1 (deduplicated)
// Callback receives sum = 15 + 20 = 35
Examples
Simple Sum
import { ReactiveValue, createComputed, DisposableResource } from '@xndrjs/core';
class Calculator extends DisposableResource {
private a = new ReactiveValue(2);
private b = new ReactiveValue(3);
public sum = createComputed(this.a, this.b)
.as((x, y) => x + y)
.for(this);
setA(value: number) {
this.a.set(value);
}
setB(value: number) {
this.b.set(value);
}
}
const calc = new Calculator();
console.log(calc.sum.get()); // 5
calc.setA(10);
console.log(calc.sum.get()); // 13
Nested Computed Values
const a = new ReactiveValue(2);
const b = new ReactiveValue(3);
const sum = createComputed(a, b)
.as((x, y) => x + y)
.for(owner);
const doubled = createComputed(sum)
.as((s) => s * 2)
.for(owner);
console.log(doubled.get()); // 10 ((2 + 3) * 2)
a.set(5);
console.log(doubled.get()); // 16 ((5 + 3) * 2)
Conditional Logic
const count = new ReactiveValue(0);
const threshold = new ReactiveValue(100);
const status = createComputed(count, threshold)
.as((c, t) => c > t ? 'high' : 'low')
.for(owner);
console.log(status.get()); // 'low'
count.set(150);
console.log(status.get()); // 'high'
Framework Integration
Computed values work seamlessly with framework adapters:
React:
import { useReactiveValue } from '@xndrjs/adapter-react';
const sum = createComputed(a, b)
.as((x, y) => x + y)
.for(owner);
function SumDisplay() {
const value = useReactiveValue(sum);
return <div>Sum: {value}</div>;
}
Solid:
import { useReactiveValue } from '@xndrjs/adapter-solid';
function SumDisplay() {
const value = useReactiveValue(sum);
return <div>Sum: {value()}</div>;
}
Svelte:
<script>
import { reactiveValue } from '@xndrjs/adapter-svelte';
const sumStore = reactiveValue(() => sum);
</script>
<div>Sum: {$sumStore}</div>
Next Steps
- Learn about Lifecycle Management - cleanup and disposal
- Explore ReactiveValue - the simplest reactive primitive
- Check out Framework Adapters for UI integration