@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.
Files changed (2) hide show
  1. package/README.md +233 -0
  2. package/package.json +51 -0
package/README.md ADDED
@@ -0,0 +1,233 @@
1
+ # Reactive Proxy State
2
+
3
+ [![npm version](https://badge.fury.io/js/yiin/reactive-proxy-state.svg)](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
+ }