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 testsThe 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"| ListenerThe 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) aDataNodefor that property and calls it. If the value is itself wrappable (object/array), it is recursively wrapped in another proxy. If the value is anArray.prototype.<method>(push, splice, …), the call is wrapped in abatchso the mutations notify together.has— registers tracking on a$HASdata node sokey in storeis reactive.set— callssetProperty(target, key, unwrap(value))inside abatch.deleteProperty— callssetProperty(target, key, undefined, true)inside abatch.ownKeys— registers tracking on$SELFsoObject.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:
- Walking common prefix and suffix to skip unchanged children.
- Building a
Mapof new keys to indices (default key:"id"). - For each old item, finding its new index (if any) and reusing its identity.
- For each new item not in the old set, inserting it as a new child.
- 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'sgetListener,batch,createSignal,$PROXY,$TRACK, andDEV— see Reactivity primitives. The Rollup config (packages/solid/rollup.config.js) markssolid-jsasexternalfor the store bundle so it dedupes against the consumer'ssolid-js. - The
<For each={store.array}>integration is implicit: the proxy materialises the array and the$TRACKsymbol gives<For>an opportunity to subscribe to length changes via(newItems as any)[$TRACK]inmapArray/indexArray. See Components and control flow. - The optional
storageoption oncreateResourceaccepts a custom signal-like factory;createStore-backed storage is a common pattern documented inpackages/solid/test/resource.spec.ts.
Entry points for modification
- Adding a new modifier alongside
produce/reconcile: create the helper inpackages/solid/store/src/modifiers.ts, re-export fromindex.ts, mirror an inert version inserver.tsif the modifier could be called during SSR. - Adjusting proxy traps:
proxyTrapsinpackages/solid/store/src/store.tsis the trickiest part of the codebase. Walk throughpackages/solid/store/test/store.spec.tsafter 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, andtest-integrationwill 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.