wana
Version:
Easy observable state for React
351 lines (326 loc) • 13.8 kB
TypeScript
import { ReactElement, Ref, RefAttributes } from 'react';
interface Disposable {
/** Release any memory that would otherwise leak */
dispose: () => void;
}
declare type Falsy = false | null | undefined;
/** For accessing the proxied state */
declare const $$: unique symbol;
/** For storing observers and the observable proxy */
declare const $O: unique symbol;
/** For injecting custom proxy traps */
declare const $T: unique symbol;
/** Watch the properties of the given observable object */
declare const shallowChanges: (target: ObserverTarget, onChange: (change: Change) => void) => {
onChange: (change: Change) => void;
dispose(): void;
};
/** A function that can subscribe to observable objects. */
declare abstract class Observer implements Disposable {
observed: ReadonlySet<ObservedSlot>;
onChange: ((change: Change) => void) | null;
/** The current nonce of our observed values combined */
get nonce(): number;
dispose(): void;
}
/** @internal */
declare class Observable<T extends object = any> extends Map<ObservedKey, ObservedSlot> {
readonly source?: T | undefined;
readonly proxy: T | undefined;
constructor(source?: T | undefined);
/** Return true if at least one observer exists for the given key. */
has(key: ObservedKey): boolean;
/** Return a `Set` of observers for the given key, even if not observed. */
get(key: ObservedKey): ObservedSlot;
/** Create an observer for the given key. */
observe(key: ObservedKey, onChange: (change: Change) => void): {
onChange: (change: Change) => void;
dispose(): void;
};
}
/** Mutable state with an associated observable */
declare type ObserverTarget = object & {
[$O]?: Observable;
};
/** Any value acting as an object key */
declare type ObservedKey = any;
/** An observer set with metadata about what's being observed */
declare class ObservedSlot extends Set<ChangeObserver> {
readonly owner: Observable;
readonly key: ObservedKey;
nonce: number;
constructor(owner: Observable, key: ObservedKey);
onChange(change: Change): void;
}
/** An observed mutation of an observable object. */
interface Change<T = any> {
op: 'add' | 'replace' | 'remove' | 'splice' | 'clear' | 'define';
target: object;
key?: any;
value?: T;
oldValue?: T;
}
/** The most basic observer of changes. */
interface ChangeObserver extends Disposable {
onChange: ((change: Change) => void) | null;
}
/** Run an effect when its tracked values change. */
declare function auto(effect: () => void, config?: AutoConfig): Auto;
interface AutoConfig {
/** When true, react to changes immediately. By default, changes are delayed until the next microtask loop */
sync?: boolean;
/** By default, rerun the last effect (after any delay) */
onDirty?: Auto['onDirty'];
/** By default, errors are rethrown */
onError?: Auto['onError'];
/** Run an effect after an `observer` is activated */
onCommit?: Auto['onCommit'];
/** Run an effect after being disposed */
onDispose?: Auto['onDispose'];
}
declare class Auto {
sync: boolean;
dirty: boolean;
nonce: number;
observer: AutoObserver | null;
onDirty: (this: Auto) => void;
onError: (this: Auto, error: Error) => void;
onCommit: (observer: AutoObserver) => void;
onDispose: () => void;
constructor(config?: AutoConfig);
run<T extends Function>(compute: T): any;
/** Rerun the last effect and commit its observer */
rerun(): any;
/**
* Replace the current `observer`.
*
* Pass a nonce to bail out if observed values have since changed.
*/
commit(observer: AutoObserver, nonce?: number): boolean;
dispose(): void;
/**
* @internal
* Create an observer and start observing.
*
* The given `effect` is used by `rerun` calls made after
* the new observer is committed.
*/
start(effect: Function): AutoObserver;
/**
* @internal
* Stop observing and reset the `dirty` flag.
*/
stop(): this;
/**
* @internal
* Replace the current `observer` when appropriate.
*/
clear(): void;
protected _onChange(change: Change): void;
protected _onDirty(): void;
}
declare class AutoObserver extends Observer {
readonly effect: Function;
observed: Set<ObservedSlot>;
constructor(effect: Function);
}
/**
* An observable getter that memoizes its result.
*
* The memoization is observed, so when a dependency changes,
* the memoized value is released and observers are notified.
*/
interface Derived<T = any> extends Disposable {
/** The underlying observable */
[$O]?: Observable;
/** The underlying observer */
auto: Auto;
/** Get the current value */
(): T;
}
declare function isDerived(value: unknown): value is Derived;
/** Convert all `Derived<T>` property types into `T` */
declare type WithDerived<T extends object> = {
[P in keyof T]: T[P] extends Derived<infer U> ? U : T[P];
};
/**
* Pass an **object** to receive an observable proxy.
* Pass a **function** to receive an observable getter.
* Anything else is returned as-is.
*/
declare function o<T>(value: T): T extends () => infer U ? Derived<U> : T extends Function ? Derived : T;
/**
* Create a promise to resolve when the given condition returns true.
* Any observable access in the condition is tracked.
*/
declare const when: (condition: () => boolean) => Promise<void>;
declare type WatchedState = ObserverTarget & {
forEach?: (cb: (value: any, key: any, ctx: any) => void) => void;
};
declare type ChangeHandler = (change: Change) => void;
/**
* Watch a single property.
* If its value is observable, watch it recursively.
*/
declare function watch<P>(root: Map<P, any>, key: P, onChange: (change: Change) => void): Watcher;
declare function watch<T extends object, P extends string & keyof T>(root: T, key: P, onChange: (change: Change) => void): Watcher;
/**
* Watch an observable tree for changes.
* Only observable objects are searched for watchable values.
*/
declare function watch(root: object, onChange: ChangeHandler): Watcher;
/** An observer of deep changes */
declare class Watcher extends Observer {
readonly root: object;
readonly key?: any;
observed: Set<ObservedSlot>;
counts: Map<object, number>;
constructor(root: object, onChange: ChangeHandler, key?: any);
watch: (value: any, key?: any, ctx?: any) => void;
unwatch: (value: any, key?: any, ctx?: any) => void;
dispose(): void;
protected _watch(target: WatchedState): void;
protected _unwatch(target: WatchedState): void;
}
/**
* Get the original object from an observable proxy,
* or wrap a function to disable observation inside it.
*
* Read `no` as "non-observable", essentially the reverse of the `o` function.
*/
declare function no<T>(value: T): T;
/** Run an effect without any observable tracking */
declare function noto<T>(effect: () => T): T;
interface DebugState {
name: string;
actions?: any[];
renders?: number;
}
/** Get the `Auto` object for the current `withAuto` component being rendered. */
declare function getCurrentAuto(): Auto | null;
/**
* Get the `DebugState` object of the `target` object.
*
* Returns `undefined` if target was never passed to `setDebug`.
*/
declare function getDebug(target: object): DebugState;
/**
* Set the `DebugState` object of the `target` object.
*/
declare function setDebug<T>(target: T, debug: DebugState): T;
/** Safely add an action to a `DebugState` object */
declare function addDebugAction(target: any, action: any): void;
declare type OnRender = (auto: Auto, depth: number, component: React.FunctionComponent<any>) => void;
declare type Globals = {
/** Notify the current observer. */
observe: ((target: ObserverTarget, key: any) => void) | null;
/** The `Auto` object for the current `withAuto` component being rendered. */
auto: Auto | null;
/** For debugging re-renders. Only called in development. */
onRender: OnRender | null;
/** For spying on every change event. */
onChange: ((change: Change) => void) | null;
};
declare const globals: Globals;
/**
* Flush the queue of delayed reactions.
*
* Returns `true` if the queue wasn't flushed entirely.
*
* Useful when testing `wana`-integrated components/hooks.
*/
declare function flushSync(): boolean;
interface Props {
observer?: AutoObserver;
mounted?: boolean;
}
/**
* Call the returned function to set the "mounting state" for the
* given `Auto` object. Observation is postponed until mounted.
*/
declare function mountAuto(auto: Auto): (props: Props) => void;
declare const ObjectTraps: ProxyHandler<object>;
declare function useO<T extends object>(state: Exclude<T, Function>, deps?: readonly any[]): WithDerived<T>;
/**
* Create an observable getter that is managed by React.
* This lets you memoize an expensive combination of observable values.
*/
declare function useO<T>(create: () => () => T, deps?: readonly any[]): Derived<T>;
/** Create observable component state. */
declare function useO<T>(create: () => Exclude<T, Function>, deps?: readonly any[]): T;
/** Memoize an object and return its observable proxy. Non-objects are returned as-is. */
declare function useO<T>(state: T, deps?: readonly any[]): T;
declare type Deps$1 = readonly any[];
declare type EffectReturn = void | (() => void | undefined);
/** Wrap a `useEffect` call with magic observable tracking */
declare function useAuto(effect: () => EffectReturn, deps?: Deps$1): Auto;
/**
* Combine a `useEffect` call with an `Auto` instance that invokes an
* unobserved side `effect` when the `compute` function returns a new value.
*/
declare function useAuto<T>(compute: () => T, effect: (value: T) => EffectReturn, deps?: Deps$1): Auto;
interface Component<P = any> {
(props: P): ReactElement | null;
displayName?: string;
}
interface RefForwardingComponent<T = any, P = any> {
(props: P, ref: Ref<T>): ReactElement | null;
displayName?: string;
}
declare type RefForwardingAuto<T extends RefForwardingComponent> = T & ((props: T extends RefForwardingComponent<infer U, infer P> ? P & RefAttributes<U> : never) => ReactElement | null);
/** Wrap a component with magic observable tracking */
declare function withAuto<T extends Component>(render: T): T;
/** Wrap a component with `forwardRef` and magic observable tracking */
declare function withAuto<T extends RefForwardingComponent>(render: T): RefForwardingAuto<T>;
/**
* Create an observable getter that is managed by React.
* This lets you memoize an expensive combination of observable values.
*
* If the `compute` argument changes over the course of a component's lifetime,
* its value should be added to the `deps` array.
*
* When `deps` are changed, the derived state is reset. This is useful when
* the `compute` function is using a variable from another function scope.
*/
declare function useDerived<T>(compute: () => T, deps?: readonly any[]): Derived<T>;
declare function useDerived<T>(compute: (() => T) | Falsy, deps?: readonly any[]): Derived<T> | null;
declare function useDerived<T>(compute: () => T, discard: (memo: T, oldMemo: T | undefined) => boolean, deps?: readonly any[]): Derived<T>;
declare function useDerived<T>(compute: (() => T) | Falsy, discard: (memo: T, oldMemo: T | undefined) => boolean, deps?: readonly any[]): Derived<T> | null;
/** Listen for shallow changes to an observable object. */
declare function useChanges(target: ObserverTarget, onChange: (change: Change) => void, deps?: readonly any[]): void;
declare type Deps = readonly any[];
declare type UnmountFn = () => undefined | void;
declare type Source<T = any> = {
[key: string]: T;
} | ReadonlyArray<T> | ReadonlySet<T> | ReadonlyMap<any, T>;
declare type Effect<T extends Source> = T extends Source<infer U> ? T extends ReadonlyMap<infer P, U> ? (value: U, key: P) => UnmountFn | void : T extends ReadonlyArray<U> | ReadonlySet<U> ? (value: U) => UnmountFn | void : (value: U, key: string) => UnmountFn | void : never;
/**
* Create a layout effect for every key of your `values` object.
*
* Values are passed to your `effect` function as they are added and replaced.
* The `effect` can return a cleanup function to call for removed values.
*
* Map objects use their keys
*/
declare function useEffects<T extends Source>(source: T, effect: Effect<T>, deps?: Deps): void;
/**
* An alternative to `withAuto` that re-renders the caller
* when the given observable value is changed.
*
* If you pass an object without a key, the entire object
* is observed.
*
* ⚠️ This hook has performance drawbacks! It cannot automatically
* batch React updates, so you need to wrap changes with `batchedUpdates`
* to avoid multiple re-renders. It also cannot wait for ancestor
* components to re-render first, which also leads to excessive re-renders.
*/
declare function useBinding<T extends object, P extends keyof T>(target: T extends ReadonlyMap<any, any> ? never : T, key: P): T[P];
declare function useBinding<K, V>(target: ReadonlyMap<K, V>, key: K): V | undefined;
declare function useBinding<T>(target: Derived<T>): T;
declare function useBinding<T extends object>(target: T): T;
/** @internal */
declare const useAutoContext: () => {
depth: number;
};
export { $$, $O, $T, Auto, AutoConfig, Change, ChangeObserver, Derived, ObjectTraps, Observable, ObservedSlot, Observer, ObserverTarget, Watcher, addDebugAction, auto, flushSync, getCurrentAuto, getDebug, globals, isDerived, mountAuto, no, noto, o, setDebug, shallowChanges, useAuto, useAutoContext, useBinding, useChanges, useDerived, useEffects, useO, watch, when, withAuto };