@rimitive/signals
v0.4.0
Published
Reactive primitives for Rimitive components
Maintainers
Readme
@rimitive/signals
Reactive primitives for rimitive. Signals, computeds, effects, and friends.
Quick Start
import { compose } from '@rimitive/core';
import {
SignalModule,
ComputedModule,
EffectModule,
} from '@rimitive/signals/extend';
const { signal, computed, effect } = compose(
SignalModule,
ComputedModule,
EffectModule
);
const count = signal(0);
const doubled = computed(() => count() * 2);
effect(() => console.log('Count:', count()));
// logs: "Count: 0"
count(5);
// logs: "Count: 5"signal
Reactive state. Read with sig(), write with sig(value).
const name = signal('Alice');
name(); // 'Alice'
name('Bob');
name(); // 'Bob'
name.peek(); // read without tracking
const count = signal(0);
count(count() + 1); // read then write
const items = signal<string[]>([]);
items([...items(), 'new']); // spread and appendcomputed
Derived values. Lazy, cached, auto-tracks dependencies.
const firstName = signal('Ada');
const lastName = signal('Lovelace');
const fullName = computed(() => `${firstName()} ${lastName()}`);
fullName(); // 'Ada Lovelace'
lastName('Byron');
fullName(); // 'Ada Byron'effect
Side effects when dependencies change. Synchronous—runs immediately, not next tick.
const user = signal({ name: 'Alice', online: false });
effect(() => {
console.log(`${user().name} is ${user().online ? 'online' : 'offline'}`);
});
// logs: "Alice is offline"
user({ ...user(), online: true });
// logs: "Alice is online"Returns a dispose function:
const dispose = effect(() => console.log(count()));
dispose(); // stops trackingFlush Strategies
By default, effects run synchronously—the moment a dependency changes, the effect runs. Sometimes you want to defer execution. That's what flush strategies are for.
import { mt, raf, debounce } from '@rimitive/signals/extend';
// Run on next microtask (coalesces rapid updates)
effect(mt(() => console.log(count())));
// Run on requestAnimationFrame (ideal for DOM reads/writes)
effect(raf(() => updateCanvas(data())));
// Run after 300ms of no changes
effect(debounce(300, () => search(query())));| Strategy | Use case |
| ------------------ | ------------------------------------------------------------- |
| mt(fn) | Batch multiple synchronous signal updates into one effect run |
| raf(fn) | DOM measurements, canvas rendering, animations |
| debounce(ms, fn) | User input, search boxes, expensive operations |
Without a strategy, effects are synchronous. Wrap your effect function in a strategy to control timing.
batch
Group updates into a single notification.
import { BatchModule } from '@rimitive/signals/extend';
const { signal, effect, batch } = compose(
SignalModule,
ComputedModule,
EffectModule,
BatchModule
);
const a = signal(1);
const b = signal(2);
effect(() => console.log(a() + b()));
// logs: 3
batch(() => {
a(10);
b(20);
});
// logs: 30 (once, not twice)subscribe
Subscribe without creating a reactive context.
import { SubscribeModule } from '@rimitive/signals/extend';
const { signal, subscribe } = compose(SignalModule, SubscribeModule);
const count = signal(0);
const unsubscribe = subscribe(count, (value) => {
console.log('Count changed to:', value);
});
count(1); // logs: "Count changed to: 1"
unsubscribe();untrack
Read signals without tracking.
import { UntrackModule } from '@rimitive/signals/extend';
const { signal, computed, untrack } = compose(
SignalModule,
ComputedModule,
UntrackModule
);
const a = signal(1);
const b = signal(2);
// Only tracks `a`, not `b`
const result = computed(() => a() + untrack(() => b()));
result(); // 3
b(10);
result(); // still 3 (b not tracked)
a(5);
result(); // 15 (recomputes, reads current b)iter
Reactive collection with O(1) mutations. Like a signal, but for lists.
import { IterModule } from '@rimitive/signals/extend';
const { signal, computed, effect, iter } = compose(
SignalModule,
ComputedModule,
EffectModule,
IterModule
);
type Todo = { id: number; text: string };
const todos = iter<Todo>(
(todo) => todo.id,
[
{ id: 1, text: 'Buy milk' },
{ id: 2, text: 'Walk dog' },
]
);Reading (tracks dependency)
// Callable - returns array
todos(); // [{ id: 1, ... }, { id: 2, ... }]
// Iterable - works with for...of
for (const todo of todos) {
console.log(todo.text);
}
// In computed/effect - auto-tracks
const count = computed(() => todos().length);
effect(() => console.log('Todos:', [...todos]));O(1) Mutations
todos.append({ id: 3, text: 'New todo' });
todos.prepend({ id: 0, text: 'First' });
todos.insertAfter(refTodo, newTodo);
todos.insertBefore(refTodo, newTodo);
todos.remove(todo); // by item
todos.remove(1); // by key
todos.update({ id: 1, text: 'Updated' });
todos.clear();Bulk Replace (with reconciliation)
// Callable with array - reconciles efficiently
todos([
{ id: 2, text: 'Walk dog' },
{ id: 3, text: 'New item' },
]);
// Removes id:1, adds id:3, reorders - minimal operationsLookups
todos.get(1); // { id: 1, text: '...' } | undefined
todos.has(1); // true
todos.size; // 2 (reactive)
todos.peek(); // [...] without tracking (like signal.peek())Why iter?
| Operation | signal<T[]> | iter<T> |
| ----------- | ------------- | --------- |
| Append | O(n) copy | O(1) |
| Remove | O(n) copy | O(1) |
| Update item | O(n) copy | O(1) |
| Read all | O(1) | O(n) |
Use iter when you frequently mutate lists. Use signal<T[]> for small, rarely-changed arrays.
