UNPKG

ranui

Version:

A framework-agnostic Web Components UI library built on native custom elements, with TypeScript types, light/dark theming, SSR and PWA support.

117 lines (116 loc) 5.57 kB
/** * Reactive primitives — signal / computed / createEffect / batch, plus an * ownership layer (createRoot / onCleanup / getOwner / runWithOwner) and untrack. * * Design reference: SwiftUI @Observable + Solid.js fine-grained reactivity. * * View = f(State) * signal — reactive source (@State) * computed — derived, LAZY + memoized (SwiftUI computed property / Solid memo) * createEffect— side effect that re-runs when its reads change (≈ body) * ownership — effects/memos form an owner tree; disposing a scope disposes * everything created under it (critical for MPA: tear down a page * and every effect it spawned is cleaned up in one call). * * Model (push-pull scheduling + lazy memos): * - Reading a source inside a computation links them both ways. * - Writing a signal marks direct observers DIRTY and their transitive memo * observers CHECK ("maybe dirty"), scheduling any dependent effects. Effects * flush once (deduped — diamond dependencies run an effect a single time). * - Memos recompute lazily on read, and only re-notify their observers when * their derived value actually changes (via `equals`). A CHECK computation * first pulls its sources up to date; if none changed value, it is skipped — * so a memo whose output is stable never wakes the effects behind it. * - Every computation is owned; re-running or disposing it first disposes its * owned children and runs its cleanups (no leaked nested effects). */ export type Getter<T> = () => T; export type Setter<T> = (value: T | ((prev: T) => T)) => void; export interface SignalOptions<T> { /** Custom equality. Defaults to Object.is. Return true to skip the update. */ equals?: (prev: T, next: T) => boolean; } /** Anything a computation can subscribe to (a signal, or a memo). */ interface SourceLike { observers: Set<Computation>; } /** Staleness of a computation. CLEAN → up to date; CHECK → a memo source may * have changed (verify before recomputing); DIRTY → definitely recompute. */ declare const CLEAN = 0; declare const CHECK = 1; declare const DIRTY = 2; type State = typeof CLEAN | typeof CHECK | typeof DIRTY; /** An effect or a memo — a node that runs a function and tracks its reads. */ interface Computation extends Owner { fn: () => unknown; /** Sources (signals / memos) read during the last run. */ sources: Set<SourceLike>; isEffect: boolean; running: boolean; disposed: boolean; state: State; observers: Set<Computation>; value: unknown; equals: (a: unknown, b: unknown) => boolean; } /** A node that can own child computations + cleanups (effect, memo, or root). */ export interface Owner { owned: Computation[]; cleanups: Array<() => void>; owner: Owner | null; } /** * Create a reactive value. Returns a `[getter, setter]` tuple. * * const [count, setCount] = signal(0); * count(); // read — auto-tracked inside computed / createEffect * setCount(1); // write — notifies dependents; skipped if unchanged * setCount((n) => n + 1) // updater form */ export declare function signal<T>(initial: T, options?: SignalOptions<T>): [Getter<T>, Setter<T>]; /** * Derived, read-only, LAZY + memoized value. Recomputes only when read after a * dependency changed; unread memos never recompute. When a recompute produces an * equal value (default `Object.is`, override via `options.equals`) the memo does * NOT notify its observers — so effects behind a value-stable memo stay asleep. * Owned by the current scope, so it is disposed when that scope is. * * const fullName = computed(() => `${first()} ${last()}`); * fullName(); // computes on first read, caches until a dep's value changes */ export declare function computed<T>(fn: () => T, options?: SignalOptions<T>): Getter<T>; /** * Run `fn` immediately, then re-run it whenever a signal/memo it read changes. * Owned by the current scope. `fn` may return a cleanup, called before each * re-run and on dispose. Returns a dispose function. * * const dispose = createEffect(() => { * el.textContent = `Count: ${count()}`; * return () => {}; // optional cleanup * }); */ export declare function createEffect(fn: () => void | (() => void)): () => void; /** * Coalesce multiple writes into one flush. Effects run once after `fn` returns, * deduplicated. Nested batches are absorbed by the outermost one. */ export declare function batch(fn: () => void): void; /** Read signals without subscribing the current computation to them. */ export declare function untrack<T>(fn: () => T): T; /** * Create a disposable reactive scope. Effects/memos created inside are owned by * it; calling the passed `dispose` tears them all down. Use one per page/route * in an MPA/SPA so navigating away cleans up every effect that page spawned. * * const dispose = createRoot((dispose) => { renderPage(); return dispose; }); * // ...later, on navigation: * dispose(); */ export declare function createRoot<T>(fn: (dispose: () => void) => T): T; /** Register a cleanup on the current scope, run when it re-runs or is disposed. */ export declare function onCleanup(fn: () => void): void; /** The current owner scope (for advanced integrations, e.g. a router). */ export declare function getOwner(): Owner | null; /** Run `fn` under a specific owner scope (pairs with getOwner). */ export declare function runWithOwner<T>(owner: Owner | null, fn: () => T): T; export {};