@solidjs/signals 0.11.0 → 0.11.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,5 +1,255 @@
1
1
  # @solidjs/signals
2
2
 
3
- Standalone Reactive implementation to serve as the basis of future (post 1.x) versions of SolidJS. This package aims to be core for Signals library made with rendering in mind so it may have more features and opinions than common Signals libraries, but are still necessary core to accomplish the type of capabilities we intend.
3
+ The reactive core that powers [SolidJS 2.0](https://github.com/solidjs/solid). This is a standalone signals library designed for rendering it includes first-class support for async, transitions, optimistic updates, and deeply reactive stores that go beyond what general-purpose signals libraries offer.
4
4
 
5
- This is not ready for production and should be considered pre-alpha. This package is completely experimental and every release may be breaking. It is also not tuned for performance as of yet as we are still focusing on capability.
5
+ > **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.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ npm install @solidjs/signals
11
+ # or
12
+ pnpm add @solidjs/signals
13
+ ```
14
+
15
+ ## Overview
16
+
17
+ `@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.
18
+
19
+ ```typescript
20
+ import { createEffect, createMemo, createRoot, createSignal, flush } from "@solidjs/signals";
21
+
22
+ createRoot(() => {
23
+ const [count, setCount] = createSignal(0);
24
+ const doubled = createMemo(() => count() * 2);
25
+
26
+ createEffect(
27
+ () => doubled(),
28
+ value => {
29
+ console.log("Doubled:", value);
30
+ }
31
+ );
32
+
33
+ setCount(5);
34
+ flush(); // "Doubled: 10"
35
+ });
36
+ ```
37
+
38
+ ### Batched Updates
39
+
40
+ Signal writes are batched — reads after a write won't reflect the new value until `flush()` runs. This prevents glitches and unnecessary recomputation.
41
+
42
+ ```typescript
43
+ const [a, setA] = createSignal(1);
44
+ const [b, setB] = createSignal(2);
45
+
46
+ setA(10);
47
+ setB(20);
48
+ // Neither has updated yet — both writes are batched
49
+ flush(); // Now both update and downstream effects run once
50
+ ```
51
+
52
+ ## Core Primitives
53
+
54
+ ### Signals
55
+
56
+ ```typescript
57
+ const [value, setValue] = createSignal(initialValue, options?);
58
+ ```
59
+
60
+ Reactive state with a getter/setter pair. Supports custom equality via `options.equals`.
61
+
62
+ ### Memos
63
+
64
+ ```typescript
65
+ const derived = createMemo(() => expensive(signal()));
66
+ ```
67
+
68
+ 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.
69
+
70
+ ### Effects
71
+
72
+ ```typescript
73
+ // Two-phase: compute tracks dependencies, effect runs side effects
74
+ createEffect(
75
+ () => count(),
76
+ value => {
77
+ console.log(value);
78
+ }
79
+ );
80
+
81
+ // Render-phase effect (runs before user effects)
82
+ createRenderEffect(
83
+ () => count(),
84
+ value => {
85
+ updateDOM(value);
86
+ }
87
+ );
88
+ ```
89
+
90
+ Effects split tracking from execution. `createEffect` and `createRenderEffect` take a compute function (for tracking) and an effect function (for side effects).
91
+
92
+ ### Writable Memos
93
+
94
+ Pass a function to `createSignal` to get a writable derived value — a memo you can also set:
95
+
96
+ ```typescript
97
+ const [value, setValue] = createSignal(prev => transform(source(), prev), initialValue);
98
+ ```
99
+
100
+ ## Async
101
+
102
+ 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.
103
+
104
+ ```typescript
105
+ const data = createMemo(async () => {
106
+ const response = await fetch(`/api/items?q=${query()}`);
107
+ return response.json();
108
+ });
109
+
110
+ // Check async state
111
+ isPending(data); // true while loading
112
+ latest(data); // last resolved value
113
+ ```
114
+
115
+ Use `action()` to coordinate async workflows with the reactive graph:
116
+
117
+ ```typescript
118
+ const save = action(function* (item) {
119
+ yield fetch("/api/save", { method: "POST", body: JSON.stringify(item) });
120
+ });
121
+ ```
122
+
123
+ ## Optimistic Updates
124
+
125
+ Optimistic signals show an immediate value while async work is pending, then automatically revert when it settles:
126
+
127
+ ```typescript
128
+ const [optimisticCount, setOptimisticCount] = createOptimistic(0);
129
+
130
+ // Immediate UI update — reverts when the async work resolves
131
+ setOptimisticCount(count + 1);
132
+ ```
133
+
134
+ Also available for stores via `createOptimisticStore()`.
135
+
136
+ ## Stores
137
+
138
+ Proxy-based deeply reactive objects with per-property tracking:
139
+
140
+ ```typescript
141
+ import { createStore, reconcile } from "@solidjs/signals";
142
+
143
+ const [store, setStore] = createStore({ todos: [], filter: "all" });
144
+
145
+ // Setter takes a mutating callback — mutations are intercepted by the proxy
146
+ setStore(s => {
147
+ s.filter = "active";
148
+ });
149
+ setStore(s => {
150
+ s.todos.push({ text: "New", done: false });
151
+ });
152
+ setStore(s => {
153
+ s.todos[0].done = true;
154
+ });
155
+
156
+ // Reconcile from server data
157
+ setStore(s => {
158
+ reconcile(serverTodos, "id")(s.todos);
159
+ });
160
+ ```
161
+
162
+ ### Projections
163
+
164
+ Derived stores that transform data reactively:
165
+
166
+ ```typescript
167
+ import { createProjection } from "@solidjs/signals";
168
+
169
+ const filtered = createProjection(
170
+ draft => {
171
+ draft.items = store.todos.filter(t => !t.done);
172
+ },
173
+ { items: [] }
174
+ );
175
+ ```
176
+
177
+ ## Boundaries
178
+
179
+ Intercept async loading and error states in the reactive graph:
180
+
181
+ ```typescript
182
+ import { createErrorBoundary, createLoadBoundary } from "@solidjs/signals";
183
+
184
+ createErrorBoundary(
185
+ () => riskyComputation(),
186
+ (error, reset) => handleError(error)
187
+ );
188
+
189
+ createLoadBoundary(
190
+ () => asyncContent(),
191
+ () => showFallback()
192
+ );
193
+ ```
194
+
195
+ ## Ownership & Context
196
+
197
+ All reactive nodes exist within an **owner** tree that handles disposal and context propagation:
198
+
199
+ ```typescript
200
+ import { createContext, createRoot, getContext, onCleanup, setContext } from "@solidjs/signals";
201
+
202
+ const ThemeContext = createContext("light");
203
+
204
+ createRoot(dispose => {
205
+ setContext(ThemeContext, "dark");
206
+
207
+ createEffect(
208
+ () => getContext(ThemeContext),
209
+ theme => {
210
+ console.log("Theme:", theme);
211
+ }
212
+ );
213
+
214
+ onCleanup(() => console.log("Disposed"));
215
+
216
+ // Call dispose() to tear down the tree
217
+ });
218
+ ```
219
+
220
+ ## Utilities
221
+
222
+ | Function | Description |
223
+ | ------------------------ | ------------------------------------------------------------------ |
224
+ | `flush()` | Process all pending updates |
225
+ | `untrack(fn)` | Run `fn` without tracking dependencies |
226
+ | `isPending(accessor)` | Check if an async accessor is loading |
227
+ | `latest(accessor)` | Get the last resolved value of an async accessor |
228
+ | `refresh(accessor)` | Re-trigger an async computation |
229
+ | `isRefreshing(accessor)` | Check if an async accessor is refreshing |
230
+ | `resolve(fn)` | Returns a promise that resolves when a reactive expression settles |
231
+ | `mapArray(list, mapFn)` | Reactive array mapping with keyed reconciliation |
232
+ | `repeat(count, mapFn)` | Reactive repeat based on a reactive count |
233
+ | `onSettled(fn)` | Run a callback after the current flush cycle completes |
234
+ | `snapshot(store)` | Returns a non-reactive copy of a store, preserving unmodified references |
235
+ | `reconcile(value, key)` | Returns a diffing function for updating stores from new data |
236
+ | `merge(...sources)` | Reactively merges multiple objects/stores, last source wins |
237
+ | `omit(props, ...keys)` | Creates a reactive view of an object with specified keys removed |
238
+ | `deep(store)` | Tracks all nested changes on a store |
239
+ | `storePath(...path)` | Path-based setter for stores as an alternative to mutating callbacks |
240
+
241
+ ## Development
242
+
243
+ ```bash
244
+ pnpm install
245
+ pnpm build # Rollup build (dev/prod/node outputs)
246
+ pnpm test # Run tests
247
+ pnpm test:watch # Watch mode
248
+ pnpm test:gc # Tests with GC exposed
249
+ pnpm bench # Benchmarks
250
+ pnpm format # Prettier + import sorting
251
+ ```
252
+
253
+ ## License
254
+
255
+ MIT
package/dist/dev.js CHANGED
@@ -34,6 +34,7 @@ const REACTIVE_ZOMBIE = 1 << 5;
34
34
  const REACTIVE_DISPOSED = 1 << 6;
35
35
  const REACTIVE_OPTIMISTIC_DIRTY = 1 << 7;
36
36
  const REACTIVE_SNAPSHOT_STALE = 1 << 8;
37
+ const REACTIVE_LAZY = 1 << 9;
37
38
  const STATUS_PENDING = 1 << 0;
38
39
  const STATUS_ERROR = 1 << 1;
39
40
  const STATUS_UNINITIALIZED = 1 << 2;
@@ -392,7 +393,7 @@ function finalizePureQueue(completingTransition = null, incomplete = false) {
392
393
  n._pendingValue = NOT_PENDING;
393
394
  if (n._type && n._type !== EFFECT_TRACKED) n._modified = true;
394
395
  }
395
- n._statusFlags &= ~STATUS_UNINITIALIZED;
396
+ if (!(n._statusFlags & STATUS_PENDING)) n._statusFlags &= ~STATUS_UNINITIALIZED;
396
397
  if (n._fn) GlobalQueue._dispose(n, false, true);
397
398
  }
