You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Documents the shallow store option shipped in #2931: single-layer
stores for record-granularity data (root keys reactive, values plain
records replaced by reference), the replacement contract (setter reads
serve the record, in-place mutation is inert, once-plain-always-plain
identity), positional reconcile at the boundary with keyed identity
owned by <For keyed>, projection/optimistic support, and when to
prefer the default deep store. Also updates reconcile's signature
docs: key defaults to "id", null selects positional matching (the 1.x
{ key: null, merge: true } pattern).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Copy file name to clipboardExpand all lines: documentation/solid-2.0/04-stores.md
+34-3Lines changed: 34 additions & 3 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -4,7 +4,7 @@
4
4
5
5
## Summary
6
6
7
-
Solid 2.0’s store layer leans into “mutable draft” ergonomics by default: store setters accept a draft callback (produce-style) and can optionally return a value to perform a shallow replacement/diff. Helper APIs are simplified (`mergeProps` → `merge`, `splitProps` → `omit`), and a new derived-store primitive (`createProjection`, also reachable via `createStore(fn)`) replaces selector-style patterns with a more general “mutate a projection” approach. A `deep()` helper is provided for cases where you need deep observation rather than property-level tracking.
7
+
Solid 2.0’s store layer leans into “mutable draft” ergonomics by default: store setters accept a draft callback (produce-style) and can optionally return a value to perform a shallow replacement/diff. Helper APIs are simplified (`mergeProps` → `merge`, `splitProps` → `omit`), and a new derived-store primitive (`createProjection`, also reachable via `createStore(fn)`) replaces selector-style patterns with a more general “mutate a projection” approach. A `deep()` helper is provided for cases where you need deep observation rather than property-level tracking, and `createStore(value, { shallow: true })` provides a single-layer store for record-granularity data — root keys reactive, values plain records replaced by reference.
### `reconcile(value, key?)` (diffing into stores)
132
132
133
-
`reconcile` returns a diffing function that updates a store (or a nested part of a store) from new data while preserving identity for unchanged entries. The second argument is the key used for identity matching (a string property name or a function).
133
+
`reconcile` returns a diffing function that updates a store (or a nested part of a store) from new data while preserving identity for unchanged entries. The second argument is the key used for identity matching (a string property name or a function). It defaults to `"id"`; pass `null` for **positional** matching — index N of the new array merges into index N of the old, with no keyed diff pass. Positional mode is the classic pattern for fixed-shape data that churns in place (dashboards, monitors), and is what 1.x expressed as `reconcile(v, { key: null, merge: true })` — merge semantics are always on in 2.0.
134
134
135
135
In 2.0 the usage changes from 1.x because setters are now draft-first: you call `reconcile`*inside* the setter callback, targeting the specific part of the draft you want to reconcile.
136
136
@@ -146,6 +146,37 @@ setStore(s => {
146
146
147
147
This pairs naturally with `createProjection`, where returning a value from the derive function uses reconciliation automatically (keyed by `options.key`, default `"id"`).
148
148
149
+
### Shallow stores (`shallow: true`)
150
+
151
+
By default stores track at property level all the way down. `createStore(value, { shallow: true })` creates a **single-layer** store instead: the root's own keys are fully reactive (per-key tracking, membership, enumeration, `length`), while the values under them are **plain records replaced by reference** — no proxies, no tracking, and no deep diffing below the boundary.
Use this when the *record* is the unit of change: rows, entities, or feed items that arrive or update wholesale (server collections, polling dashboards, streamed lists). Deep tracking earns its cost by skipping unchanged leaves; when every leaf of a changed record changes together, that machinery is pure overhead — a shallow store makes ingestion a per-slot reference compare and reads below the boundary plain property access.
161
+
162
+
The contract, stated once: **records are replaced, never edited.**
163
+
164
+
- Reads below the boundary — including inside a setter — hand back the plain record, so read-then-replace, `filter`/`pop` removal idioms, and projection derives all work naturally. Mutating a record in place notifies nothing.
165
+
- Records that pass through a shallow boundary stay plain permanently and present identically through every store (one identity — never wrapped elsewhere). Ingesting a value that is already deep-tracked throws in dev.
166
+
-`reconcile` at the boundary is positional (`reconcile(fresh, null)`). Keyed row identity belongs to the consumer:
167
+
168
+
```js
169
+
<For each={rows} keyed={(row) =>row.id}>
170
+
{(row) =><tr>...{row().name}...</tr>}
171
+
</For>
172
+
```
173
+
174
+
Unchanged slots skip by reference equality (a partial payload that reuses row objects re-renders nothing), same-key replacements update through the row accessor without touching the DOM row, and new/removed keys create and dispose as usual.
175
+
176
+
`shallow` is also accepted by `createProjection` and `createOptimisticStore`. Optimistic writes compose cleanly with the replacement contract: a tentative record replacement stages in the overlay, shows immediately, and reverts to the untouched original — the base records are never mutated.
177
+
178
+
When *not* to use it: state you edit field-by-field (forms, editors, sparse in-place mutation). That is exactly what the default deep store is optimal for — shallow trades leaf granularity for record-granularity throughput, and re-runs all of a record's bindings when the record is replaced.
179
+
149
180
### `snapshot(store)` (replaces `unwrap`)
150
181
151
182
`snapshot(store)` produces a **non-reactive plain value** suitable for serialization or interop with libraries that expect plain objects/arrays.
0 commit comments