Skip to content
ripple logoRippleState
Tiny, type-safe reactive primitives — signals, effects, computed values, and object stores. Zero dependencies, works everywhere.
v0.0.15.4 KB gzip 0 depsBrowser · Node.js · SSR · Deno
signalcomputedeffecteffectAsyncresourceasyncComputedwatchbatch +19 more →

Why Ripple?

Plain variables don't notify anything when they change. Framework-specific stores add boilerplate and coupling.

ts
// Before — manual notification
let count = 0;
const listeners: Array<() => void> = [];
function setCount(n: number) {
  count = n;
  listeners.forEach((fn) => fn());
}

// After — Ripple signals
import { signal, effect } from '@vielzeug/ripple';
const count = signal(0);
effect(() => console.log(count.value)); // auto-tracks dependencies
count.value = 1; // notifies automatically
FeatureRippleZustandJotaiNanostores
Bundle size5.4 KB~3.5 kB~7 kB~2 kB
Zero dependencies
Framework-agnosticReact-first
Fine-grained reactivity (per-property) (whole store) (atom)
Structured object stores (store, lens)Manual atoms
Async computed (asyncComputed)Manual
Undo / redo history (storeWithHistory)Manual
Computed signals (lazy, glitch-free)Selectors (atoms)
Batched writes (batch)
Explicit cleanup / scopes (scope, onCleanup)
SSR support
TypeScript — strict genericsPartial
React Suspense
Redux DevTools

Use Ripple when you need fine-grained, per-property reactivity without framework lock-in — especially for web components, vanilla JS apps, or any environment where you want zero runtime dependencies and explicit lifecycle control.

Consider alternatives when you are React-only and need Suspense or React Query integration (Jotai), need Redux DevTools out of the box (Zustand), or need a minimal atom store with no extra features (Nanostores).

Installation

sh
pnpm add @vielzeug/ripple
sh
npm install @vielzeug/ripple
sh
yarn add @vielzeug/ripple

Quick Start

ts
import { signal, derive, effect, watch, batch } from '@vielzeug/ripple';

const count = signal(0);
const doubled = derive(count, (n) => n * 2); // ComputedSignal<number>

// Side-effect: runs immediately and re-runs on change
const sub = effect(() => {
  console.log('doubled:', doubled.value);
});

count.value = 5; // → logs "doubled: 10"

// Explicit subscription — only fires on change, not immediately
const stopWatch = watch(count, (next, prev) => {
  console.log(prev, '→', next);
});

batch(() => {
  count.value = 10;
  count.value = 20; // one notification fires with the final value
});

sub.dispose();
stopWatch.dispose();
doubled.dispose();
ts
import { store, watch, batch, computed } from '@vielzeug/ripple';

const counter = store({ count: 0, label: 'counter' });

// Read
console.log(counter.value.count); // 0

// Watch a typed lens — only fires when that path changes
const countLens = counter.lens('count'); // Signal<number>
const stopWatch = watch(countLens, (next, prev) => {
  console.log('count:', prev, '→', next);
});

// Derived slice
const label = computed(() => counter.value.label); // ComputedSignal<string>

// Mutations
counter.patch({ count: 1 }); // shallow merge
counter.replace((s) => ({ ...s, count: s.count + 1 })); // replace via fn
countLens.value = 10; // write directly through the lens

batch(() => {
  counter.patch({ count: 5 });
  counter.patch({ label: 'done' });
});

counter.reset();
stopWatch.dispose();
label.dispose();

Features

  • signal(value, options?) — reactive atom; read .value, write .value = next; batched: true coalesces rapid writes into one microtask notification
  • computed(fn, options?) — lazy derived signal; glitch-free; auto-tracks dependencies read inside fn
  • effect(fn, options?) — side-effect that re-runs when dependencies change; options: scheduler ('sync' | 'microtask' | custom fn), maxIterations, name
  • effectAsync(fn, options?) — async side-effect with an AbortSignal that fires on re-run or dispose; returns AsyncSubscription with [Symbol.asyncDispose]
  • resource(factory, options?) — preferred name for async computed; exposes .data, .error, .isLoading signals; factory receives an AbortSignal
  • asyncComputed(factory, options?) — same as resource(); kept for compatibility
  • watch(source, cb, options?) — explicit subscription that fires only when the value changes; returns a Subscription
  • batch(fn) — flush all notifications once after bulk updates
  • untrack(fn) — read signals inside an effect without creating subscriptions
  • onCleanup(fn) — register teardown from inside an effect or scope without using the return value
  • scope(setup?) — isolated cleanup context; collect teardown via onCleanup inside scope.run(fn); release with scope.dispose()
  • derive(source, project, options?) — project a reactive source into a ComputedSignal; cleaner alternative to selector(source, project)
  • filter(source, predicate, options?) — filter a reactive source; returns value when predicate is true, undefined otherwise; type-predicate overload narrows T → U | undefined
  • selector(source, project, predicate?, options?) — project + optional filter utility; use derive() / filter() for single-concern cases
  • readonly(source) — wraps any signal as a ComputedSignal — read-only at the type level; dispose() is always a no-op
  • debugEffect(fn, options?) — like effect(), but logs reactive deps on every run; import from @vielzeug/ripple/devtools — tree-shaken from production bundles
  • isSignal(v), isComputed(v), isStore(v) — type guards using an internal symbol marker
  • store(init, options?) — structured reactive object container
  • .patch(partial) — shallow-merge a Partial<T> into state
  • .replace(fn) — derive next state from current via a function; same-reference return is a no-op
  • .reset() — restore the initial state baseline
  • .lens<P>(path) — cached writable Signal for a property or dot-path; writes produce an immutable copy
  • storeWithHistory(storeOrInit, options?) — store with snapshot history; accepts an existing Store<T> (not owned) or a plain object; undo(), redo(), historyAt(i), historyLength; reactive canUndo/canRedo properties
  • getDevToolsHook() — returns the currently installed DevTools hook, or null; install via @vielzeug/ripple/devtools
  • Glitch-free propagation — computed signals propagate in dependency order; effects always observe a consistent snapshot
  • Infinite loop detection — built-in guard against effect re-entry cycles (100 iterations default, configurable per effect)
  • Automatic computed disposalcomputed() created inside effect() auto-disposes with the effect

Sub-paths

ImportPurpose
@vielzeug/rippleAll exports and types
@vielzeug/ripple/devtoolsinstallDevTools, debugEffect — DevTools hook and reactive source tracing (dev-only, tree-shaken)
@vielzeug/ripple/ssrSSR tracking isolation helpers (withProvider, runWithProvider, createAsyncProvider). Node.js only.

Documentation

See Also

  • Craft — web-component authoring framework built on ripple
  • Forge — typed form state that uses signals for field reactivity and submission tracking
  • Herald — typed event bus; use alongside ripple for cross-module messaging without shared signals