Skip to main content

Overview

Kraken TUI uses @preact/signals-core for fine-grained reactivity. Signal values are automatically tracked when accessed inside effect() or computed(), and components re-execute only the minimal set of effects when dependencies change. All signal APIs are re-exported from kraken-tui for convenience.

signal()

Create a mutable reactive signal.
initialValue
T
required
The initial value of the signal.
Returns: Signal<T> — A reactive container with a .value property.
import { signal, render } from "kraken-tui";

const count = signal(0);

render(
  <Box flexDirection="column" padding={2}>
    <Text content={count} />
    <Text content="Press 'i' to increment" />
  </Box>,
  app
);

// In your event loop:
if (event.keyCode === KeyCode.I) {
  count.value += 1;
}

Signal Access

  • Read: signal.value
  • Write: signal.value = newValue
  • Assignments trigger effects and update bound props

computed()

Create a read-only derived signal that automatically recomputes when dependencies change.
fn
() => T
required
A function that computes the derived value. Dependencies are tracked automatically.
Returns: ReadonlySignal<T> — A reactive container with a read-only .value property.
import { signal, computed, render } from "kraken-tui";

const count = signal(0);
const doubled = computed(() => count.value * 2);

render(
  <Box flexDirection="column">
    <Text content={count} />
    <Text content={doubled} />
  </Box>,
  app
);

count.value = 5; // doubled.value is now 10
computed() signals are lazy — they only recompute when accessed. If no effect or component reads the value, the computation is skipped.

effect()

Run a side effect when dependencies change. The effect function is called immediately and re-runs whenever accessed signals change.
fn
() => void
required
The effect function. Dependencies are tracked automatically.
Returns: () => void — A cleanup function to stop the effect.
import { signal, effect } from "kraken-tui";

const count = signal(0);

const dispose = effect(() => {
  console.log(`Count changed to: ${count.value}`);
});

// Later: stop the effect
dispose();

Automatic Effect Binding (JSX)

When you pass a signal to a JSX prop, the reconciler automatically creates an effect:
// This:
<Text content={count} />

// Is equivalent to:
mount(vnode, parent) {
  const dispose = effect(() => {
    applyStaticProp(handle, "content", count.value);
  });
  instance.cleanups.push(dispose);
}
Effects are automatically disposed on unmount.

batch()

Group multiple signal updates into a single reactive update cycle.
fn
() => void
required
A function that updates signals. Effects are deferred until the batch completes.
Returns: void
import { signal, effect } from "kraken-tui";

const x = signal(0);
const y = signal(0);

effect(() => {
  console.log(`Position: ${x.value}, ${y.value}`);
});

x.value = 10; // "Position: 10, 0"
y.value = 20; // "Position: 10, 20"
x.value = 30; // "Position: 30, 20"
Use batch() to optimize multi-signal updates, especially when updating layout props together (width + height, margin + padding, etc.).

Type Signatures

Signal<T>

interface Signal<T> {
  value: T;
}

ReadonlySignal<T>

interface ReadonlySignal<T> {
  readonly value: T;
}

MaybeSignal<T>

Kraken’s JSX props accept either static values or signals:
type MaybeSignal<T> = T | Signal<T>;

interface TextProps {
  content?: MaybeSignal<string>;
  fg?: MaybeSignal<string | number>;
  bold?: MaybeSignal<boolean>;
  // ...
}

Signal Detection

The reconciler detects signals using the Symbol.for("preact-signals") brand:
function isSignal(value: unknown): value is { readonly value: unknown } {
  return (
    value != null &&
    typeof value === "object" &&
    "brand" in value &&
    (value as { brand: unknown }).brand === Symbol.for("preact-signals")
  );
}
This is a public contract from @preact/signals-core, not an internal implementation detail.

Reactivity Rules

1. Access .value inside effects

// ✅ Reactive
effect(() => {
  console.log(count.value);
});

// ❌ Not reactive (reads signal object, not value)
effect(() => {
  console.log(count);
});

2. Effects track all accessed signals

const x = signal(1);
const y = signal(2);

effect(() => {
  console.log(x.value + y.value); // Tracks both x and y
});

x.value = 10; // Effect runs
y.value = 20; // Effect runs

3. Conditional dependencies are dynamic

const flag = signal(true);
const a = signal(1);
const b = signal(2);

effect(() => {
  console.log(flag.value ? a.value : b.value);
});

flag.value = false; // Effect runs, now tracks b instead of a
a.value = 10;       // No effect (a is no longer tracked)
b.value = 20;       // Effect runs

Performance Tips

Use computed() for expensive derivations

// ✅ Cached (only recomputes when data changes)
const filtered = computed(() => 
  items.value.filter(item => item.active)
);

// ❌ Recomputes on every access
const filtered = () => items.value.filter(item => item.active);
// ✅ One layout pass
batch(() => {
  width.value = 100;
  height.value = 50;
  padding.value = 2;
});

// ❌ Three layout passes
width.value = 100;
height.value = 50;
padding.value = 2;

Avoid unnecessary computed()

// ✅ Direct access (no overhead)
<Text content={count} />

// ❌ Unnecessary wrapper
const countText = computed(() => count.value);
<Text content={countText} />

See Also

Build docs developers (and LLMs) love