@solidjs/signals
The reactive core that powers SolidJS 2.0. It is developed in the Solid monorepo and includes first-class support for async, transitions, optimistic updates, and deeply reactive stores that go beyond what general-purpose signals libraries offer.
Status: Beta — this 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
npm install @solidjs/signals
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 effects — all connected through an automatic dependency graph. Updates are batched and flushed asynchronously via microtask, giving you consistent state without glitches.
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();
});
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.
const [a, setA] = createSignal(1);
const [b, setB] = createSignal(2);
setA(10);
setB(20);
flush();
Core Primitives
Signals
const [value, setValue] = createSignal(initialValue, options?);
Reactive state with a getter/setter pair. Supports custom equality via options.equals.
Memos
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
createEffect(
() => count(),
value => {
console.log(value);
}
);
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:
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.
const data = createMemo(async () => {
const response = await fetch(`/api/items?q=${query()}`);
return response.json();
});
isPending(data);
latest(data);
Use action() for mutations — imperative async workflows whose writes span an async gap. Each invocation runs as a single transaction: every write between yields batches into one atomic update, and nothing commits until the action completes or the next yield resolves.
const save = action(function* (item) {
yield fetch("/api/save", { method: "POST", body: JSON.stringify(item) });
});
Navigation-shaped updates don't need an action. A plain setter call is enough — reads pull the async, downstream async computeds hold their previous values until new ones are ready, and isPending/latest expose the in-flight state. Reach for action when writes happen after async work (pairing with optimistic primitives for tentative state that reverts on failure), not when the async is merely downstream of a synchronous write.
Framework-level actions (router form actions, server actions) are specializations of this primitive: the same transactional semantics with form binding, serialization, and submission tracking layered on top.
Optimistic Updates
Optimistic signals show an immediate value while async work is pending, then automatically revert when it settles:
const [optimisticCount, setOptimisticCount] = createOptimistic(0);
setOptimisticCount(count + 1);
Also available for stores via createOptimisticStore().
Stores
Proxy-based deeply reactive objects with per-property tracking:
import { createStore, reconcile } from "@solidjs/signals";
const [store, setStore] = createStore({ todos: [], filter: "all" });
setStore(s => {
s.filter = "active";
});
setStore(s => {
s.todos.push({ text: "New", done: false });
});
setStore(s => {
s.todos[0].done = true;
});
setStore(s => {
reconcile(serverTodos, "id")(s.todos);
});
Projections
Derived stores that transform data reactively:
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:
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:
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"));
});
Utilities
flush() | Process all pending updates |
untrack(fn) | Run fn without tracking dependencies |
isPending(accessor) | Check if an unrevealed value change is in flight for a read |
latest(accessor) | Get the last resolved value, or follow not-ready if none exists |
refresh(accessor) | Re-trigger an async computation (a quiet re-ask — not pending) |
affects(target, key?) | Declare that in-flight work will change the targeted data (reads pending until settle) |
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
From the monorepo root:
pnpm install
pnpm --filter @solidjs/signals build
pnpm --filter @solidjs/signals test
pnpm --filter @solidjs/signals test:watch
pnpm --filter @solidjs/signals test:gc
pnpm --filter @solidjs/signals bench
License
MIT