reazor-react 0.0.1
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/LICENSE +21 -0
- package/README.md +582 -0
- package/build/core/batch.d.ts +7 -0
- package/build/core/effectRuntime.d.ts +7 -0
- package/build/core/effects.d.ts +5 -0
- package/build/core/index.d.ts +6 -0
- package/build/core/listeners.d.ts +2 -0
- package/build/core/proxy.d.ts +4 -0
- package/build/core/tracking.d.ts +3 -0
- package/build/index.cjs.js +1 -0
- package/build/index.d.ts +3 -0
- package/build/index.esm.js +1 -0
- package/build/react/hooks.d.ts +6 -0
- package/build/react/index.d.ts +1 -0
- package/build/reaxor/computed.d.ts +8 -0
- package/build/reaxor/createReactive.d.ts +2 -0
- package/build/reaxor/index.d.ts +3 -0
- package/build/reaxor/types.d.ts +73 -0
- package/package.json +86 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Delpi.Kye
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,582 @@
|
|
|
1
|
+
# ⚛️⚡ reazor-react
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/reazor-react)
|
|
4
|
+

|
|
5
|
+
|
|
6
|
+
<a href="https://codesandbox.io/p/sandbox/85f9pd" target="_blank">LIVE EXAMPLE</a>
|
|
7
|
+
|
|
8
|
+
`reazor-react` brings Solid-style fine-grained reactivity to React 18+.
|
|
9
|
+
|
|
10
|
+
It provides:
|
|
11
|
+
|
|
12
|
+
* Deep Proxy-based reactivity
|
|
13
|
+
* Path-level rendering updates
|
|
14
|
+
* Sync computed (derived) values
|
|
15
|
+
* Batched updates
|
|
16
|
+
* Path-based & auto-tracked effects
|
|
17
|
+
* Map / Set / Array support
|
|
18
|
+
* Fully type-safe inference
|
|
19
|
+
* Concurrent-safe React binding
|
|
20
|
+
|
|
21
|
+
> Fine-grained reactive state without atoms or reducers.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## ✨ Why reazor-react?
|
|
26
|
+
|
|
27
|
+
Unlike reducer or atom-based stores, `reazor-react`:
|
|
28
|
+
|
|
29
|
+
✅ Mutates state directly
|
|
30
|
+
✅ Tracks property access automatically
|
|
31
|
+
✅ Re-renders only components that access changed paths
|
|
32
|
+
✅ Supports deeply nested Map / Set / Array
|
|
33
|
+
✅ Supports sync computed
|
|
34
|
+
✅ Works in React 18 concurrent mode
|
|
35
|
+
✅ Batches updates to reduce redundant renders
|
|
36
|
+
|
|
37
|
+
No boilerplate. No selectors required. No atoms.
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## 🧠 Mental Model
|
|
42
|
+
|
|
43
|
+
```txt
|
|
44
|
+
Component render
|
|
45
|
+
↓
|
|
46
|
+
Proxy tracks accessed paths
|
|
47
|
+
↓
|
|
48
|
+
Mutation triggers exact listeners
|
|
49
|
+
↓
|
|
50
|
+
Computed values updated
|
|
51
|
+
↓
|
|
52
|
+
Only affected components re-render
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Core principle:
|
|
56
|
+
|
|
57
|
+
> Property access defines dependency automatically.
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## 📦 Installation
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
npm install reazor-react
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## 🚀 Quick Start
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
import { createReactive } from "reazor-react"
|
|
73
|
+
|
|
74
|
+
const counter = createReactive({
|
|
75
|
+
count: 0,
|
|
76
|
+
todos: []
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
// define computed
|
|
80
|
+
counter.computed("double", s => s.count * 2, {
|
|
81
|
+
cache: true
|
|
82
|
+
})
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### React Usage
|
|
86
|
+
|
|
87
|
+
```tsx
|
|
88
|
+
function Counter() {
|
|
89
|
+
const state = counter.use()
|
|
90
|
+
|
|
91
|
+
return (
|
|
92
|
+
<div>
|
|
93
|
+
<h2>{state.count}</h2>
|
|
94
|
+
<button onClick={() => state.count++}>+</button>
|
|
95
|
+
</div>
|
|
96
|
+
)
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
✔ No dispatch
|
|
101
|
+
✔ No reducer
|
|
102
|
+
✔ No setState
|
|
103
|
+
✔ Direct mutation
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## 🎯 useSelector (Ultra Fine-Grained Rendering)
|
|
108
|
+
|
|
109
|
+
```tsx
|
|
110
|
+
function CountOnly() {
|
|
111
|
+
const count = counter.useSelector(s => s.count)
|
|
112
|
+
return <h2>{count}</h2>
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
✔ Only re-renders when `count` changes
|
|
117
|
+
✔ Does NOT re-render on unrelated state changes
|
|
118
|
+
✔ No selector memo needed
|
|
119
|
+
✔ Auto dependency tracking
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
## 🎯 useComputed (Ultra Fine-Grained Rendering for Computed)
|
|
124
|
+
|
|
125
|
+
```tsx
|
|
126
|
+
function Double() {
|
|
127
|
+
const double = counter.useComputed(s => s.double)
|
|
128
|
+
return <h2>{double}</h2>
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
✔ Only re-renders when dependencies of double change
|
|
133
|
+
✔ Does NOT re-render on unrelated state changes
|
|
134
|
+
✔ No memo required
|
|
135
|
+
✔ Uses computed dependency graph
|
|
136
|
+
✔ Supports nested computed
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
# 🧩 Examples
|
|
141
|
+
|
|
142
|
+
## 1️⃣ Fine-Grained Multiple Components
|
|
143
|
+
|
|
144
|
+
```ts
|
|
145
|
+
const store = createReactive({
|
|
146
|
+
count: 0,
|
|
147
|
+
name: "Vu"
|
|
148
|
+
})
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
```tsx
|
|
152
|
+
function CountView() {
|
|
153
|
+
const state = store.use()
|
|
154
|
+
return <h2>{state.count}</h2>
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function NameView() {
|
|
158
|
+
const state = store.use()
|
|
159
|
+
return <h2>{state.name}</h2>
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function Controls() {
|
|
163
|
+
const state = store.use()
|
|
164
|
+
return (
|
|
165
|
+
<>
|
|
166
|
+
<button onClick={() => state.count++}>+</button>
|
|
167
|
+
<button onClick={() => (state.name = "Tung")}>Rename</button>
|
|
168
|
+
</>
|
|
169
|
+
)
|
|
170
|
+
}
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Changing `count` does NOT re-render `NameView`.
|
|
174
|
+
|
|
175
|
+
Changing `name` does NOT re-render `CountView`.
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
## 2️⃣ Deep Nested Reactivity
|
|
180
|
+
|
|
181
|
+
```ts
|
|
182
|
+
const store = createReactive({
|
|
183
|
+
user: {
|
|
184
|
+
profile: {
|
|
185
|
+
name: "Vu",
|
|
186
|
+
age: 28
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
})
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
```tsx
|
|
193
|
+
function Profile() {
|
|
194
|
+
const state = store.use()
|
|
195
|
+
|
|
196
|
+
return (
|
|
197
|
+
<div>
|
|
198
|
+
<h3>{state.user.profile.name}</h3>
|
|
199
|
+
<button onClick={() => state.user.profile.age++}>
|
|
200
|
+
Age++
|
|
201
|
+
</button>
|
|
202
|
+
</div>
|
|
203
|
+
)
|
|
204
|
+
}
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
Only components accessing `user.profile.age` re-render.
|
|
208
|
+
|
|
209
|
+
---
|
|
210
|
+
|
|
211
|
+
## 3️⃣ Map / Set Support
|
|
212
|
+
|
|
213
|
+
```ts
|
|
214
|
+
const store = createReactive({
|
|
215
|
+
settings: new Map([["theme", "light"]]),
|
|
216
|
+
tags: new Set(["react"])
|
|
217
|
+
})
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
```tsx
|
|
221
|
+
function ThemeToggle() {
|
|
222
|
+
const state = store.use()
|
|
223
|
+
const theme = state.settings.get("theme")
|
|
224
|
+
|
|
225
|
+
return (
|
|
226
|
+
<button
|
|
227
|
+
onClick={() =>
|
|
228
|
+
state.settings.set(
|
|
229
|
+
"theme",
|
|
230
|
+
theme === "light" ? "dark" : "light"
|
|
231
|
+
)
|
|
232
|
+
}
|
|
233
|
+
>
|
|
234
|
+
Theme: {theme}
|
|
235
|
+
</button>
|
|
236
|
+
)
|
|
237
|
+
}
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
Mutating `Map` or `Set` triggers exact listeners.
|
|
241
|
+
|
|
242
|
+
---
|
|
243
|
+
|
|
244
|
+
## 4️⃣ Transaction (Batch Multiple Mutations)
|
|
245
|
+
|
|
246
|
+
```ts
|
|
247
|
+
store.transaction(() => {
|
|
248
|
+
store.state.count++
|
|
249
|
+
store.state.count++
|
|
250
|
+
store.state.count++
|
|
251
|
+
})
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
Only ONE render triggered.
|
|
255
|
+
|
|
256
|
+
Perfect for:
|
|
257
|
+
|
|
258
|
+
* Form updates
|
|
259
|
+
* Bulk mutations
|
|
260
|
+
* Async merges
|
|
261
|
+
|
|
262
|
+
---
|
|
263
|
+
|
|
264
|
+
## 5️⃣ Computed (Sync)
|
|
265
|
+
|
|
266
|
+
```ts
|
|
267
|
+
const store = createReactive({
|
|
268
|
+
price: 10,
|
|
269
|
+
quantity: 2
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
store.computed("total", (s) => s.price * s.quantity, {
|
|
273
|
+
cache: true
|
|
274
|
+
})
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
```tsx
|
|
278
|
+
function Total() {
|
|
279
|
+
const state = store.use()
|
|
280
|
+
return <h3>Total: {state.total}</h3>
|
|
281
|
+
}
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
✔ Recomputed only when dependencies change
|
|
285
|
+
✔ Supports nested computed
|
|
286
|
+
✔ Topological scheduling
|
|
287
|
+
✔ No stale reads
|
|
288
|
+
|
|
289
|
+
---
|
|
290
|
+
|
|
291
|
+
### Dynamic Dependency Tracking
|
|
292
|
+
|
|
293
|
+
```ts
|
|
294
|
+
store.computed("displayName", (s) => {
|
|
295
|
+
return s.user.loggedIn
|
|
296
|
+
? s.user.profile.name
|
|
297
|
+
: "Guest"
|
|
298
|
+
}, { cache: true })
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
Behavior:
|
|
302
|
+
|
|
303
|
+
- Initially tracks `user.loggedIn`
|
|
304
|
+
- If `loggedIn` becomes true → also tracks `user.profile.name`
|
|
305
|
+
- If `loggedIn` becomes false → removes profile dependency
|
|
306
|
+
|
|
307
|
+
---
|
|
308
|
+
|
|
309
|
+
### Nested Computed Graph
|
|
310
|
+
|
|
311
|
+
```ts
|
|
312
|
+
store.computed("subtotal", s => s.price * s.qty, { cache: true })
|
|
313
|
+
store.computed("tax", s => s.subtotal * 0.1, { cache: true })
|
|
314
|
+
store.computed("total", s => s.subtotal + s.tax, { cache: true })
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
Execution order:
|
|
318
|
+
|
|
319
|
+
```
|
|
320
|
+
price/qty change
|
|
321
|
+
↓
|
|
322
|
+
subtotal recomputed
|
|
323
|
+
↓
|
|
324
|
+
tax recomputed
|
|
325
|
+
↓
|
|
326
|
+
total recomputed
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
---
|
|
330
|
+
|
|
331
|
+
## 6️⃣ Effects
|
|
332
|
+
|
|
333
|
+
Two types:
|
|
334
|
+
|
|
335
|
+
- **Path-based effects**
|
|
336
|
+
- **Auto-tracked effects**
|
|
337
|
+
|
|
338
|
+
---
|
|
339
|
+
|
|
340
|
+
### Path-Based Effect
|
|
341
|
+
|
|
342
|
+
```ts
|
|
343
|
+
store.effect("count", (newValue, oldValue) => {
|
|
344
|
+
console.log("Count changed:", oldValue, "→", newValue)
|
|
345
|
+
})
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
Supports deep paths:
|
|
349
|
+
|
|
350
|
+
```ts
|
|
351
|
+
store.effect("todos.0.done", (newValue, oldValue) => {
|
|
352
|
+
console.log("Todo #0 changed:", oldValue, "→", newValue)
|
|
353
|
+
})
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
Best for:
|
|
357
|
+
|
|
358
|
+
- Logging
|
|
359
|
+
- Analytics
|
|
360
|
+
- Watching specific field
|
|
361
|
+
|
|
362
|
+
---
|
|
363
|
+
|
|
364
|
+
### Auto Effect (Fine-Grained)
|
|
365
|
+
|
|
366
|
+
```ts
|
|
367
|
+
store.autoEffect(() => {
|
|
368
|
+
console.log("Count is:", store.state.count)
|
|
369
|
+
})
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
Automatically tracks dependencies accessed during execution.
|
|
373
|
+
|
|
374
|
+
---
|
|
375
|
+
|
|
376
|
+
### Dynamic Dependencies
|
|
377
|
+
|
|
378
|
+
```ts
|
|
379
|
+
store.autoEffect(() => {
|
|
380
|
+
if (store.state.user.loggedIn) {
|
|
381
|
+
console.log(store.state.user.profile.name)
|
|
382
|
+
}
|
|
383
|
+
})
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
Dependencies adjust automatically when conditions change.
|
|
387
|
+
|
|
388
|
+
---
|
|
389
|
+
|
|
390
|
+
### Cleanup Support
|
|
391
|
+
|
|
392
|
+
```ts
|
|
393
|
+
store.autoEffect(() => {
|
|
394
|
+
const id = setInterval(() => {
|
|
395
|
+
console.log(store.state.count)
|
|
396
|
+
}, 1000)
|
|
397
|
+
|
|
398
|
+
return () => clearInterval(id)
|
|
399
|
+
})
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
Previous cleanup runs before re-execution.
|
|
403
|
+
|
|
404
|
+
---
|
|
405
|
+
|
|
406
|
+
### Infinite Loop Protection
|
|
407
|
+
|
|
408
|
+
```ts
|
|
409
|
+
store.autoEffect(() => {
|
|
410
|
+
if (store.state.count < 5) {
|
|
411
|
+
store.state.count++
|
|
412
|
+
}
|
|
413
|
+
})
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
Scheduler prevents runaway recursion.
|
|
417
|
+
|
|
418
|
+
---
|
|
419
|
+
|
|
420
|
+
## 7️⃣ Large List Performance
|
|
421
|
+
|
|
422
|
+
```ts
|
|
423
|
+
const store = createReactive({
|
|
424
|
+
items: Array.from({ length: 1000 }, (_, i) => ({
|
|
425
|
+
id: i,
|
|
426
|
+
done: false
|
|
427
|
+
}))
|
|
428
|
+
})
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
```tsx
|
|
432
|
+
function List() {
|
|
433
|
+
const state = store.use()
|
|
434
|
+
|
|
435
|
+
return (
|
|
436
|
+
<ul>
|
|
437
|
+
{state.items.map(item => (
|
|
438
|
+
<Item key={item.id} item={item} />
|
|
439
|
+
))}
|
|
440
|
+
</ul>
|
|
441
|
+
)
|
|
442
|
+
}
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
Only modified item re-renders.
|
|
446
|
+
|
|
447
|
+
---
|
|
448
|
+
|
|
449
|
+
# 🧠 Advanced Internals
|
|
450
|
+
|
|
451
|
+
### Dependency Graph
|
|
452
|
+
- Computed tracks exact dependencies
|
|
453
|
+
- Nested computed supported
|
|
454
|
+
- Minimal invalidation graph
|
|
455
|
+
|
|
456
|
+
### Batched Updates
|
|
457
|
+
- All mutations inside same tick batched
|
|
458
|
+
- Transaction API supported
|
|
459
|
+
- Prevents render thrashing
|
|
460
|
+
|
|
461
|
+
---
|
|
462
|
+
|
|
463
|
+
# 🔍 Comparison
|
|
464
|
+
|
|
465
|
+
| Criteria | ⚡ reazor-react | 🧰 Redux Toolkit | 🐻 Zustand | 🧠 MobX |
|
|
466
|
+
| ------------------------------ | ---------------- | ----------------| ---------- | ------- |
|
|
467
|
+
| Fine-grained rendering | ✅ | ❌ | ❌ | ✅ |
|
|
468
|
+
| Direct mutation | ✅ | ❌ | ✅ | ✅ |
|
|
469
|
+
| Deep Map / Set support | ✅ | ❌ | ❌ | ⚠️ |
|
|
470
|
+
| React 18 concurrent-safe | ✅ | ✅ | ✅ | ⚠️ |
|
|
471
|
+
| Automatic batching | ✅ | ❌ | ⚠️ | ⚠️ |
|
|
472
|
+
| Reducer-free | ✅ | ❌ | ✅ | ❌ |
|
|
473
|
+
|
|
474
|
+
---
|
|
475
|
+
|
|
476
|
+
# 🖼 Reactive Flow Diagram
|
|
477
|
+
|
|
478
|
+
```txt
|
|
479
|
+
┌─────────────┐
|
|
480
|
+
│ Component │
|
|
481
|
+
│ Render │
|
|
482
|
+
└─────┬───────┘
|
|
483
|
+
│ Access paths
|
|
484
|
+
▼
|
|
485
|
+
┌─────────────┐
|
|
486
|
+
│ Proxy │
|
|
487
|
+
│ Tracks deps │
|
|
488
|
+
└─────┬───────┘
|
|
489
|
+
│ Mutate
|
|
490
|
+
▼
|
|
491
|
+
┌─────────────┐
|
|
492
|
+
│ Computed │
|
|
493
|
+
│ Sync │
|
|
494
|
+
└─────┬───────┘
|
|
495
|
+
│ Batched triggers
|
|
496
|
+
▼
|
|
497
|
+
┌─────────────┐
|
|
498
|
+
│ Listener │
|
|
499
|
+
│ Re-render │
|
|
500
|
+
└─────────────┘
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
---
|
|
504
|
+
|
|
505
|
+
# 🧪 Testing
|
|
506
|
+
|
|
507
|
+
You can test:
|
|
508
|
+
|
|
509
|
+
* Direct mutations
|
|
510
|
+
* Computed derivations
|
|
511
|
+
* Transaction batching
|
|
512
|
+
* Nested structures
|
|
513
|
+
* Map / Set behavior
|
|
514
|
+
* Effects
|
|
515
|
+
|
|
516
|
+
All without mocking React.
|
|
517
|
+
|
|
518
|
+
## Basic Reactivity
|
|
519
|
+
|
|
520
|
+
```ts
|
|
521
|
+
import { createReactive } from "reazor-react"
|
|
522
|
+
|
|
523
|
+
const store = createReactive({ count: 0 })
|
|
524
|
+
|
|
525
|
+
store.state.count++
|
|
526
|
+
|
|
527
|
+
expect(store.state.count).toBe(1)
|
|
528
|
+
```
|
|
529
|
+
|
|
530
|
+
---
|
|
531
|
+
|
|
532
|
+
## Computed
|
|
533
|
+
|
|
534
|
+
```ts
|
|
535
|
+
const store = createReactive({
|
|
536
|
+
price: 10,
|
|
537
|
+
qty: 2
|
|
538
|
+
})
|
|
539
|
+
|
|
540
|
+
store.computed("total", s => s.price * s.qty, { cache: true })
|
|
541
|
+
|
|
542
|
+
expect(store.state.total).toBe(20)
|
|
543
|
+
|
|
544
|
+
store.state.qty = 3
|
|
545
|
+
|
|
546
|
+
expect(store.state.total).toBe(30)
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
---
|
|
550
|
+
|
|
551
|
+
## Transaction
|
|
552
|
+
|
|
553
|
+
```ts
|
|
554
|
+
store.transaction(() => {
|
|
555
|
+
store.state.count++
|
|
556
|
+
store.state.count++
|
|
557
|
+
})
|
|
558
|
+
|
|
559
|
+
expect(store.state.count).toBe(2)
|
|
560
|
+
```
|
|
561
|
+
|
|
562
|
+
---
|
|
563
|
+
|
|
564
|
+
## Effect
|
|
565
|
+
|
|
566
|
+
```ts
|
|
567
|
+
let triggered = false
|
|
568
|
+
|
|
569
|
+
store.effect("count", () => {
|
|
570
|
+
triggered = true
|
|
571
|
+
})
|
|
572
|
+
|
|
573
|
+
store.state.count++
|
|
574
|
+
|
|
575
|
+
expect(triggered).toBe(true)
|
|
576
|
+
```
|
|
577
|
+
|
|
578
|
+
---
|
|
579
|
+
|
|
580
|
+
# 📜 License
|
|
581
|
+
|
|
582
|
+
MIT
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Listener } from "../reaxor/types";
|
|
2
|
+
export declare function createBatch(): {
|
|
3
|
+
add: (path: string) => void;
|
|
4
|
+
schedule: (notify: (key: string) => void) => void;
|
|
5
|
+
transaction: (fn: Listener, notify: (key: string) => void) => void;
|
|
6
|
+
getVersion: () => number;
|
|
7
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { EffectOptions, ListenerManager } from "../reaxor";
|
|
2
|
+
type EffectFn = () => void | (() => void);
|
|
3
|
+
export declare function createEffectRuntime(listeners: ListenerManager): {
|
|
4
|
+
effect: (fn: EffectFn, options?: EffectOptions) => () => void;
|
|
5
|
+
batch: (fn: () => void) => void;
|
|
6
|
+
};
|
|
7
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";var e=require("react");function t(){const e=new Set;let t=!1,n=0,r=!1,o=!1;function c(r){n++;for(const t of e)r(t);e.clear(),t=!1}return{add:function(t){e.add(t),e.add("__root__")},schedule:function(e){r?o=!0:t||(t=!0,queueMicrotask(()=>c(e)))},transaction:function(e,t){r=!0,e(),r=!1,o&&(o=!1,c(t))},getVersion:()=>n}}let n=null;function r(){return n=new Set,n}function o(){n=null}function c(e){n&&n.add(e)}function u(e){const t=new Set;let n=!1,c=0;function u(){n||(n=!0,queueMicrotask(s))}function s(){for(;t.size;){const e=Array.from(t).sort(i);t.clear();for(const t of e)f(t)}n=!1}function i(e,t){return e.depth!==t.depth?e.depth-t.depth:e.priority-t.priority}function f(t){if(t.disposed)return;if(t.running)return;if(t.runCount>100)return;t.runCount++,t.running=!0,t.deps.forEach(n=>{e.remove(n,t.scheduler)}),t.cleanup&&(t.cleanup(),t.cleanup=void 0);const n=r(),c=t.fn();o(),t.deps=n,t.deps.forEach(n=>{e.add(n,t.scheduler)}),"function"==typeof c&&(t.cleanup=c),t.running=!1}return{effect:function(r,o){const s={fn:r,deps:new Set,depth:o?.depth??100,priority:o?.priority??10,running:!1,runCount:0,disposed:!1,scheduler:()=>function(e){e.disposed||(e.sync?f(e):(t.add(e),n||0!==c||u()))}(s),sync:o?.sync??!1};return f(s),()=>{s.disposed||(s.disposed=!0,s.deps.forEach(t=>{e.remove(t,s.scheduler)}),s.cleanup&&s.cleanup(),s.deps.clear())}},batch:function(e){c++;try{e()}finally{c--,0===c&&u()}}}}function s(){const e=new Map;return{register:function(t,n){let r=e.get(t);return r||(r=new Set,e.set(t,r)),r.add(n),()=>{const r=e.get(t);r&&(r.delete(n),0===r.size&&e.delete(t))}},run:function(t,n,r){const o=e.get(t);o&&[...o].forEach(e=>e(n,r,t))}}}function i(){const e=new Map;return{add:function(t,n){let r=e.get(t);r||(r=new Set,e.set(t,r)),r.add(n)},remove:function(t,n){e.get(t)?.delete(n)},notify:function(t){e.get(t)?.forEach(e=>e())}}}function f(e){return"symbol"!=typeof e&&("__proto__"!==e&&"constructor"!==e)}function a(e){const{root:t,listeners:n,batch:r,effects:o,computed:u}=e,s=new WeakMap;function i(e,t,c){r.add(e),r.schedule(n.notify),o.run(e,c,t),u.invalidateComputed(e,e=>{r.add(e)})}const a=function e(t,n,r=""){if("object"!=typeof(o=t)||null===o)return t;var o;if(s.has(t))return s.get(t);const d=new Proxy(t,{get(t,o,s){if(!f(o))return Reflect.get(t,o,s);const d=r?`${r}.${o+""}`:o+"";if(""===r&&"string"==typeof o&&u.computedMap.has(o)){const e=u.computedMap.get(o);return u.evaluateComputed(e,a)}u.trackDependency(d),c(d);const p=Reflect.get(t,o,s);if(Array.isArray(t)&&"function"==typeof p){if(["push","pop","splice","shift","unshift","sort","reverse"].includes(o))return(...e)=>{const n=t.slice(),o=p.apply(t,e);return i(r,n,t),o}}return e(p,n,d)},set(e,t,n,o){if(!f(t))return Reflect.set(e,t,n,o);const c=r?`${r}.${t+""}`:t+"",u=e[t];if(Object.is(u,n))return!0;const s=Reflect.set(e,t,n,o);return i(c,u,n),s}});return s.set(t,d),d}(t);return{rootProxy:a}}function d(t,n,r){const o="__root__";function c(e){return n.add(o,e),()=>n.remove(o,e)}function u(n){const r=e.useRef();return e.useSyncExternalStore(c,()=>{const e=n(t);return Object.is(r.current,e)?r.current:(r.current=e,e)},()=>n(t))}return{useReactive:function(){return e.useSyncExternalStore(c,r,r),t},useSelector:u,useComputed:function(e){return u(e)}}}function p(){const e=new Map,t=new Map;let n=null;function r(e,t){n=e;const r=e.getter(t);return n=null,r}return{computedMap:e,register:function(t,n,r){if(e.has(t))throw Error(`Computed key "${t}" already exists`);const o={key:t,getter:n,cache:r?.cache??!1,value:void 0,dirty:!0,deps:new Set};e.set(t,o)},trackDependency:function(e){if(!n)return;n.deps.add(e);let r=t.get(e);r||(r=new Set,t.set(e,r)),r.add(n)},invalidateComputed:function(e,n){const r=t.get(e);if(r)for(const e of r)e.cache&&(e.dirty=!0),n(e.key)},evaluateComputed:function(e,n){if(!e.cache)return r(e,n);if(!e.dirty)return e.value;e.deps.forEach(n=>{t.get(n)?.delete(e)}),e.deps.clear();const o=r(e,n);return e.value=o,e.dirty=!1,e.value}}}exports.createBatch=t,exports.createComputed=p,exports.createEffectRuntime=u,exports.createEffects=s,exports.createListeners=i,exports.createProxy=a,exports.createReactHooks=d,exports.createReactive=function(e){const n=i(),r=t(),o=s(),c=p(),{rootProxy:f}=a({root:e,listeners:n,batch:r,effects:o,computed:c}),{useReactive:l,useSelector:y}=d(f,n,r.getVersion),h=u(n);return{state:f,use:l,useSelector:y,effect:o.register,autoEffect:h.effect,computed:c.register,transaction:e=>r.transaction(e,n.notify),getVersion:r.getVersion}},exports.startTracking=r,exports.stopTracking=o,exports.track=c;
|
package/build/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{useRef as e,useSyncExternalStore as t}from"react";function n(){const e=new Set;let t=!1,n=0,r=!1,o=!1;function c(r){n++;for(const t of e)r(t);e.clear(),t=!1}return{add:function(t){e.add(t),e.add("__root__")},schedule:function(e){r?o=!0:t||(t=!0,queueMicrotask(()=>c(e)))},transaction:function(e,t){r=!0,e(),r=!1,o&&(o=!1,c(t))},getVersion:()=>n}}let r=null;function o(){return r=new Set,r}function c(){r=null}function u(e){r&&r.add(e)}function i(e){const t=new Set;let n=!1,r=0;function u(){n||(n=!0,queueMicrotask(i))}function i(){for(;t.size;){const e=Array.from(t).sort(s);t.clear();for(const t of e)f(t)}n=!1}function s(e,t){return e.depth!==t.depth?e.depth-t.depth:e.priority-t.priority}function f(t){if(t.disposed)return;if(t.running)return;if(t.runCount>100)return;t.runCount++,t.running=!0,t.deps.forEach(n=>{e.remove(n,t.scheduler)}),t.cleanup&&(t.cleanup(),t.cleanup=void 0);const n=o(),r=t.fn();c(),t.deps=n,t.deps.forEach(n=>{e.add(n,t.scheduler)}),"function"==typeof r&&(t.cleanup=r),t.running=!1}return{effect:function(o,c){const i={fn:o,deps:new Set,depth:c?.depth??100,priority:c?.priority??10,running:!1,runCount:0,disposed:!1,scheduler:()=>function(e){e.disposed||(e.sync?f(e):(t.add(e),n||0!==r||u()))}(i),sync:c?.sync??!1};return f(i),()=>{i.disposed||(i.disposed=!0,i.deps.forEach(t=>{e.remove(t,i.scheduler)}),i.cleanup&&i.cleanup(),i.deps.clear())}},batch:function(e){r++;try{e()}finally{r--,0===r&&u()}}}}function s(){const e=new Map;return{register:function(t,n){let r=e.get(t);return r||(r=new Set,e.set(t,r)),r.add(n),()=>{const r=e.get(t);r&&(r.delete(n),0===r.size&&e.delete(t))}},run:function(t,n,r){const o=e.get(t);o&&[...o].forEach(e=>e(n,r,t))}}}function f(){const e=new Map;return{add:function(t,n){let r=e.get(t);r||(r=new Set,e.set(t,r)),r.add(n)},remove:function(t,n){e.get(t)?.delete(n)},notify:function(t){e.get(t)?.forEach(e=>e())}}}function d(e){return"symbol"!=typeof e&&("__proto__"!==e&&"constructor"!==e)}function a(e){const{root:t,listeners:n,batch:r,effects:o,computed:c}=e,i=new WeakMap;function s(e,t,u){r.add(e),r.schedule(n.notify),o.run(e,u,t),c.invalidateComputed(e,e=>{r.add(e)})}const f=function e(t,n,r=""){if("object"!=typeof(o=t)||null===o)return t;var o;if(i.has(t))return i.get(t);const a=new Proxy(t,{get(t,o,i){if(!d(o))return Reflect.get(t,o,i);const a=r?`${r}.${o+""}`:o+"";if(""===r&&"string"==typeof o&&c.computedMap.has(o)){const e=c.computedMap.get(o);return c.evaluateComputed(e,f)}c.trackDependency(a),u(a);const p=Reflect.get(t,o,i);if(Array.isArray(t)&&"function"==typeof p){if(["push","pop","splice","shift","unshift","sort","reverse"].includes(o))return(...e)=>{const n=t.slice(),o=p.apply(t,e);return s(r,n,t),o}}return e(p,n,a)},set(e,t,n,o){if(!d(t))return Reflect.set(e,t,n,o);const c=r?`${r}.${t+""}`:t+"",u=e[t];if(Object.is(u,n))return!0;const i=Reflect.set(e,t,n,o);return s(c,u,n),i}});return i.set(t,a),a}(t);return{rootProxy:f}}function p(n,r,o){const c="__root__";function u(e){return r.add(c,e),()=>r.remove(c,e)}function i(r){const o=e();return t(u,()=>{const e=r(n);return Object.is(o.current,e)?o.current:(o.current=e,e)},()=>r(n))}return{useReactive:function(){return t(u,o,o),n},useSelector:i,useComputed:function(e){return i(e)}}}function l(){const e=new Map,t=new Map;let n=null;function r(e,t){n=e;const r=e.getter(t);return n=null,r}return{computedMap:e,register:function(t,n,r){if(e.has(t))throw Error(`Computed key "${t}" already exists`);const o={key:t,getter:n,cache:r?.cache??!1,value:void 0,dirty:!0,deps:new Set};e.set(t,o)},trackDependency:function(e){if(!n)return;n.deps.add(e);let r=t.get(e);r||(r=new Set,t.set(e,r)),r.add(n)},invalidateComputed:function(e,n){const r=t.get(e);if(r)for(const e of r)e.cache&&(e.dirty=!0),n(e.key)},evaluateComputed:function(e,n){if(!e.cache)return r(e,n);if(!e.dirty)return e.value;e.deps.forEach(n=>{t.get(n)?.delete(e)}),e.deps.clear();const o=r(e,n);return e.value=o,e.dirty=!1,e.value}}}function y(e){const t=f(),r=n(),o=s(),c=l(),{rootProxy:u}=a({root:e,listeners:t,batch:r,effects:o,computed:c}),{useReactive:d,useSelector:y}=p(u,t,r.getVersion),h=i(t);return{state:u,use:d,useSelector:y,effect:o.register,autoEffect:h.effect,computed:c.register,transaction:e=>r.transaction(e,t.notify),getVersion:r.getVersion}}export{n as createBatch,l as createComputed,i as createEffectRuntime,s as createEffects,f as createListeners,a as createProxy,p as createReactHooks,y as createReactive,o as startTracking,c as stopTracking,u as track};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { ListenerManager } from "../reaxor/types";
|
|
2
|
+
export declare function createReactHooks<T>(rootProxy: T, listeners: ListenerManager, getVersion: () => number): {
|
|
3
|
+
useReactive: () => T;
|
|
4
|
+
useSelector: <R>(selector: (state: T) => R) => R;
|
|
5
|
+
useComputed: <R>(compute: (state: T) => R) => R;
|
|
6
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./hooks";
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ComputedNode, ComputedOptions, ReactiveProxy } from "./types";
|
|
2
|
+
export declare function createComputed<T extends object>(): {
|
|
3
|
+
computedMap: Map<string, ComputedNode>;
|
|
4
|
+
register: (key: string, getter: (state: ReactiveProxy<T>) => any, options?: ComputedOptions) => void;
|
|
5
|
+
trackDependency: (path: string) => void;
|
|
6
|
+
invalidateComputed: (path: string, trigger: (path: string) => void) => void;
|
|
7
|
+
evaluateComputed: (node: ComputedNode, state: ReactiveProxy<T>) => any;
|
|
8
|
+
};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
export type Listener = () => void;
|
|
2
|
+
export type EffectFn<T = any> = (newValue: T, oldValue: T, path: string) => void;
|
|
3
|
+
export type EffectOptions = {
|
|
4
|
+
priority?: number;
|
|
5
|
+
depth?: number;
|
|
6
|
+
sync?: boolean;
|
|
7
|
+
};
|
|
8
|
+
export type ReactiveProxy<T> = T extends Array<infer U> ? ReactiveProxyArray<U> : T extends Map<infer K, infer V> ? ReactiveProxyMap<K, V> : T extends Set<infer U> ? ReactiveProxySet<U> : T extends object ? {
|
|
9
|
+
[K in keyof T]: ReactiveProxy<T[K]>;
|
|
10
|
+
} : T;
|
|
11
|
+
export interface ReactiveProxyArray<T> extends Array<ReactiveProxy<T>> {
|
|
12
|
+
}
|
|
13
|
+
export interface ReactiveProxyMap<K, V> extends Map<K, ReactiveProxy<V>> {
|
|
14
|
+
}
|
|
15
|
+
export interface ReactiveProxySet<T> extends Set<ReactiveProxy<T>> {
|
|
16
|
+
}
|
|
17
|
+
export type ComputedOptions = {
|
|
18
|
+
cache?: boolean;
|
|
19
|
+
async?: boolean;
|
|
20
|
+
};
|
|
21
|
+
export type ComputedNode<T = any> = {
|
|
22
|
+
key: string;
|
|
23
|
+
getter: (state: any) => T;
|
|
24
|
+
cache: boolean;
|
|
25
|
+
value: T;
|
|
26
|
+
dirty: boolean;
|
|
27
|
+
deps: Set<string>;
|
|
28
|
+
};
|
|
29
|
+
export interface ListenerManager {
|
|
30
|
+
add(path: string, listener: Listener): void;
|
|
31
|
+
remove(path: string, listener: Listener): void;
|
|
32
|
+
notify(path: string): void;
|
|
33
|
+
}
|
|
34
|
+
export interface BatchManager {
|
|
35
|
+
add(path: string): void;
|
|
36
|
+
schedule(notify: (path: string) => void): void;
|
|
37
|
+
transaction(fn: Listener, notify: (path: string) => void): void;
|
|
38
|
+
getVersion(): number;
|
|
39
|
+
}
|
|
40
|
+
export interface EffectManager {
|
|
41
|
+
register(path: string, fn: EffectFn): void;
|
|
42
|
+
run(path: string, newValue: any, oldValue: any): void;
|
|
43
|
+
}
|
|
44
|
+
export interface ComputedManager<T extends object> {
|
|
45
|
+
computedMap: Map<string, ComputedNode>;
|
|
46
|
+
register(key: string, getter: (state: ReactiveProxy<T>) => any, options?: ComputedOptions): void;
|
|
47
|
+
trackDependency(path: string): void;
|
|
48
|
+
invalidateComputed(path: string, trigger: (path: string) => void): void;
|
|
49
|
+
evaluateComputed(node: ComputedNode, state: ReactiveProxy<T>): any;
|
|
50
|
+
}
|
|
51
|
+
export interface ProxyOptions<T extends object> {
|
|
52
|
+
root: T;
|
|
53
|
+
listeners: ListenerManager;
|
|
54
|
+
batch: BatchManager;
|
|
55
|
+
effects: EffectManager;
|
|
56
|
+
computed: ComputedManager<T>;
|
|
57
|
+
}
|
|
58
|
+
export type Selector<T, R> = (state: T) => R;
|
|
59
|
+
export interface Store<T extends object> {
|
|
60
|
+
state: ReactiveProxy<T>;
|
|
61
|
+
use(): ReactiveProxy<T>;
|
|
62
|
+
useSelector<R>(selector: (state: ReactiveProxy<T>) => R): R;
|
|
63
|
+
computed<K extends string, V>(key: K, getter: (state: ReactiveProxy<T>) => V, options?: {
|
|
64
|
+
cache?: boolean;
|
|
65
|
+
}): void;
|
|
66
|
+
effect<K extends keyof T & string>(key: K, callback: (newVal: T[K], oldVal: T[K]) => void): Listener;
|
|
67
|
+
autoEffect(fn: Listener, options?: {
|
|
68
|
+
priority?: number;
|
|
69
|
+
depth?: number;
|
|
70
|
+
}): Listener;
|
|
71
|
+
transaction(fn: Listener): void;
|
|
72
|
+
getVersion(): number;
|
|
73
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "reazor-react",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Fine-grained Solid-style reactivity for React 18+",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Delpi.Kye",
|
|
7
|
+
"sideEffects": false,
|
|
8
|
+
"type": "module",
|
|
9
|
+
|
|
10
|
+
"main": "build/index.cjs.js",
|
|
11
|
+
"module": "build/index.esm.js",
|
|
12
|
+
"types": "build/index.d.ts",
|
|
13
|
+
|
|
14
|
+
"exports": {
|
|
15
|
+
".": {
|
|
16
|
+
"types": "./build/index.d.ts",
|
|
17
|
+
"import": "./build/index.esm.js",
|
|
18
|
+
"require": "./build/index.cjs.js"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
|
|
22
|
+
"files": [
|
|
23
|
+
"build"
|
|
24
|
+
],
|
|
25
|
+
|
|
26
|
+
"scripts": {
|
|
27
|
+
"clean": "rimraf build",
|
|
28
|
+
"build": "rollup -c",
|
|
29
|
+
"cb": "npm run clean && npm run build",
|
|
30
|
+
"prepublishOnly": "npm run cb"
|
|
31
|
+
},
|
|
32
|
+
|
|
33
|
+
"repository": {
|
|
34
|
+
"type": "git",
|
|
35
|
+
"url": "https://github.com/delpikye-v/reazor-react.git"
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
"homepage": "https://github.com/delpikye-v/reazor-react#readme",
|
|
39
|
+
|
|
40
|
+
"bugs": {
|
|
41
|
+
"url": "https://github.com/delpikye-v/reazor-react/issues"
|
|
42
|
+
},
|
|
43
|
+
|
|
44
|
+
"funding": {
|
|
45
|
+
"type": "github",
|
|
46
|
+
"url": "https://github.com/sponsors/delpikye-v"
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
"keywords": [
|
|
50
|
+
"react",
|
|
51
|
+
"react-18",
|
|
52
|
+
"react-state",
|
|
53
|
+
"react-solid",
|
|
54
|
+
"react-store",
|
|
55
|
+
"fine-grained",
|
|
56
|
+
"proxy-state",
|
|
57
|
+
"reactive",
|
|
58
|
+
"reactivity",
|
|
59
|
+
"map-support",
|
|
60
|
+
"set-support",
|
|
61
|
+
"concurrent-safe",
|
|
62
|
+
"external-store",
|
|
63
|
+
"computed-state",
|
|
64
|
+
"derived-state",
|
|
65
|
+
"state-management",
|
|
66
|
+
"solid-style"
|
|
67
|
+
],
|
|
68
|
+
|
|
69
|
+
"peerDependencies": {
|
|
70
|
+
"react": ">=18.0.0"
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
"devDependencies": {
|
|
74
|
+
"@rollup/plugin-commonjs": "^25.0.0",
|
|
75
|
+
"@rollup/plugin-node-resolve": "^15.2.3",
|
|
76
|
+
"@rollup/plugin-terser": "^0.4.4",
|
|
77
|
+
"@types/react": "^18.2.43",
|
|
78
|
+
"react": "^18.2.0",
|
|
79
|
+
"rimraf": "^5.0.5",
|
|
80
|
+
"rollup": "^4.9.6",
|
|
81
|
+
"rollup-plugin-peer-deps-external": "^2.2.4",
|
|
82
|
+
"rollup-plugin-typescript2": "^0.36.0",
|
|
83
|
+
"tslib": "^2.6.2",
|
|
84
|
+
"typescript": "^5.3.3"
|
|
85
|
+
}
|
|
86
|
+
}
|