Helios uses a signals-based reactive system for state management.
signal()
Creates a reactive signal.
const count = signal(initialValue);
Initial value of the signal
A signal object with:
value: Current value (read/write)
peek(): Read value without subscribing
subscribe(): Subscribe to changes
Example
import { signal } from '@heliosvideo/core';
const count = signal(0);
// Read value (creates dependency if in effect/computed)
console.log(count.value); // 0
// Update value
count.value = 1;
// Read without creating dependency
const current = count.peek(); // 1
// Subscribe to changes
const unsubscribe = count.subscribe((value) => {
console.log('Count changed:', value);
});
// Cleanup
unsubscribe();
computed()
Creates a computed value that automatically updates when dependencies change.
const derived = computed(fn);
Function that computes the value (can access other signals)
A readonly signal that recomputes when dependencies change
Example
import { signal, computed } from '@heliosvideo/core';
const firstName = signal('John');
const lastName = signal('Doe');
const fullName = computed(() => {
return `${firstName.value} ${lastName.value}`;
});
console.log(fullName.value); // "John Doe"
firstName.value = 'Jane';
console.log(fullName.value); // "Jane Doe"
effect()
Creates a side effect that runs when dependencies change.
const dispose = effect(fn);
Function to run when dependencies change
Function to stop the effect and clean up
Example
import { signal, effect } from '@heliosvideo/core';
const count = signal(0);
const message = signal('');
const dispose = effect(() => {
// This runs immediately and whenever count changes
message.value = `Count is ${count.value}`;
console.log(message.value);
});
count.value = 1; // Logs: "Count is 1"
count.value = 2; // Logs: "Count is 2"
// Stop the effect
dispose();
count.value = 3; // No log (effect is disposed)
untracked()
Reads signals without creating dependencies.
const value = untracked(fn);
Function to run without tracking dependencies
Return value of the function
Example
import { signal, computed, untracked } from '@heliosvideo/core';
const a = signal(1);
const b = signal(2);
const sum = computed(() => {
// This computed only depends on 'a'
const aValue = a.value;
// Read 'b' without creating dependency
const bValue = untracked(() => b.value);
return aValue + bValue;
});
console.log(sum.value); // 3
a.value = 10;
console.log(sum.value); // 12 (recomputed)
b.value = 20;
console.log(sum.value); // 12 (NOT recomputed, no dependency on b)
Signal
Interface for a writable signal.
interface Signal<T> {
value: T;
peek(): T;
subscribe(fn: (value: T) => void): () => void;
}
value
Gets or sets the current value. Reading creates a dependency in effects/computed.
const current = signal.value; // Read
signal.value = newValue; // Write
peek()
Reads the current value without creating a dependency.
const current = signal.peek();
subscribe()
Subscribes to value changes.
const unsubscribe = signal.subscribe((value) => {
console.log('Changed:', value);
});
// Later...
unsubscribe();
ReadonlySignal
Interface for a readonly signal (like computed values).
interface ReadonlySignal<T> {
readonly value: T;
peek(): T;
subscribe(fn: (value: T) => void): () => void;
}
Complete example
import { signal, computed, effect, untracked } from '@heliosvideo/core';
// State
const frame = signal(0);
const fps = signal(30);
const playing = signal(false);
// Derived state
const time = computed(() => frame.value / fps.value);
const formattedTime = computed(() => {
const t = time.value;
const minutes = Math.floor(t / 60);
const seconds = Math.floor(t % 60);
return `${minutes}:${seconds.toString().padStart(2, '0')}`;
});
// Side effects
const disposeLogger = effect(() => {
if (playing.value) {
console.log(`Playing at frame ${frame.value} (${formattedTime.value})`);
}
});
// Animation loop
let animationId: number;
const disposePlayer = effect(() => {
if (playing.value) {
const tick = () => {
frame.value += 1;
animationId = requestAnimationFrame(tick);
};
animationId = requestAnimationFrame(tick);
} else {
cancelAnimationFrame(animationId);
}
});
// Start playback
playing.value = true;
// Stop after 3 seconds
setTimeout(() => {
playing.value = false;
disposeLogger();
disposePlayer();
}, 3000);
Hot vs cold computed
Computed values are:
- Hot: When they have active subscribers (actively listening to dependencies)
- Cold: When they have no subscribers (dependencies tracked but not subscribed)
This optimization reduces memory usage and unnecessary computations for unused computed values.