Open-Source Wikis

/

Solid

/

Packages

/

solid-js/store

solidjs/solid

solid-js/store

Active contributors: Ryan Carniato, Damian Tarnawski

solid-js/store provides reactive trees backed by Proxies. Where createSignal gives you one reactive cell, createStore gives you a (potentially nested) object whose individual properties become reactive on first access.

Purpose

Stores let you write code that looks like it is mutating a plain object — setStore("user", "name", "Alice") — while actually getting fine-grained reactive updates. Each property is backed by a lazily-allocated DataNode (a stripped-down signal). Components that read store.user.name subscribe only to that path; mutating store.user.email does not invalidate them.

Directory layout

packages/solid/store/
├── package.json                # ./store export selector
├── tsconfig.build.json
├── tsconfig.json
├── src/
│   ├── index.ts                # public re-exports + dev hooks
│   ├── store.ts                # createStore, proxy traps, DataNode allocation, unwrap, setProperty
│   ├── mutable.ts              # createMutable, modifyMutable
│   ├── modifiers.ts            # produce, reconcile
│   └── server.ts               # server stubs (no proxies, no signals)
└── test/                       # vitest specs and type tests

The internal package.json files in store/ and store/dist/ exist so that bundlers honour the solid-js/store deep import.

Key abstractions

Symbol File Description
createStore<T>(state, options?) packages/solid/store/src/store.ts Returns [store, setStore]. The store is a Proxy over state. The setter accepts variadic path / value arguments or a function.
unwrap(item) packages/solid/store/src/store.ts Returns the underlying object behind a store proxy (recursively unwraps nested proxies).
Store<T> packages/solid/store/src/store.ts Type alias T. Stores are typed as their underlying shape.
SetStoreFunction<T> packages/solid/store/src/store.ts The variadic setter type. Accepts up to 8 path arguments before the final value or Part<T>-like shape.
Part<T> / StorePathRange / StoreSetter / ArrayFilterFn packages/solid/store/src/store.ts Helper types for the path-based setter signature.
DeepReadonly<T> / DeepMutable<T> packages/solid/store/src/store.ts Recursive readonly/mutable transformations for store types.
NotWrappable packages/solid/store/src/store.ts Union of values that are not wrapped in proxies (primitives, functions, the SolidStore.Unwrappable extension point).
createMutable<T>(state, options?) packages/solid/store/src/mutable.ts Returns a Proxy where assignment (obj.x = 1) triggers reactivity directly — no setter function.
modifyMutable(state, modifier) packages/solid/store/src/mutable.ts Apply a synchronous modifier inside a batch.
produce(fn) packages/solid/store/src/modifiers.ts Immer-style mutation: setStore(produce(s => s.x = 1)).
reconcile(value, options?) packages/solid/store/src/modifiers.ts Diff-and-patch a sub-tree against a new value, preserving identity for unchanged children (key default "id").
ReconcileOptions packages/solid/store/src/modifiers.ts { key?: string | null; merge?: boolean }.
$RAW, $NODE, $HAS, $SELF packages/solid/store/src/store.ts Internal symbols on store nodes. $RAW returns the unwrapped value, $NODE holds per-property DataNodes, $HAS holds has-key tracking signals.
$PROXY, $TRACK re-exported from solid-js $PROXY identifies a store; $TRACK triggers self-tracking on a store node.
DEV.hooks.onStoreNodeUpdate packages/solid/store/src/index.ts Devtool callback fired when a store node changes.

How stores work

graph TD
    User["user code:<br>store.user.name"]
    Proxy["Proxy(target)<br>(get trap)"]
    Nodes["target[$NODE]<br>(per-property DataNodes)"]
    DataNode["DataNode (signal-like)"]
    Subscribe["getNode(nodes, key, value)()"]
    Listener["active Listener"]

    User --> Proxy
    Proxy -->|"key === $RAW"| Raw[returns target]
    Proxy -->|"key === $TRACK"| Track[trackSelf]
    Proxy -->|other key| Nodes
    Nodes --> DataNode
    DataNode --> Subscribe
    Subscribe -->|"if Listener"| Listener