398
399
  pendingNodes.length = 0;
@@ -1237,7 +1238,7 @@ function computed(fn, initialValue, options) {
1237
1238
  _parent: context,
1238
1239
  _nextSibling: null,
1239
1240
  _firstChild: null,
1240
- _flags: REACTIVE_NONE,
1241
+ _flags: options?.lazy ? REACTIVE_LAZY : REACTIVE_NONE,
1241
1242
  _statusFlags: STATUS_UNINITIALIZED,
1242
1243
  _time: clock,
1243
1244
  _pendingValue: NOT_PENDING,
@@ -1369,10 +1370,20 @@ function read(el) {
1369
1370
  let c = context;
1370
1371
  if (c?._root) c = c._parentComputed;
1371
1372
  if (refreshing && el._fn) recompute(el);
1373
+ if (el._flags & REACTIVE_LAZY) {
1374
+ el._flags &= ~REACTIVE_LAZY;
1375
+ recompute(el, true);
1376
+ }
1377
+ const owner = el._firewall || el;
1378
+ if (strictRead && owner._statusFlags & STATUS_PENDING) {
1379
+ throw new Error(
1380
+ `Reading a pending async value in ${strictRead}. ` +
1381
+ `Async values must be read within a tracking scope (JSX, computations, effects).`
1382
+ );
1383
+ }
1372
1384
  if (c && tracking) {
1373
1385
  if (el._fn && el._flags & REACTIVE_DISPOSED) recompute(el);
1374
1386
  link(el, c);
1375
- const owner = el._firewall || el;
1376
1387
  if (owner._fn) {
1377
1388
  const isZombie = el._flags & REACTIVE_ZOMBIE;
1378
1389
  if (owner._height >= (isZombie ? zombieQueue._min : dirtyQueue._min)) {
@@ -1386,25 +1397,21 @@ function read(el) {
1386
1397
  }
1387
1398
  }
1388
1399
  }
1389
- const asyncCompute = el._firewall || el;
1390
- if (asyncCompute._statusFlags & STATUS_PENDING) {
1391
- if (
1392
- c &&
1393
- !(stale && asyncCompute._transition && activeTransition !== asyncCompute._transition)
1394
- ) {
1400
+ if (owner._statusFlags & STATUS_PENDING) {
1401
+ if (c && !(stale && owner._transition && activeTransition !== owner._transition)) {
1395
1402
  if (currentOptimisticLane) {
1396
- const pendingLane = asyncCompute._optimisticLane;
1403
+ const pendingLane = owner._optimisticLane;
1397
1404
  const lane = findLane(currentOptimisticLane);
1398
- if (pendingLane && findLane(pendingLane) === lane && !hasActiveOverride(asyncCompute)) {
1405
+ if (pendingLane && findLane(pendingLane) === lane && !hasActiveOverride(owner)) {
1399
1406
  if (!tracking) link(el, c);
1400
- throw asyncCompute._error;
1407
+ throw owner._error;
1401
1408
  }
1402
1409
  } else {
1403
1410
  if (!tracking) link(el, c);
1404
- throw asyncCompute._error;
1411
+ throw owner._error;
1405
1412
  }
1406
- } else if (!c && asyncCompute._statusFlags & STATUS_UNINITIALIZED) {
1407
- throw asyncCompute._error;
1413
+ } else if (!c && owner._statusFlags & STATUS_UNINITIALIZED) {
1414
+ throw owner._error;
1408
1415
  }
1409
1416
  }
1410
1417
  if (el._fn && el._statusFlags & STATUS_ERROR) {
@@ -1424,7 +1431,8 @@ function read(el) {
1424
1431
  }
1425
1432
  if (strictRead && !tracking)
1426
1433
  console.warn(
1427
- `Untracked reactive read in ${strictRead}. This value won't update use untrack() if intentional.`
1434
+ `Reactive value read at the top level of ${strictRead} will not update. ` +
1435
+ `Move it into a tracking scope (JSX, computations, effects).`
1428
1436
  );
1429
1437
  return !c ||
1430
1438
  currentOptimisticLane !== null ||
@@ -1445,7 +1453,8 @@ function setSignal(el, v) {
1445
1453
  ? el._value
1446
1454
  : el._pendingValue;
1447
1455
  if (typeof v === "function") v = v(currentValue);
1448
- const valueChanged = !el._equals || !el._equals(currentValue, v);
1456
+ const valueChanged =
1457
+ !el._equals || !el._equals(currentValue, v) || !!(el._statusFlags & STATUS_UNINITIALIZED);
1449
1458
  if (!valueChanged) {
1450
1459
  if (isOptimistic && el._pendingValue !== NOT_PENDING && el._fn) {
1451
1460
  insertSubs(el, true);
@@ -2143,7 +2152,8 @@ const storeTraps = {
2143
2152
  }
2144
2153
  if (strictRead && !tracking && typeof property === "string")
2145
2154
  console.warn(
2146
- `Untracked reactive read in ${strictRead}. This value won't update use untrack() if intentional.`
2155
+ `Reactive value read at the top level of ${strictRead} will not update. ` +
2156
+ `Move it into a tracking scope (JSX, computations, effects).`
2147
2157
  );
2148
2158
  return isWrappable(value) ? wrap(value, target) : value;
2149
2159
  },