solidjs/solid
Reactivity primitives
Active contributors: Ryan Carniato, Damian Tarnawski
This page documents the reactive runtime — the core of solid-js. The runtime exposes signals, derivations (memo, effect, render-effect, computed, reaction, deferred, selector), resources, transitions, error boundaries, contexts, and a small set of interop helpers (observable, from, mapArray, indexArray, requestCallback).
Purpose
The reactive runtime is the dependency graph that backs every Solid app. A read of a signal inside a tracking scope registers the active Listener as an observer; a write notifies observers and queues them for re-execution. Memos cache derivations; effects perform side effects; resources wrap async work and integrate with <Suspense>.
Directory layout
packages/solid/src/reactive/
├── signal.ts # 1,821 lines — the entire reactive graph algorithm
├── scheduler.ts # MessageChannel-based time-slicer (port of React's scheduler)
├── observable.ts # observable(...) / from(...) for RxJS-style interop
└── array.ts # mapArray (keyed) and indexArray (positional) for <For> / <Index>packages/solid/src/index.ts re-exports the public surface from these files.
Key abstractions
| Symbol | File | Description |
|---|---|---|
createSignal<T>(value?, options?) |
signal.ts |
Returns [Accessor<T>, Setter<T>]. Reading is a tracking dependency; writing notifies observers. |
createMemo<T>(fn, value?, options?) |
signal.ts |
Cached derivation. Re-runs when sources change; only propagates when the result fails the equality check. |
createEffect<T>(fn, value?, options?) |
signal.ts |
User-level side effect. Runs after the next microtask flush. |
createRenderEffect<T>(fn, value?, options?) |
signal.ts |
Like createEffect but runs synchronously during the update pass. The compiler emits these for DOM bindings. |
createComputed<T>(fn, value?, options?) |
signal.ts |
Like createRenderEffect but pure (no Suspense interaction). Used internally. |
createReaction(onInvalidate, options?) |
signal.ts |
Returns a track(fn) that runs fn once and calls onInvalidate when any tracked source changes. |
createDeferred<T>(source, options?) |
signal.ts |
Returns a delayed accessor that updates only when the runtime is idle (uses requestCallback). |
createSelector<T, U>(source, equals?) |
signal.ts |
Returns a function (key) => boolean that subscribes only the keys that actually match. Used to keep <For> selection updates O(1). |
createResource<T, S>(source?, fetcher, options?) |
signal.ts |
Async accessor with loading / error / latest / state properties. Integrates with <Suspense>. |
createRoot<T>(fn, detachedOwner?) |
signal.ts |
Allocates a top-level Owner. Required at the entry of a render tree. |
createContext<T>(default?) |
signal.ts |
Returns { id, defaultValue, Provider }. |
useContext(ctx) |
signal.ts |
Reads the nearest provided value walking up Owner.context. |
getOwner() / runWithOwner(o, fn) / getListener() |
signal.ts |
Manual access to the active Owner / Listener. |
untrack(fn) |
signal.ts |
Run fn with Listener = null. |
batch(fn) |
signal.ts |
Defer observer notifications until fn returns. |
on(deps, fn, options?) |
signal.ts |
Helper for explicit dependency tracking inside an effect/memo. |
onMount(fn) / onCleanup(fn) / onError(fn) |
signal.ts |
Lifecycle helpers. onMount is createEffect(() => untrack(fn)). |
catchError(fn, onError) |
signal.ts |
Programmatic equivalent of <ErrorBoundary>. |
startTransition(fn) / useTransition() |
signal.ts |
Concurrent rendering: stages updates against a shadow tValue and commits when ready. |
enableScheduling(scheduler?) |
signal.ts |
Activates the time-slicing scheduler (scheduler.ts). |
enableExternalSource(factory, untrack) |
signal.ts |
Adapter so 3rd-party reactive sources (RxJS, MobX, …) can interop with Solid. |
equalFn(a, b) |
signal.ts |
Default Object.is-style equality check. |
requestCallback(fn, options?) / cancelCallback(task) |
scheduler.ts |
MessageChannel-based deferred callbacks. |
observable(accessor) / from(producer, init?) |
observable.ts |
Bridge to TC39 / RxJS observables. |
mapArray(list, fn, options?) |
array.ts |
Keyed reconciliation; underlying helper for <For>. |
indexArray(list, fn, options?) |
array.ts |
Positional reconciliation; underlying helper for <Index>. |
How it works
The reactive graph
Every reactive primitive contributes a node to the graph:
- A
SignalState<T>hasvalue,observers[],observerSlots[], optionalcomparator, and (in dev) aname. - A
Computation<Init, Next>has the function to run (fn),state(STALE/PENDING/0),sources[],sourceSlots[],value,updatedAt, and the standardOwnerfields (owned,cleanups,owner,context). - A memo is both a
SignalState(it has observers) and aComputation(it has sources). TheMemo<Prev, Next>interface intersects both shapes.
graph TD
Listener["Active Listener<br>(current Computation)"]
Owner["Active Owner"]
subgraph "Read path"
ReadSignal["readSignal()<br>register Listener as observer"]
ReadSignal -->|push to observers[]| Signal[SignalState]
ReadSignal -->|push to sources[]| Computation
end
subgraph "Write path"
WriteSignal["writeSignal()<br>compare via equals; if changed:"]
WriteSignal -->|enqueue observers| Updates[Updates queue]
end
subgraph "Update pass"
RunUpdates["runUpdates()<br>drains Updates"]
RunEffects["runEffects()<br>drains Effects (user effects)"]
Updates --> RunUpdates
RunUpdates --> Effects
Effects --> RunEffects
end
Listener -.-> ReadSignal
Owner -.->|owns| ComputationrunUpdates (in signal.ts) drains the Updates queue iteratively: it pops a Computation, runs it, which produces new writes that push more computations, and repeats until the queue is empty. User effects (createEffect) are deferred into a separate Effects array and run after the update pass to avoid stalling the synchronous reactive pipeline.
Owners and disposal
Owner forms a tree mirroring the lexical structure of the program. createRoot(fn) pushes a fresh root; createComponent (in packages/solid/src/render/component.ts) implicitly creates a child owner for each component's reactive scope. When an owner disposes:
- All
cleanups[]registered viaonCleanuprun in LIFO order. - All
owned[]child computations are disposed recursively. - The owner's slot is removed from its parent's
owned[].
runWithOwner(o, fn) lets you escape the lexical owner — useful for libraries that need to register cleanups against a long-lived parent.
Signal updates and equality
writeSignal uses comparator || equalFn to decide whether to notify. equalFn is (a, b) => a === b. To force a notification on every write, pass { equals: false }. To use a custom comparator, pass { equals: (a, b) => deepEqual(a, b) }.
Memos
createMemo is the deduplication primitive. Internally it is a Computation whose value is read by other tracking scopes through readSignal. Because the computation has both observers[] (downstream subscribers) and sources[] (upstream signals), it stops a memo from re-emitting downstream when its result is unchanged.
Effects vs render effects vs computed
| Primitive | When it runs | Used by |
|---|---|---|
createComputed |
Synchronously when sources change (no defer) | Internal infrastructure |
createRenderEffect |
Synchronously during the update pass | The compiler (DOM bindings, Portal, Dynamic) |
createEffect |
After the update pass, in a deferred Effects queue |
User code |
createEffect sets c.user = true and is collected by the active <Suspense> if any. createRenderEffect and createComputed do not interact with Suspense.
Resources
createResource is built on top of signals. The ResourceSource is either a value, a falsy sentinel, or a function returning one of those. The ResourceFetcher is (key, info) => T | Promise<T>. The result is a tuple [Resource<T>, { mutate, refetch }].
The Resource<T> shape is a discriminated union (Unresolved | Pending | Ready<T> | Refreshing<T> | Errored). Reading resource() while pending throws to a <Suspense> ancestor (via the SuspenseContext), so the boundary can render its fallback.
Resources also participate in SSR streaming — seroval serializes the resolved value into the streamed HTML and the client's sharedConfig.load(id) rehydrates without re-fetching. See solid-js/web server entry for the streaming side.
Transitions
startTransition(fn) and useTransition() open a TransitionState:
interface TransitionState {
sources: Set<SignalState<any>>;
effects: Computation<any>[];
promises: Set<Promise<any>>;
disposed: Set<Computation<any>>;
queue: Set<Computation<any>>;
scheduler?: (fn: () => void) => unknown;
running: boolean;
done?: Promise<void>;
resolve?: () => void;
}Writes inside the transition are recorded against signal.tValue rather than signal.value. Reads inside affected computations also use tValue. When all tracked promises resolve, the transition commits — tValue becomes value and the queued effects run. This lets you keep the current UI visible while async work completes.
Scheduler
scheduler.ts is a port of React's MessageChannel-based scheduler. enableScheduling() installs it; once enabled, the runtime yields to the host between long update passes via shouldYieldToHost(). The scheduler also detects pending input via navigator.scheduling.isInputPending when available. A recent fix added port1.unref() / port2.unref() so the scheduler does not keep a Node process alive after dispose — see the changeset behind the prevent createDeferred from keeping Node.js process alive commit.
Observable interop
observable(accessor) returns a TC39-Observable-shaped object with a .subscribe(observer) method. RxJS's from() will pick it up via Symbol.observable. from(producer, init?) accepts either a function (setter) => cleanup or any object with .subscribe(...), and turns it into a Solid signal.
mapArray and indexArray
These two are the workhorses behind <For> and <Index>. Both accept an array accessor and a mapping function, and return an accessor that produces a stable, reconciled array of mapped values.
mapArrayis keyed by value. It diffs the new array against the previous one with a Map of indices, reuses the mapped output for items it can match, disposes computations for items that left, and creates new ones for items that arrived. The algorithm is a port of S-array.indexArrayis keyed by position. It updates the underlying signal at each index when the value changes, but never disposes/recreates child computations unless the array length shrinks. Faster but breaks identity when items reorder.
Source / observer registration
sequenceDiagram
participant User as User code
participant ReadSig as readSignal()
participant Listener as active Listener
participant Signal as SignalState
User->>ReadSig: count() inside createEffect
ReadSig->>Listener: read globals
alt Listener present
ReadSig->>Signal: push Listener into observers[]
ReadSig->>Listener: push Signal into sources[]
end
ReadSig->>User: value
User->>WriteSignal: setCount(2)
WriteSignal->>Signal: compare with comparator
alt changed
WriteSignal->>Updates: push every observer
Updates-->>RunUpdates: drained on next tick
endKey source files
| File | Purpose |
|---|---|
packages/solid/src/reactive/signal.ts |
Signals, memos, effects, resources, transitions, contexts, error handling, dev hooks. The whole reactive graph. |
packages/solid/src/reactive/scheduler.ts |
MessageChannel time-slicer; opt-in via enableScheduling(). |
packages/solid/src/reactive/observable.ts |
observable() / from() interop. |
packages/solid/src/reactive/array.ts |
mapArray / indexArray reconcilers. |
packages/solid/src/index.ts |
Re-exports the public surface. |
Integration points
- The reactive runtime is consumed directly by components and control flow (
packages/solid/src/render/); see Components and control flow. solid-js/web(browser) and the SSR entry build their renderer-specific behaviour on top ofcreateRenderEffect,createMemo,createSignal, andgetOwner— seesolid-js/weband SSR entry.solid-js/store(packages/solid/store/) builds proxy-based reactive trees fromcreateSignal,getListener, andbatch— seesolid-js/store.- Server primitives in
packages/solid/src/server/reactive.tsre-implement the same surface as no-op getters/setters for SSR.
Entry points for modification
- Adding a new primitive (e.g.
createObservable2): add the export topackages/solid/src/reactive/signal.ts(or a new file), re-export from the correspondingexport {...}block insignal.tsand frompackages/solid/src/index.ts. Add behaviour tests inpackages/solid/test/signals.spec.tsand a type test inpackages/solid/test/signals.type-tests.ts. - Tuning the update queue / scheduler integration: the
runUpdates,runComputation, andrunEffectsfunctions insignal.tsare the hot path. Be ready to back any change with a benchmark frompackages/solid/test/component.bench.tsorpackages/solid/bench/. - Changing transition semantics:
runTransitionand theTransitionStatemachinery insignal.ts. Validate againstpackages/solid/test/external-source.spec.tsandpackages/solid/web/test/transition.spec.tsx(the most-touched test in the last 90 days).
Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.