The proxy traps in packages/solid/store/src/store.ts are the centre of the file:

  • get — special-cases $RAW, $PROXY, $TRACK, $NODE, $HAS. For any other key, it allocates (or reuses) a DataNode for that property and calls it. If the value is itself wrappable (object/array), it is recursively wrapped in another proxy. If the value is an Array.prototype.<method> (push, splice, …), the call is wrapped in a batch so the mutations notify together.
  • has — registers tracking on a $HAS data node so key in store is reactive.
  • set — calls setProperty(target, key, unwrap(value)) inside a batch.
  • deleteProperty — calls setProperty(target, key, undefined, true) inside a batch.
  • ownKeys — registers tracking on $SELF so Object.keys(store) is reactive.
  • getOwnPropertyDescriptor — returns a getter/setter pair that round-trips through the proxy.

setProperty (in store.ts) is what actually writes: it updates the underlying object, fires the corresponding DataNode, and triggers $HAS and $SELF listeners as appropriate. In dev it also calls DevHooks.onStoreNodeUpdate.

createMutable vs createStore

createMutable shares the proxy machinery with createStore but uses different set / deleteProperty traps (packages/solid/store/src/mutable.ts):

  • It wraps each setter call in batch(() => setProperty(...)) directly — no separate setter argument.
  • It binds class-style getters/setters to the proxy at wrap time, so class Counter { get value() { return this._v; } set value(v) { this._v = v; } } works transparently.

The trade-off: mutables encourage assignment, which is ergonomic but harder to track at the call site than the explicit setStore(...) form. Solid's docs recommend createStore for most apps.

produce

produce (in packages/solid/store/src/modifiers.ts) wraps the input in a Proxy whose set and deleteProperty go through setProperty directly (no batching, no reactive notifications). The producer pattern lets you write:

setStore(
  produce((s) => {
    s.users.push({ id: 3, name: 'Eve' });
    s.count += 1;
  })
);

…and have all mutations applied transactionally via the outer setStore's batch.

reconcile

reconcile(value, options) (in packages/solid/store/src/modifiers.ts) returns a function that, given the previous store sub-tree, produces an updated tree by:

  1. Walking common prefix and suffix to skip unchanged children.
  2. Building a Map of new keys to indices (default key: "id").
  3. For each old item, finding its new index (if any) and reusing its identity.
  4. For each new item not in the old set, inserting it as a new child.
  5. Truncating the old array if the new one is shorter.

This is what makes setStore("users", reconcile(newUsers)) cheap: existing <For each={store.users}> blocks reuse their per-item DOM and effects when the underlying objects keep their id.

Server build

packages/solid/store/src/server.ts re-implements createStore, createMutable, etc. as plain object operations — assignments mutate the underlying object, no proxies, no signals. This is what gets bundled into the SSR build (selected by the node / deno / worker export conditions in packages/solid/store/package.json).

Key source files

File Purpose
packages/solid/store/src/store.ts The proxy trap implementation, DataNode allocation, unwrap, setProperty, ownKeys reactivity, $RAW / $NODE / $HAS / $SELF symbols.
packages/solid/store/src/mutable.ts createMutable, modifyMutable, the mutable proxy traps.
packages/solid/store/src/modifiers.ts produce (Immer-style) and reconcile (diff/patch).
packages/solid/store/src/server.ts Server-side stubs that drop reactivity.
packages/solid/store/src/index.ts Public re-exports + DEV hooks object.
packages/solid/store/test/store.spec.ts The 1,124-line behaviour suite — the de-facto specification of store semantics.

Integration points

  • Stores depend on solid-js's getListener, batch, createSignal, $PROXY, $TRACK, and DEV — see Reactivity primitives. The Rollup config (packages/solid/rollup.config.js) marks solid-js as external for the store bundle so it dedupes against the consumer's solid-js.
  • The <For each={store.array}> integration is implicit: the proxy materialises the array and the $TRACK symbol gives <For> an opportunity to subscribe to length changes via (newItems as any)[$TRACK] in mapArray / indexArray. See Components and control flow.
  • The optional storage option on createResource accepts a custom signal-like factory; createStore-backed storage is a common pattern documented in packages/solid/test/resource.spec.ts.

Entry points for modification

  • Adding a new modifier alongside produce / reconcile: create the helper in packages/solid/store/src/modifiers.ts, re-export from index.ts, mirror an inert version in server.ts if the modifier could be called during SSR.
  • Adjusting proxy traps: proxyTraps in packages/solid/store/src/store.ts is the trickiest part of the codebase. Walk through packages/solid/store/test/store.spec.ts after every change — many tests cover edge cases (Symbol keys, classes with prototype methods, frozen objects, sparse arrays).
  • Server / browser parity: if you add a public store function, mirror it in server.ts. The 16-bundle Rollup config produces a separate server build of the store, and test-integration will fail if a named export goes missing.

Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.

solid-js/store – Solid wiki | Factory