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.
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.
A function that computes the derived value. Dependencies are tracked automatically.
Returns: ReadonlySignal<T> — A reactive container with a read-only .value property.
Derived State
Conditional Formatting
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.
The effect function. Dependencies are tracked automatically.
Returns: () => void — A cleanup function to stop the effect.
Manual Effect
Effect with Cleanup
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.
A function that updates signals. Effects are deferred until the batch completes.
Returns: void
Without batch (3 effect runs)
With batch (1 effect run)
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
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