@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 +252 -2
- package/dist/dev.js +28 -18
- package/dist/node.cjs +520 -516
- package/dist/prod.js +141 -137
- package/dist/types/core/constants.d.ts +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,5 +1,255 @@
|
|
|
1
1
|
# @solidjs/signals
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1390
|
-
|
|
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 =
|
|
1403
|
+
const pendingLane = owner._optimisticLane;
|
|
1397
1404
|
const lane = findLane(currentOptimisticLane);
|
|
1398
|
-
if (pendingLane && findLane(pendingLane) === lane && !hasActiveOverride(
|
|
1405
|
+
if (pendingLane && findLane(pendingLane) === lane && !hasActiveOverride(owner)) {
|
|
1399
1406
|
if (!tracking) link(el, c);
|
|
1400
|
-
throw
|
|
1407
|
+
throw owner._error;
|
|
1401
1408
|
}
|
|
1402
1409
|
} else {
|
|
1403
1410
|
if (!tracking) link(el, c);
|
|
1404
|
-
throw
|
|
1411
|
+
throw owner._error;
|
|
1405
1412
|
}
|
|
1406
|
-
} else if (!c &&
|
|
1407
|
-
throw
|
|
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
|
-
`
|
|
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 =
|
|
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
|
-
`
|
|
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
|
},
|