ReactiveValue
ReactiveValue<T> is the simplest implementation of StatePort<T>. It holds a single primitive value and notifies subscribers when the value changes.
Class Definition
class ReactiveValue<T> extends AbstractReactiveValue<T, T> {
constructor(initialValue: T);
// StatePort interface
get(): T;
set(value: T | ((prev: T) => T)): void;
subscribe(callback: (value: T) => void): () => void;
// Additional methods
notify(): void;
}
Constructor
new ReactiveValue<T>(initialValue: T)
Creates a new ReactiveValue instance.
Type Parameter:
T- The type of the value (must be a primitive: string, number, boolean, null, undefined)
Parameters:
initialValue: T- The initial value
Runtime Validation:
- Throws an error if
initialValueis an array (useReactiveArrayinstead) - Throws an error if
initialValueis an object (useReactiveObjectinstead)
Example:
const count = new ReactiveValue(0);
const name = new ReactiveValue('Alice');
const isActive = new ReactiveValue(true);
const nullable = new ReactiveValue<string | null>(null);
Methods
get(): T
Returns the current value.
Returns: The current value of type T
Example:
const count = new ReactiveValue(10);
const current = count.get(); // 10
set(value: T | ((prev: T) => T)): void
Updates the value. Accepts either a direct value or an updater function.
Parameters:
value: T | ((prev: T) => T)- The new value or a function that receives the previous value and returns the new value
Behavior:
- Uses
Object.is()for comparison (only notifies subscribers if value actually changed) - Notifications are batched using
BatchContext - Subscribers are called asynchronously in the next microtask
Example:
const count = new ReactiveValue(0);
// Direct value
count.set(10);
// Updater function
count.set((prev) => prev + 1);
count.set((prev) => prev * 2);
// Conditional update
count.set((prev) => prev > 100 ? 0 : prev + 1);
subscribe(callback: (value: T) => void): () => void
Subscribes to value changes. Returns an unsubscribe function.
Parameters:
callback: (value: T) => void- Function called when the value changes
Returns: An unsubscribe function that removes the subscription
Behavior:
- The callback is called immediately with the current value when subscribing
- Callbacks are called asynchronously (batched via
BatchContext) - Errors in callbacks are caught and logged to console.error
Example:
const count = new ReactiveValue(0);
const unsubscribe = count.subscribe((value) => {
console.log('Count changed to:', value);
});
count.set(10); // Logs: "Count changed to: 10"
count.set(20); // Logs: "Count changed to: 20"
// Unsubscribe when done
unsubscribe();
count.set(30); // No log (unsubscribed)
notify(): void
Manually trigger notifications to all subscribers. Useful when you've made direct mutations (not recommended).
Example:
const count = new ReactiveValue(0);
// ... direct mutation (not recommended)
count.notify(); // Manually notify subscribers
Supported Types
ReactiveValue is designed for primitive values only:
stringnumberbooleannullundefinedsymbolbigint
For complex data structures, use:
ReactiveObjectfor plain objectsReactiveArrayfor arraysReactiveSetfor SetsReactiveMapfor Maps
Comparison Behavior
Values are compared using Object.is():
NaN === NaNisfalse, butObject.is(NaN, NaN)istrue0 === -0istrue, butObject.is(0, -0)isfalse- Subscribers are only notified if the value actually changed
Batching
Notifications can be batched using the batched() function. Multiple set() calls inside a batched() callback result in a single notification per subscriber. Please note that some frameworks may already have their own logics for batching (i.e. React v18+), or may operate with fine-grained UI updates (Solid, Svelte), leading to different re-render counts anyway
Example:
import { batched } from '@xndrjs/core';
const count = new ReactiveValue(0);
let callCount = 0;
count.subscribe(() => {
callCount++;
});
// Without batching: each set() triggers a notification immediately
count.set(1);
count.set(2);
count.set(3);
// callCount is 3 (one notification per set)
// With batching: all sets are batched together
batched(() => {
count.set(1);
count.set(2);
count.set(3);
});
// callCount is 4 (3 from above + 1 batched notification)
Note: If you don't use batched(), each set() call triggers notifications immediately.
Example: Counter
import { ReactiveValue } from '@xndrjs/core';
class Counter {
private count = new ReactiveValue(0);
get value() {
return this.count.get();
}
increment() {
this.count.set((prev) => prev + 1);
}
decrement() {
this.count.set((prev) => prev - 1);
}
reset() {
this.count.set(0);
}
subscribe(callback: (value: number) => void) {
return this.count.subscribe(callback);
}
}
const counter = new Counter();
counter.subscribe((value) => {
console.log('Counter:', value);
});
counter.increment(); // Logs: "Counter: 1"
counter.increment(); // Logs: "Counter: 2"
counter.reset(); // Logs: "Counter: 0"
Framework Integration
Use framework adapters to connect ReactiveValue to your UI:
React:
import { useReactiveValue } from '@xndrjs/adapter-react';
const count = new ReactiveValue(0);
function Counter() {
const value = useReactiveValue(count);
return <div>{value}</div>;
}
Solid:
import { useReactiveValue } from '@xndrjs/adapter-solid';
const count = new ReactiveValue(0);
function Counter() {
const value = useReactiveValue(count);
return <div>{value()}</div>; // Solid accessor
}
Svelte:
<script>
import { reactiveValue } from '@xndrjs/adapter-svelte';
const count = new ReactiveValue(0);
const countStore = reactiveValue(() => count);
</script>
<div>{$countStore}</div>
Next Steps
- Learn about ComputedValue - derived reactive values
- Explore StatePort - the foundation interface
- Check out Framework Adapters for UI integration