@yiin/reactive-proxy-state 1.0.0
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 +233 -0
- package/package.json +51 -0
package/README.md
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
# Reactive Proxy State
|
|
2
|
+
|
|
3
|
+
[](https://badge.fury.io/js/yiin/reactive-proxy-state) <!-- Replace yiin/reactive-proxy-state -->
|
|
4
|
+
|
|
5
|
+
A simple, standalone reactivity library inspired by Vue 3's reactivity system, designed for use outside of Vue, particularly in server-side contexts or for data synchronization tasks. It uses JavaScript Proxies to track changes in plain objects, Arrays, Maps, and Sets.
|
|
6
|
+
|
|
7
|
+
**Note:** This library currently only supports synchronous effect execution.
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
bun add yiin/reactive-proxy-state
|
|
13
|
+
# or npm install yiin/reactive-proxy-state
|
|
14
|
+
# or yarn add yiin/reactive-proxy-state
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Core Concepts
|
|
18
|
+
|
|
19
|
+
1. **Reactive State**: Create reactive versions of your objects using `wrapState`. Any mutations to these wrapped objects will be tracked.
|
|
20
|
+
2. **Dependency Tracking**: When code inside a `watchEffect` reads a property of a reactive object, a dependency is established.
|
|
21
|
+
3. **Effect Triggering**: When a tracked property is mutated, any dependent effects (`watchEffect` or `watch` callbacks) are re-run **synchronously**.
|
|
22
|
+
|
|
23
|
+
## API
|
|
24
|
+
|
|
25
|
+
### `wrapState<T extends object>(obj: T): T`
|
|
26
|
+
|
|
27
|
+
Creates a reactive proxy for the given object, Array, Map, or Set. Nested objects/collections are also recursively wrapped.
|
|
28
|
+
|
|
29
|
+
```typescript
|
|
30
|
+
import { wrapState } from 'yiin/reactive-proxy-state';
|
|
31
|
+
|
|
32
|
+
const state = wrapState({
|
|
33
|
+
count: 0,
|
|
34
|
+
user: { name: 'Alice' },
|
|
35
|
+
items: ['a', 'b'],
|
|
36
|
+
settings: new Map([['theme', 'dark']]),
|
|
37
|
+
ids: new Set([1, 2])
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// Mutations to 'state' and its nested properties/elements will be tracked.
|
|
41
|
+
state.count++;
|
|
42
|
+
state.user.name = 'Bob';
|
|
43
|
+
state.items.push('c');
|
|
44
|
+
state.settings.set('theme', 'light');
|
|
45
|
+
state.ids.add(3);
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### `ref<T>(value?: T): Ref<T | undefined>`
|
|
49
|
+
|
|
50
|
+
Creates a reactive "reference" object for any value type (primitive or object). The value is accessed and mutated through the `.value` property. Reactivity is tracked on the `.value` property itself.
|
|
51
|
+
|
|
52
|
+
**Note:** If a plain object is passed to `ref`, the object *itself* is not made deeply reactive. Only assignment to the `.value` property is tracked. Use `wrapState` for deep object reactivity.
|
|
53
|
+
|
|
54
|
+
```typescript
|
|
55
|
+
import { ref, watchEffect, isRef, unref } from 'yiin/reactive-proxy-state';
|
|
56
|
+
|
|
57
|
+
// Ref for a primitive
|
|
58
|
+
const count = ref(0);
|
|
59
|
+
console.log(count.value); // 0
|
|
60
|
+
|
|
61
|
+
watchEffect(() => {
|
|
62
|
+
console.log('Count is:', count.value);
|
|
63
|
+
});
|
|
64
|
+
// Output: Count is: 0
|
|
65
|
+
|
|
66
|
+
count.value++; // Triggers the effect
|
|
67
|
+
// Output: Count is: 1
|
|
68
|
+
|
|
69
|
+
// Ref for an object
|
|
70
|
+
const user = ref({ name: 'Alice' });
|
|
71
|
+
|
|
72
|
+
watchEffect(() => {
|
|
73
|
+
// This effect depends on the object reference stored in user.value
|
|
74
|
+
console.log('User object:', user.value);
|
|
75
|
+
});
|
|
76
|
+
// Output: User object: { name: 'Alice' }
|
|
77
|
+
|
|
78
|
+
// Mutating the inner object DOES NOT trigger the effect above
|
|
79
|
+
user.value.name = 'Bob';
|
|
80
|
+
|
|
81
|
+
// Assigning a new object DOES trigger the effect
|
|
82
|
+
user.value = { name: 'Charles' };
|
|
83
|
+
// Output: User object: { name: 'Charles' }
|
|
84
|
+
|
|
85
|
+
// Helpers
|
|
86
|
+
console.log(isRef(count)); // true
|
|
87
|
+
console.log(isRef({ value: 0 })); // false
|
|
88
|
+
|
|
89
|
+
console.log(unref(count)); // 1 (current value)
|
|
90
|
+
console.log(unref(123)); // 123 (returns non-refs as is)
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### `computed<T>(getter: () => T): ComputedRef<T>`
|
|
94
|
+
|
|
95
|
+
Creates a computed property based on a getter function. The getter tracks reactive dependencies (`ref`s or reactive object properties) and its result is cached. The computed value only recalculates when a dependency changes.
|
|
96
|
+
Computed refs are read-only.
|
|
97
|
+
|
|
98
|
+
```typescript
|
|
99
|
+
import { ref, computed, watchEffect, isComputed } from 'yiin/reactive-proxy-state';
|
|
100
|
+
|
|
101
|
+
const firstName = ref('John');
|
|
102
|
+
const lastName = ref('Doe');
|
|
103
|
+
|
|
104
|
+
const fullName = computed(() => {
|
|
105
|
+
console.log('Computing fullName...');
|
|
106
|
+
return `${firstName.value} ${lastName.value}`;
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// Accessing .value triggers computation
|
|
110
|
+
console.log(fullName.value);
|
|
111
|
+
// Output: Computing fullName...
|
|
112
|
+
// Output: John Doe
|
|
113
|
+
|
|
114
|
+
// Accessing again uses the cache
|
|
115
|
+
console.log(fullName.value);
|
|
116
|
+
// Output: John Doe
|
|
117
|
+
|
|
118
|
+
watchEffect(() => {
|
|
119
|
+
console.log('Full name changed:', fullName.value);
|
|
120
|
+
});
|
|
121
|
+
// Output: Full name changed: John Doe
|
|
122
|
+
|
|
123
|
+
// Changing a dependency marks computed as dirty
|
|
124
|
+
firstName.value = 'Jane';
|
|
125
|
+
|
|
126
|
+
// Accessing .value again triggers re-computation and the effect
|
|
127
|
+
console.log(fullName.value);
|
|
128
|
+
// Output: Computing fullName...
|
|
129
|
+
// Output: Full name changed: Jane Doe
|
|
130
|
+
// Output: Jane Doe
|
|
131
|
+
|
|
132
|
+
// Chained computed
|
|
133
|
+
const message = computed(() => `User: ${fullName.value}`);
|
|
134
|
+
console.log(message.value); // User: Jane Doe
|
|
135
|
+
|
|
136
|
+
lastName.value = 'Smith';
|
|
137
|
+
// Output: Computing fullName...
|
|
138
|
+
// Output: Full name changed: Jane Smith
|
|
139
|
+
console.log(message.value); // User: Jane Smith (message recomputed automatically)
|
|
140
|
+
|
|
141
|
+
// Read-only check
|
|
142
|
+
try {
|
|
143
|
+
(fullName as any).value = 'Test'; // Throws warning
|
|
144
|
+
} catch (e) { /* ... */ }
|
|
145
|
+
|
|
146
|
+
// Helper
|
|
147
|
+
console.log(isComputed(fullName)); // true
|
|
148
|
+
console.log(isComputed(firstName)); // false
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### `watchEffect(effect: () => void, options?: WatchEffectOptions)`
|
|
152
|
+
|
|
153
|
+
Runs a function immediately, tracks its reactive dependencies, and re-runs it synchronously whenever any of those dependencies change.
|
|
154
|
+
|
|
155
|
+
`WatchEffectOptions`:
|
|
156
|
+
* `onTrack?(event)`: Debug hook called when a dependency is tracked.
|
|
157
|
+
* `onTrigger?(event)`: Debug hook called when the effect is triggered by a mutation.
|
|
158
|
+
|
|
159
|
+
```typescript
|
|
160
|
+
import { wrapState, ref, watchEffect } from 'yiin/reactive-proxy-state';
|
|
161
|
+
|
|
162
|
+
// ... existing watchEffect example using wrapState ...
|
|
163
|
+
|
|
164
|
+
// Using watchEffect with refs
|
|
165
|
+
const counter = ref(10);
|
|
166
|
+
watchEffect(() => {
|
|
167
|
+
console.log('Counter:', counter.value);
|
|
168
|
+
});
|
|
169
|
+
// Output: Counter: 10
|
|
170
|
+
counter.value--;
|
|
171
|
+
// Output: Counter: 9
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### `watch<T>(source: WatchSource<T> | T, callback: (newValue: T, oldValue: T | undefined) => void, options?: WatchOptions)`
|
|
175
|
+
|
|
176
|
+
Watches a specific reactive source (either a getter function, a direct reactive object/value created by `wrapState`, or a `ref`) and runs a callback when the source's value changes.
|
|
177
|
+
|
|
178
|
+
`WatchSource<T>`: A function that returns the value to watch, or a `ref`.
|
|
179
|
+
`callback`: Function executed on change. Receives the new value and the old value.
|
|
180
|
+
`WatchOptions`:
|
|
181
|
+
* `immediate?: boolean`: If `true`, runs the callback immediately with the initial value (oldValue will be `undefined`). Defaults to `false`.
|
|
182
|
+
* `deep?: boolean`: If `true`, deeply traverses the source for dependency tracking and uses deep comparison logic. **Defaults to `true`**. Set to `false` for shallow watching (only triggers on direct assignment or identity change).
|
|
183
|
+
|
|
184
|
+
```typescript
|
|
185
|
+
import { wrapState, ref, watch } from 'yiin/reactive-proxy-state';
|
|
186
|
+
|
|
187
|
+
// ... existing watch examples using wrapState ...
|
|
188
|
+
|
|
189
|
+
// Watching a ref
|
|
190
|
+
const count = ref(0);
|
|
191
|
+
watch(count, (newVal, oldVal) => {
|
|
192
|
+
console.log(`Count changed from ${oldVal} to ${newVal}`);
|
|
193
|
+
});
|
|
194
|
+
count.value = 5; // Output: Count changed from 0 to 5
|
|
195
|
+
|
|
196
|
+
// Watching a getter involving a ref
|
|
197
|
+
const doubleCount = ref(100);
|
|
198
|
+
watch(
|
|
199
|
+
() => doubleCount.value * 2,
|
|
200
|
+
(newDouble, oldDouble) => {
|
|
201
|
+
console.log(`Double changed from ${oldDouble} to ${newDouble}`);
|
|
202
|
+
}
|
|
203
|
+
);
|
|
204
|
+
doubleCount.value = 110; // Output: Double changed from 200 to 220
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
## Collections (Arrays, Maps, Sets)
|
|
208
|
+
|
|
209
|
+
`wrapState` automatically handles Arrays, Maps, and Sets. Mutations via standard methods (`push`, `pop`, `splice`, `set`, `delete`, `add`, `clear`, etc.) are reactive and will trigger effects that depend on the collection or its contents (if watched deeply).
|
|
210
|
+
|
|
211
|
+
```typescript
|
|
212
|
+
import { wrapState, watchEffect } from 'yiin/reactive-proxy-state';
|
|
213
|
+
|
|
214
|
+
const state = wrapState({
|
|
215
|
+
list: [1, 2],
|
|
216
|
+
data: new Map<string, number>(),
|
|
217
|
+
tags: new Set<string>()
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
watchEffect(() => console.log('List size:', state.list.length));
|
|
221
|
+
watchEffect(() => console.log('Data has "foo":', state.data.has('foo')));
|
|
222
|
+
watchEffect(() => console.log('Tags:', Array.from(state.tags).join(', ')));
|
|
223
|
+
|
|
224
|
+
state.list.push(3); // Output: List size: 3
|
|
225
|
+
state.data.set('foo', 100); // Output: Data has "foo": true
|
|
226
|
+
state.tags.add('important'); // Output: Tags: important
|
|
227
|
+
state.data.delete('foo'); // Output: Data has "foo": false
|
|
228
|
+
state.tags.add('urgent'); // Output: Tags: important, urgent
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
## License
|
|
232
|
+
|
|
233
|
+
<!-- Add your license information here, e.g., MIT -->
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@yiin/reactive-proxy-state",
|
|
3
|
+
"private": false,
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"description": "A simple, standalone reactivity library using Proxies",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"require": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist",
|
|
16
|
+
"README.md",
|
|
17
|
+
"LICENSE"
|
|
18
|
+
],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "tsc",
|
|
21
|
+
"test": "bun test",
|
|
22
|
+
"test:watch": "bun test --watch"
|
|
23
|
+
},
|
|
24
|
+
"keywords": [
|
|
25
|
+
"state",
|
|
26
|
+
"sync",
|
|
27
|
+
"javascript",
|
|
28
|
+
"typescript",
|
|
29
|
+
"reactive",
|
|
30
|
+
"proxy",
|
|
31
|
+
"vue",
|
|
32
|
+
"reactivity"
|
|
33
|
+
],
|
|
34
|
+
"author": "Yiin <stanislovas@yiin.lt>",
|
|
35
|
+
"license": "MIT",
|
|
36
|
+
"repository": {
|
|
37
|
+
"type": "git",
|
|
38
|
+
"url": "git+https://github.com/Yiin/reactive-proxy-state.git"
|
|
39
|
+
},
|
|
40
|
+
"bugs": {
|
|
41
|
+
"url": "https://github.com/Yiin/reactive-proxy-state/issues"
|
|
42
|
+
},
|
|
43
|
+
"homepage": "https://github.com/Yiin/reactive-proxy-state#readme",
|
|
44
|
+
"publishConfig": {
|
|
45
|
+
"access": "public"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"bun-types": "^1.2.8",
|
|
49
|
+
"typescript": "^5.0.4"
|
|
50
|
+
}
|
|
51
|
+
}
|