Skip to main content
Helios uses a signals-based reactive system for state management.

signal()

Creates a reactive signal.
const count = signal(initialValue);
initialValue
T
required
Initial value of the signal
signal
Signal<T>
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);
fn
() => T
required
Function that computes the value (can access other signals)
computed
ReadonlySignal<T>
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);
fn
() => void
required
Function to run when dependencies change
dispose
() => void
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);
fn
() => T
required
Function to run without tracking dependencies
value
T
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.