UNPKG

@solidjs/signals

Version:

SolidJS' standalone reactivity implementation

256 lines (187 loc) 7.75 kB
# @solidjs/signals The reactive core that powers [SolidJS 2.0](https://github.com/solidjs/solid). This is a standalone signals library designed for rendering — it includes first-class support for async, transitions, optimistic updates, and deeply reactive stores that go beyond what general-purpose signals libraries offer. > **Status:** Betathis package is the reactive foundation of SolidJS 2.0 Beta. The API is stabilizing but may still have breaking changes before a final release. ## Installation ```bash npm install @solidjs/signals # or pnpm add @solidjs/signals ``` ## Overview `@solidjs/signals` is a push-pull hybrid reactive system. Signals hold values, computeds derive from them, and effects run side effectsall connected through an automatic dependency graph. Updates are **batched** and flushed asynchronously via microtask, giving you consistent state without glitches. ```typescript import { createEffect, createMemo, createRoot, createSignal, flush } from "@solidjs/signals"; createRoot(() => { const [count, setCount] = createSignal(0); const doubled = createMemo(() => count() * 2); createEffect( () => doubled(), value => { console.log("Doubled:", value); } ); setCount(5); flush(); // "Doubled: 10" }); ``` ### Batched Updates Signal writes are batched — reads after a write won't reflect the new value until `flush()` runs. This prevents glitches and unnecessary recomputation. ```typescript const [a, setA] = createSignal(1); const [b, setB] = createSignal(2); setA(10); setB(20); // Neither has updated yet — both writes are batched flush(); // Now both update and downstream effects run once ``` ## Core Primitives ### Signals ```typescript const [value, setValue] = createSignal(initialValue, options?); ``` Reactive state with a getter/setter pair. Supports custom equality via `options.equals`. ### Memos ```typescript const derived = createMemo(() => expensive(signal())); ``` Read-only derived values that cache their result and only recompute when dependencies change. Supports async compute functions — return a `Promise` or `AsyncIterable` and downstream consumers will wait automatically. ### Effects ```typescript // Two-phase: compute tracks dependencies, effect runs side effects createEffect( () => count(), value => { console.log(value); } ); // Render-phase effect (runs before user effects) createRenderEffect( () => count(), value => { updateDOM(value); } ); ``` Effects split tracking from execution. `createEffect` and `createRenderEffect` take a compute function (for tracking) and an effect function (for side effects). ### Writable Memos Pass a function to `createSignal` to get a writable derived value — a memo you can also set: ```typescript const [value, setValue] = createSignal(prev => transform(source(), prev), initialValue); ``` ## Async Computeds can return promises or async iterables. The reactive graph handles this automatically — previous values are held in place until the async work resolves, so downstream consumers never see an inconsistent state. ```typescript const data = createMemo(async () => { const response = await fetch(`/api/items?q=${query()}`); return response.json(); }); // Check async state isPending(data); // true while loading latest(data); // last resolved value ``` Use `action()` to coordinate async workflows with the reactive graph: ```typescript const save = action(function* (item) { yield fetch("/api/save", { method: "POST", body: JSON.stringify(item) }); }); ``` ## Optimistic Updates Optimistic signals show an immediate value while async work is pending, then automatically revert when it settles: ```typescript const [optimisticCount, setOptimisticCount] = createOptimistic(0); // Immediate UI update — reverts when the async work resolves setOptimisticCount(count + 1); ``` Also available for stores via `createOptimisticStore()`. ## Stores Proxy-based deeply reactive objects with per-property tracking: ```typescript import { createStore, reconcile } from "@solidjs/signals"; const [store, setStore] = createStore({ todos: [], filter: "all" }); // Setter takes a mutating callback — mutations are intercepted by the proxy setStore(s => { s.filter = "active"; }); setStore(s => { s.todos.push({ text: "New", done: false }); }); setStore(s => { s.todos[0].done = true; }); // Reconcile from server data setStore(s => { reconcile(serverTodos, "id")(s.todos); }); ``` ### Projections Derived stores that transform data reactively: ```typescript import { createProjection } from "@solidjs/signals"; const filtered = createProjection( draft => { draft.items = store.todos.filter(t => !t.done); }, { items: [] } ); ``` ## Boundaries Intercept async loading and error states in the reactive graph: ```typescript import { createErrorBoundary, createLoadingBoundary } from "@solidjs/signals"; createErrorBoundary( () => riskyComputation(), (error, reset) => handleError(error) ); createLoadingBoundary( () => asyncContent(), () => showFallback() ); ``` ## Ownership & Context All reactive nodes exist within an **owner** tree that handles disposal and context propagation: ```typescript import { createContext, createRoot, getContext, onCleanup, setContext } from "@solidjs/signals"; const ThemeContext = createContext("light"); createRoot(dispose => { setContext(ThemeContext, "dark"); createEffect( () => getContext(ThemeContext), theme => { console.log("Theme:", theme); } ); onCleanup(() => console.log("Disposed")); // Call dispose() to tear down the tree }); ``` ## Utilities | Function | Description | | ------------------------ | ------------------------------------------------------------------ | | `flush()` | Process all pending updates | | `untrack(fn)` | Run `fn` without tracking dependencies | | `isPending(accessor)` | Check if an async accessor is loading | | `latest(accessor)` | Get the last resolved value of an async accessor | | `refresh(accessor)` | Re-trigger an async computation | | `isRefreshing(accessor)` | Check if an async accessor is refreshing | | `resolve(fn)` | Returns a promise that resolves when a reactive expression settles | | `mapArray(list, mapFn)` | Reactive array mapping with keyed reconciliation | | `repeat(count, mapFn)` | Reactive repeat based on a reactive count | | `onSettled(fn)` | Run a callback after the current flush cycle completes | | `snapshot(store)` | Returns a non-reactive copy of a store, preserving unmodified references | | `reconcile(value, key)` | Returns a diffing function for updating stores from new data | | `merge(...sources)` | Reactively merges multiple objects/stores, last source wins | | `omit(props, ...keys)` | Creates a reactive view of an object with specified keys removed | | `deep(store)` | Tracks all nested changes on a store | | `storePath(...path)` | Path-based setter for stores as an alternative to mutating callbacks | ## Development ```bash pnpm install pnpm build # Rollup build (dev/prod/node outputs) pnpm test # Run tests pnpm test:watch # Watch mode pnpm test:gc # Tests with GC exposed pnpm bench # Benchmarks pnpm format # Prettier + import sorting ``` ## License MIT