snap-store 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 yahiro
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,210 @@
1
+ # snap-store
2
+
3
+ ## Introduction
4
+
5
+ This is an easy-to-use global state management library for React.
6
+
7
+ ## Installation
8
+ ```sh
9
+ npm install snap-store
10
+ ```
11
+
12
+ ## Motivation
13
+
14
+ I like [valtio](https://github.com/pmndrs/valtio). But sometimes I'm confused by its proxy based design.
15
+ So I wanted to have a non-proxy version of this.
16
+
17
+ I researched some signal based libraries and how to make them work on React.
18
+ I found some hooks (`useSyncExternalStore`) could be used to implement it.
19
+ After struggling for a while, finally I got my own store library working!
20
+
21
+ ```ts
22
+ import {createStore} from "snap-store"
23
+
24
+ const store = createStore({ count: 0});
25
+
26
+ const Counter = () => {
27
+ const { count } = store.snapshot;
28
+ const { setCount } = store.mutations;
29
+ return <button onClick={() => setCount(prev => prev + 1)}>
30
+ {count}
31
+ </button>
32
+ }
33
+ ```
34
+
35
+ ## How it works
36
+ ```ts
37
+ const store = createStore({ count: 0});
38
+ ```
39
+ `createStore` takes an initial state object and returns a store.
40
+ The store holds wrapper signals for each field of the state object.
41
+ ```ts
42
+ const { count } = store.snapshot;
43
+ ```
44
+ In a component, `snapshot` getter is used to refer to the states and make them reactive.
45
+ For this line, `count` is actually a getter and it registers a listener to track the value change.
46
+
47
+ ## Usage Examples
48
+ ```ts
49
+ const store = createStore({ count: 0});
50
+
51
+ function handleButton(){
52
+ const { count } = store.state; //read store state
53
+ store.mutations.setCount(currentCount + 1); //mutate by value
54
+ store.mutations.setCount(prev => prev + 1); //mutate by function
55
+ }
56
+
57
+ const Component = () => {
58
+ const { count } = store.snapshot; //refer store state as reactive
59
+ return <button onClick={handleButton}>push me {count}</button>
60
+ }
61
+ ```
62
+ In the component, `store.snapshot` is used to refer to the store state as a reactive value.
63
+
64
+ Since this is a global state library, you can also read and write store states outside components.
65
+ `store.state` is used to read the value in non-component functions.
66
+
67
+ `store.mutations` has no difference in component or non-component context.
68
+
69
+ ```ts
70
+ const store = createStore({ user: {name: "John", age: 20 }});
71
+ store.mutations.setUser({ name: "Mike", age: 20}); //value
72
+ store.mutations.setUser(prev => ({...prev, age: 21})); //by function
73
+ store.mutations.patchUser({ age: 22}); //partial update (merged)
74
+ store.mutations.produceUser(draft => { draft.age = 23 }) //update with immer
75
+ ```
76
+ It comes with various update methods for each field.
77
+
78
+ `set*` methods are similar to the setter function of `useState`. It takes a value or a function.
79
+
80
+ `patch*` could be used for a partial update. The new state is merged with the previous state and new attributes.
81
+
82
+ `produce*` wraps the `produce` function of `immer`. (`immer` is included in the dependencies.)
83
+
84
+ ```ts
85
+ const store = createStore({
86
+ penWidth: 3,
87
+ penColor: 'black',
88
+ penStyle: 'normal'
89
+ });
90
+ store.mutations.assigns({ penWidth: 1, penStyle: 'dashed' });
91
+ //is equivalent to
92
+ store.mutations.setPenWidth(1);
93
+ store.mutations.setPenStyle('dashed');
94
+ ```
95
+ In mutations, there is `assigns` method to set multiple fields at a time.
96
+ It is useful if you want to update multiple values.
97
+
98
+ There is no performance difference since reactive effects (i.e. rendering) are batched and executed in the next frame.
99
+
100
+ ```ts
101
+ const store = createStore<{theme: "light" | "dark"}>({theme: "light" })
102
+
103
+ const ThemeSelector = () => {
104
+ const { theme } = store.snapshot;
105
+ const { setTheme } = store.mutations;
106
+ return <div>
107
+ <IconButton
108
+ icon="☀️"
109
+ active={theme === 'light'}
110
+ onClick={() => setTheme("light")}
111
+ />
112
+ <IconButton
113
+ icon="🌙"
114
+ active={theme === 'dark'}
115
+ onClick={() => setTheme("dark")}
116
+ />
117
+ </div>
118
+ }
119
+ ```
120
+ Here is a typical theme selector example.
121
+
122
+ ```ts
123
+ const store = createStore<{textSize: number, bgColor: string}>({
124
+ textSize: 5,
125
+ bgColor: "#ffffff"
126
+ })
127
+
128
+ const BookReaderSettings = () => {
129
+ const snap = store.snapshot;
130
+ const mut = store.mutations;
131
+ return <div>
132
+ <Slider
133
+ value={snap.textSize}
134
+ onChange={mut.setTextSize}
135
+ min={10}
136
+ max={20}
137
+ />
138
+ <ColorInput
139
+ value={snap.bgColor}
140
+ onChange={mut.setBgColor}
141
+ />
142
+ </div>
143
+ }
144
+ ```
145
+ Sometimes it might provide good editor completions for non-destructive use.
146
+ However there are caveats in some cases (read below).
147
+
148
+ ## Caveats
149
+ ```ts
150
+ const store = createStore({ name: "Mike", age: 20 });
151
+
152
+ //wrong code
153
+ const Component = () => {
154
+ const snap = store.snapshot;
155
+ if(snap.age < 20) return; //bad early return
156
+ return <div>Hello Gentleman, {snap.name}</div>
157
+ }
158
+
159
+ //working code
160
+ const Component = () => {
161
+ const { age, name } = store.snapshot;
162
+ if(age < 20) return; //no problem
163
+ return <div>Hello Gentleman, {name}</div>
164
+ }
165
+ ```
166
+ Each member of the snapshot object is a getter and it calls a hooks (`useSyncExternalStore`) internally.
167
+
168
+ Since a component must have the same hooks count for each render, non-destructive assign and early return are a bad combination.
169
+
170
+ The snapshot should be destructured if you have an early return.
171
+
172
+ ## Other Signal functionalities
173
+ ```ts
174
+ const store = createStore({ count: 0 });
175
+
176
+ effect(() => {
177
+ const count = store.state.count;
178
+ console.log("Effect:", count);
179
+ });
180
+
181
+ store.mutations.setCount((prev) => prev + 1);
182
+
183
+ const doubled = computed(() => {
184
+ const cnt = store.state.count;
185
+ console.log("computing for:", cnt);
186
+ return cnt * 2;
187
+ });
188
+ console.log("Doubled:", doubled.value);
189
+ ```
190
+ There are two `effect()` and `computed()` helper functions intended to be used in non-component context.
191
+
192
+ `effect()` tracks the referred state changes in the callback and is automatically re-evaluated when the tracked values change.
193
+
194
+ `computed()` is used for caching the computation result, returning a derived signal. It is re-evaluated when tracked values change.
195
+
196
+ ## References
197
+ - [valtio](https://github.com/pmndrs/valtio)
198
+ - [@preact/signals-react](https://github.com/preactjs/signals)
199
+
200
+ `snap-store` is highly influenced by these libraries.
201
+
202
+ Compared to `valtio`, this library provides a similar design of global store but it doesn't use proxies. Also the mutations are applied by the methods not by assignment.
203
+
204
+ Compared to `@preact/signals-react`, although the mechanism of the signal is similar, this library is aimed to supply an opinionated store system whereas `@preact/signals` provides the basic primitive signal functions.
205
+
206
+ ## License
207
+
208
+ MIT License
209
+
210
+
@@ -0,0 +1,4 @@
1
+ import { ComputedSignal } from "./types";
2
+ export declare const effect: (fn: () => void) => (() => void);
3
+ export declare const computed: <U>(fn: () => U) => ComputedSignal<U>;
4
+ //# sourceMappingURL=effects.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"effects.d.ts","sourceRoot":"","sources":["../../src/effects.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAUzC,eAAO,MAAM,MAAM,GAAI,IAAI,MAAM,IAAI,KAAG,CAAC,MAAM,IAAI,CAqBlD,CAAC;AAEF,eAAO,MAAM,QAAQ,GAAI,CAAC,EAAE,IAAI,MAAM,CAAC,KAAG,cAAc,CAAC,CAAC,CAsDzD,CAAC"}
@@ -0,0 +1,79 @@
1
+ import { reactivityHub } from "./reactivity-hub";
2
+ import { createSignal } from "./signal";
3
+ function createEffectReceiver(fn) {
4
+ return {
5
+ listener: fn,
6
+ signalHolders: new Set(),
7
+ cleanup: [],
8
+ };
9
+ }
10
+ export const effect = (fn) => {
11
+ const effectReceiver = createEffectReceiver(fn);
12
+ // Set current effect for dependency tracking
13
+ const prevEffect = reactivityHub.getCurrentEffect();
14
+ reactivityHub.setCurrentEffect(effectReceiver);
15
+ try {
16
+ fn(); // Execute effect to collect dependencies
17
+ }
18
+ finally {
19
+ reactivityHub.setCurrentEffect(prevEffect);
20
+ }
21
+ // Return cleanup function
22
+ return () => {
23
+ effectReceiver.cleanup.forEach((cleanup) => {
24
+ cleanup();
25
+ });
26
+ effectReceiver.cleanup.length = 0;
27
+ effectReceiver.signalHolders.clear();
28
+ };
29
+ };
30
+ export const computed = (fn) => {
31
+ // biome-ignore lint/style/noNonNullAssertion: false
32
+ const internalSignal = createSignal(undefined);
33
+ let initialized;
34
+ let cleanupEffect;
35
+ const ensureInitialized = () => {
36
+ if (!initialized) {
37
+ const effectReceiver = createEffectReceiver(() => {
38
+ const newValue = fn();
39
+ internalSignal.setValue(newValue);
40
+ });
41
+ const prevEffect = reactivityHub.getCurrentEffect();
42
+ reactivityHub.setCurrentEffect(effectReceiver);
43
+ try {
44
+ const initialValue = fn();
45
+ internalSignal.setValue(initialValue);
46
+ }
47
+ finally {
48
+ reactivityHub.setCurrentEffect(prevEffect);
49
+ }
50
+ cleanupEffect = () => {
51
+ effectReceiver.cleanup.forEach((cleanup) => {
52
+ cleanup();
53
+ });
54
+ effectReceiver.cleanup.length = 0;
55
+ effectReceiver.signalHolders.clear();
56
+ };
57
+ initialized = true;
58
+ }
59
+ };
60
+ return {
61
+ get value() {
62
+ ensureInitialized();
63
+ return internalSignal.value;
64
+ },
65
+ get snapshotValue() {
66
+ ensureInitialized();
67
+ return internalSignal.use();
68
+ },
69
+ use() {
70
+ ensureInitialized();
71
+ return internalSignal.use();
72
+ },
73
+ subscribe: internalSignal.subscribe,
74
+ cleanup() {
75
+ cleanupEffect?.();
76
+ },
77
+ };
78
+ };
79
+ //# sourceMappingURL=effects.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"effects.js","sourceRoot":"","sources":["../../src/effects.ts"],"names":[],"mappings":"AAAA,OAAO,EAAkB,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACjE,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAGxC,SAAS,oBAAoB,CAAC,EAAc;IAC1C,OAAO;QACL,QAAQ,EAAE,EAAE;QACZ,aAAa,EAAE,IAAI,GAAG,EAAE;QACxB,OAAO,EAAE,EAAE;KACZ,CAAC;AACJ,CAAC;AAED,MAAM,CAAC,MAAM,MAAM,GAAG,CAAC,EAAc,EAAgB,EAAE;IACrD,MAAM,cAAc,GAAG,oBAAoB,CAAC,EAAE,CAAC,CAAC;IAEhD,6CAA6C;IAC7C,MAAM,UAAU,GAAG,aAAa,CAAC,gBAAgB,EAAE,CAAC;IACpD,aAAa,CAAC,gBAAgB,CAAC,cAAc,CAAC,CAAC;IAE/C,IAAI,CAAC;QACH,EAAE,EAAE,CAAC,CAAC,yCAAyC;IACjD,CAAC;YAAS,CAAC;QACT,aAAa,CAAC,gBAAgB,CAAC,UAAU,CAAC,CAAC;IAC7C,CAAC;IAED,0BAA0B;IAC1B,OAAO,GAAG,EAAE;QACV,cAAc,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE;YACzC,OAAO,EAAE,CAAC;QACZ,CAAC,CAAC,CAAC;QACH,cAAc,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC;QAClC,cAAc,CAAC,aAAa,CAAC,KAAK,EAAE,CAAC;IACvC,CAAC,CAAC;AACJ,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,QAAQ,GAAG,CAAI,EAAW,EAAqB,EAAE;IAC5D,oDAAoD;IACpD,MAAM,cAAc,GAAG,YAAY,CAAI,SAAU,CAAC,CAAC;IAEnD,IAAI,WAAoB,CAAC;IACzB,IAAI,aAAuC,CAAC;IAE5C,MAAM,iBAAiB,GAAG,GAAG,EAAE;QAC7B,IAAI,CAAC,WAAW,EAAE,CAAC;YACjB,MAAM,cAAc,GAAG,oBAAoB,CAAC,GAAG,EAAE;gBAC/C,MAAM,QAAQ,GAAG,EAAE,EAAE,CAAC;gBACtB,cAAc,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;YACpC,CAAC,CAAC,CAAC;YAEH,MAAM,UAAU,GAAG,aAAa,CAAC,gBAAgB,EAAE,CAAC;YACpD,aAAa,CAAC,gBAAgB,CAAC,cAAc,CAAC,CAAC;YAE/C,IAAI,CAAC;gBACH,MAAM,YAAY,GAAG,EAAE,EAAE,CAAC;gBAC1B,cAAc,CAAC,QAAQ,CAAC,YAAY,CAAC,CAAC;YACxC,CAAC;oBAAS,CAAC;gBACT,aAAa,CAAC,gBAAgB,CAAC,UAAU,CAAC,CAAC;YAC7C,CAAC;YAED,aAAa,GAAG,GAAG,EAAE;gBACnB,cAAc,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE;oBACzC,OAAO,EAAE,CAAC;gBACZ,CAAC,CAAC,CAAC;gBACH,cAAc,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC;gBAClC,cAAc,CAAC,aAAa,CAAC,KAAK,EAAE,CAAC;YACvC,CAAC,CAAC;YAEF,WAAW,GAAG,IAAI,CAAC;QACrB,CAAC;IACH,CAAC,CAAC;IAEF,OAAO;QACL,IAAI,KAAK;YACP,iBAAiB,EAAE,CAAC;YACpB,OAAO,cAAc,CAAC,KAAK,CAAC;QAC9B,CAAC;QACD,IAAI,aAAa;YACf,iBAAiB,EAAE,CAAC;YACpB,OAAO,cAAc,CAAC,GAAG,EAAE,CAAC;QAC9B,CAAC;QACD,GAAG;YACD,iBAAiB,EAAE,CAAC;YACpB,OAAO,cAAc,CAAC,GAAG,EAAE,CAAC;QAC9B,CAAC;QACD,SAAS,EAAE,cAAc,CAAC,SAAS;QACnC,OAAO;YACL,aAAa,EAAE,EAAE,CAAC;QACpB,CAAC;KACF,CAAC;AACJ,CAAC,CAAC"}
@@ -0,0 +1,2 @@
1
+ export declare function capitalizeFirstLetter(text: string): string;
2
+ //# sourceMappingURL=helpers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"helpers.d.ts","sourceRoot":"","sources":["../../src/helpers.ts"],"names":[],"mappings":"AAAA,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,UAEjD"}
@@ -0,0 +1,4 @@
1
+ export function capitalizeFirstLetter(text) {
2
+ return text.charAt(0).toUpperCase() + text.slice(1);
3
+ }
4
+ //# sourceMappingURL=helpers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"helpers.js","sourceRoot":"","sources":["../../src/helpers.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,qBAAqB,CAAC,IAAY;IAChD,OAAO,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;AACtD,CAAC"}
@@ -0,0 +1,5 @@
1
+ export * from "./effects";
2
+ export * from "./signal";
3
+ export * from "./store";
4
+ export * from "./types";
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,WAAW,CAAC;AAC1B,cAAc,UAAU,CAAC;AACzB,cAAc,SAAS,CAAC;AACxB,cAAc,SAAS,CAAC"}
@@ -0,0 +1,5 @@
1
+ export * from "./effects";
2
+ export * from "./signal";
3
+ export * from "./store";
4
+ export * from "./types";
5
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,WAAW,CAAC;AAC1B,cAAc,UAAU,CAAC;AACzB,cAAc,SAAS,CAAC;AACxB,cAAc,SAAS,CAAC"}
@@ -0,0 +1,20 @@
1
+ import { SignalListener } from "./types";
2
+ export type SignalHolder<T> = {
3
+ value: T;
4
+ listeners: Set<SignalListener>;
5
+ };
6
+ export type EffectReceiver = {
7
+ listener: () => void;
8
+ signalHolders: Set<SignalHolder<any>>;
9
+ cleanup: (() => void)[];
10
+ };
11
+ type ReactivityHub = {
12
+ registerSignal(key: symbol, holder: SignalHolder<any>): void;
13
+ getCurrentEffect(): EffectReceiver | undefined;
14
+ setCurrentEffect(effect: EffectReceiver | undefined): void;
15
+ addPendingListener(listener: SignalListener): void;
16
+ scheduleFlush(): void;
17
+ };
18
+ export declare const reactivityHub: ReactivityHub;
19
+ export {};
20
+ //# sourceMappingURL=reactivity-hub.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reactivity-hub.d.ts","sourceRoot":"","sources":["../../src/reactivity-hub.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAEzC,MAAM,MAAM,YAAY,CAAC,CAAC,IAAI;IAC5B,KAAK,EAAE,CAAC,CAAC;IACT,SAAS,EAAE,GAAG,CAAC,cAAc,CAAC,CAAC;CAChC,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG;IAC3B,QAAQ,EAAE,MAAM,IAAI,CAAC;IACrB,aAAa,EAAE,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,CAAC,CAAC;IACtC,OAAO,EAAE,CAAC,MAAM,IAAI,CAAC,EAAE,CAAC;CACzB,CAAC;AAEF,KAAK,aAAa,GAAG;IACnB,cAAc,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,YAAY,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC;IAC7D,gBAAgB,IAAI,cAAc,GAAG,SAAS,CAAC;IAC/C,gBAAgB,CAAC,MAAM,EAAE,cAAc,GAAG,SAAS,GAAG,IAAI,CAAC;IAC3D,kBAAkB,CAAC,QAAQ,EAAE,cAAc,GAAG,IAAI,CAAC;IACnD,aAAa,IAAI,IAAI,CAAC;CACvB,CAAC;AAsCF,eAAO,MAAM,aAAa,eAAwB,CAAC"}
@@ -0,0 +1,35 @@
1
+ function createReactivityHub() {
2
+ const signalHolders = new Map();
3
+ let currentEffect;
4
+ const pendingListeners = new Set();
5
+ let flushScheduled = false;
6
+ return {
7
+ registerSignal(key, holder) {
8
+ signalHolders.set(key, holder);
9
+ },
10
+ getCurrentEffect() {
11
+ return currentEffect;
12
+ },
13
+ setCurrentEffect(effect) {
14
+ currentEffect = effect;
15
+ },
16
+ addPendingListener(listener) {
17
+ pendingListeners.add(listener);
18
+ },
19
+ scheduleFlush() {
20
+ if (flushScheduled)
21
+ return;
22
+ flushScheduled = true;
23
+ Promise.resolve().then(() => {
24
+ const listeners = Array.from(pendingListeners);
25
+ pendingListeners.clear();
26
+ flushScheduled = false;
27
+ listeners.forEach((listener) => {
28
+ listener();
29
+ });
30
+ });
31
+ },
32
+ };
33
+ }
34
+ export const reactivityHub = createReactivityHub();
35
+ //# sourceMappingURL=reactivity-hub.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reactivity-hub.js","sourceRoot":"","sources":["../../src/reactivity-hub.ts"],"names":[],"mappings":"AAqBA,SAAS,mBAAmB;IAC1B,MAAM,aAAa,GAAG,IAAI,GAAG,EAA6B,CAAC;IAC3D,IAAI,aAAyC,CAAC;IAC9C,MAAM,gBAAgB,GAAG,IAAI,GAAG,EAAkB,CAAC;IACnD,IAAI,cAAc,GAAG,KAAK,CAAC;IAE3B,OAAO;QACL,cAAc,CAAC,GAAW,EAAE,MAAyB;YACnD,aAAa,CAAC,GAAG,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;QACjC,CAAC;QACD,gBAAgB;YACd,OAAO,aAAa,CAAC;QACvB,CAAC;QACD,gBAAgB,CAAC,MAAkC;YACjD,aAAa,GAAG,MAAM,CAAC;QACzB,CAAC;QACD,kBAAkB,CAAC,QAAwB;YACzC,gBAAgB,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QACjC,CAAC;QACD,aAAa;YACX,IAAI,cAAc;gBAAE,OAAO;YAC3B,cAAc,GAAG,IAAI,CAAC;YAEtB,OAAO,CAAC,OAAO,EAAE,CAAC,IAAI,CAAC,GAAG,EAAE;gBAC1B,MAAM,SAAS,GAAG,KAAK,CAAC,IAAI,CAAC,gBAAgB,CAAC,CAAC;gBAC/C,gBAAgB,CAAC,KAAK,EAAE,CAAC;gBACzB,cAAc,GAAG,KAAK,CAAC;gBAEvB,SAAS,CAAC,OAAO,CAAC,CAAC,QAAQ,EAAE,EAAE;oBAC7B,QAAQ,EAAE,CAAC;gBACb,CAAC,CAAC,CAAC;YACL,CAAC,CAAC,CAAC;QACL,CAAC;KACF,CAAC;AACJ,CAAC;AAED,MAAM,CAAC,MAAM,aAAa,GAAG,mBAAmB,EAAE,CAAC"}
@@ -0,0 +1,3 @@
1
+ import { Signal } from "./types";
2
+ export declare function createSignal<T>(initialValue: T): Signal<T>;
3
+ //# sourceMappingURL=signal.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"signal.d.ts","sourceRoot":"","sources":["../../src/signal.ts"],"names":[],"mappings":"AAEA,OAAO,EAAiB,MAAM,EAAkB,MAAM,SAAS,CAAC;AAEhE,wBAAgB,YAAY,CAAC,CAAC,EAAE,YAAY,EAAE,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC,CAyC1D"}
@@ -0,0 +1,42 @@
1
+ import { useSyncExternalStore } from "react";
2
+ import { reactivityHub } from "./reactivity-hub";
3
+ export function createSignal(initialValue) {
4
+ const key = Symbol();
5
+ const holder = {
6
+ value: initialValue,
7
+ listeners: new Set(),
8
+ };
9
+ reactivityHub.registerSignal(key, holder);
10
+ const subscribe = (listener) => {
11
+ holder.listeners.add(listener);
12
+ return () => holder.listeners.delete(listener);
13
+ };
14
+ const signal = {
15
+ get value() {
16
+ const ce = reactivityHub.getCurrentEffect();
17
+ if (ce && !ce.signalHolders.has(holder)) {
18
+ ce.signalHolders.add(holder);
19
+ const unsubscribe = subscribe(ce.listener);
20
+ ce.cleanup.push(unsubscribe);
21
+ }
22
+ return holder.value;
23
+ },
24
+ setValue(arg) {
25
+ const value = typeof arg === "function" ? arg(holder.value) : arg;
26
+ if (value === holder.value)
27
+ return;
28
+ holder.value = value;
29
+ holder.listeners.forEach((listener) => {
30
+ reactivityHub.addPendingListener(listener);
31
+ });
32
+ reactivityHub.scheduleFlush();
33
+ },
34
+ subscribe,
35
+ use() {
36
+ const value = useSyncExternalStore(subscribe, () => holder.value);
37
+ return value;
38
+ },
39
+ };
40
+ return signal;
41
+ }
42
+ //# sourceMappingURL=signal.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"signal.js","sourceRoot":"","sources":["../../src/signal.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,OAAO,CAAC;AAC7C,OAAO,EAAE,aAAa,EAAgB,MAAM,kBAAkB,CAAC;AAG/D,MAAM,UAAU,YAAY,CAAI,YAAe;IAC7C,MAAM,GAAG,GAAG,MAAM,EAAE,CAAC;IACrB,MAAM,MAAM,GAAoB;QAC9B,KAAK,EAAE,YAAY;QACnB,SAAS,EAAE,IAAI,GAAG,EAAE;KACrB,CAAC;IACF,aAAa,CAAC,cAAc,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;IAE1C,MAAM,SAAS,GAAG,CAAC,QAAwB,EAAE,EAAE;QAC7C,MAAM,CAAC,SAAS,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QAC/B,OAAO,GAAG,EAAE,CAAC,MAAM,CAAC,SAAS,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;IACjD,CAAC,CAAC;IAEF,MAAM,MAAM,GAAc;QACxB,IAAI,KAAK;YACP,MAAM,EAAE,GAAG,aAAa,CAAC,gBAAgB,EAAE,CAAC;YAC5C,IAAI,EAAE,IAAI,CAAC,EAAE,CAAC,aAAa,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC;gBACxC,EAAE,CAAC,aAAa,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;gBAC7B,MAAM,WAAW,GAAG,SAAS,CAAC,EAAE,CAAC,QAAQ,CAAC,CAAC;gBAC3C,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;YAC/B,CAAC;YACD,OAAO,MAAM,CAAC,KAAK,CAAC;QACtB,CAAC;QACD,QAAQ,CAAC,GAAqB;YAC5B,MAAM,KAAK,GACT,OAAO,GAAG,KAAK,UAAU,CAAC,CAAC,CAAE,GAAsB,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC;YAC1E,IAAI,KAAK,KAAK,MAAM,CAAC,KAAK;gBAAE,OAAO;YAEnC,MAAM,CAAC,KAAK,GAAG,KAAK,CAAC;YACrB,MAAM,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC,QAAQ,EAAE,EAAE;gBACpC,aAAa,CAAC,kBAAkB,CAAC,QAAQ,CAAC,CAAC;YAC7C,CAAC,CAAC,CAAC;YACH,aAAa,CAAC,aAAa,EAAE,CAAC;QAChC,CAAC;QACD,SAAS;QACT,GAAG;YACD,MAAM,KAAK,GAAG,oBAAoB,CAAC,SAAS,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;YAClE,OAAO,KAAK,CAAC;QACf,CAAC;KACF,CAAC;IACF,OAAO,MAAM,CAAC;AAChB,CAAC"}
@@ -0,0 +1,3 @@
1
+ import { Store } from "./types";
2
+ export declare function createStore<T extends object>(initialState: T): Store<T>;
3
+ //# sourceMappingURL=store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/store.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAEhC,wBAAgB,WAAW,CAAC,CAAC,SAAS,MAAM,EAAE,YAAY,EAAE,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC,CAmDvE"}
@@ -0,0 +1,48 @@
1
+ import { produce } from "immer";
2
+ import { capitalizeFirstLetter } from "./helpers";
3
+ import { createSignal } from "./signal";
4
+ export function createStore(initialState) {
5
+ const stateGetters = {};
6
+ const snapshotGetters = {};
7
+ const mutations = {};
8
+ const signals = {};
9
+ for (const key in initialState) {
10
+ const initialValue = initialState[key];
11
+ const signal = createSignal(initialValue);
12
+ Object.defineProperty(stateGetters, key, {
13
+ get() {
14
+ return signal.value;
15
+ },
16
+ });
17
+ Object.defineProperty(snapshotGetters, key, {
18
+ get() {
19
+ return signal.use();
20
+ },
21
+ });
22
+ mutations[`set${capitalizeFirstLetter(key)}`] = signal.setValue;
23
+ mutations[`produce${capitalizeFirstLetter(key)}`] = (fn) => {
24
+ signal.setValue((draft) => produce(draft, fn));
25
+ };
26
+ mutations[`patch${capitalizeFirstLetter(key)}`] = (attrs) => {
27
+ signal.setValue((prev) => ({ ...prev, ...attrs }));
28
+ };
29
+ signals[key] = signal;
30
+ }
31
+ Object.assign(mutations, {
32
+ assigns: (attrs) => {
33
+ for (const key in attrs) {
34
+ const value = attrs[key];
35
+ const setValue = mutations[`set${capitalizeFirstLetter(key)}`];
36
+ setValue?.(value);
37
+ }
38
+ },
39
+ });
40
+ return {
41
+ state: stateGetters,
42
+ snapshot: snapshotGetters,
43
+ useSnapshot: () => snapshotGetters,
44
+ mutations,
45
+ signals,
46
+ };
47
+ }
48
+ //# sourceMappingURL=store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"store.js","sourceRoot":"","sources":["../../src/store.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,OAAO,CAAC;AAChC,OAAO,EAAE,qBAAqB,EAAE,MAAM,WAAW,CAAC;AAClD,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAGxC,MAAM,UAAU,WAAW,CAAmB,YAAe;IAE3D,MAAM,YAAY,GAAQ,EAAE,CAAC;IAC7B,MAAM,eAAe,GAAQ,EAAE,CAAC;IAChC,MAAM,SAAS,GAAQ,EAAE,CAAC;IAC1B,MAAM,OAAO,GAAQ,EAAE,CAAC;IAExB,KAAK,MAAM,GAAG,IAAI,YAAY,EAAE,CAAC;QAC/B,MAAM,YAAY,GAAG,YAAY,CAAC,GAAG,CAAC,CAAC;QACvC,MAAM,MAAM,GAAG,YAAY,CAAC,YAAY,CAAC,CAAC;QAE1C,MAAM,CAAC,cAAc,CAAC,YAAY,EAAE,GAAG,EAAE;YACvC,GAAG;gBACD,OAAO,MAAM,CAAC,KAAK,CAAC;YACtB,CAAC;SACF,CAAC,CAAC;QACH,MAAM,CAAC,cAAc,CAAC,eAAe,EAAE,GAAG,EAAE;YAC1C,GAAG;gBACD,OAAO,MAAM,CAAC,GAAG,EAAE,CAAC;YACtB,CAAC;SACF,CAAC,CAAC;QACH,SAAS,CAAC,MAAM,qBAAqB,CAAC,GAAa,CAAC,EAAE,CAAC,GAAG,MAAM,CAAC,QAAQ,CAAC;QAC1E,SAAS,CAAC,UAAU,qBAAqB,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAClD,EAAyB,EACzB,EAAE;YACF,MAAM,CAAC,QAAQ,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,CAAC;QACjD,CAAC,CAAC;QACF,SAAS,CAAC,QAAQ,qBAAqB,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAChD,KAAoB,EACpB,EAAE;YACF,MAAM,CAAC,QAAQ,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,EAAE,GAAG,IAAI,EAAE,GAAG,KAAK,EAAE,CAAC,CAAC,CAAC;QACrD,CAAC,CAAC;QACF,OAAO,CAAC,GAAG,CAAC,GAAG,MAAM,CAAC;IACxB,CAAC;IACD,MAAM,CAAC,MAAM,CAAC,SAAS,EAAE;QACvB,OAAO,EAAE,CAAC,KAAiB,EAAE,EAAE;YAC7B,KAAK,MAAM,GAAG,IAAI,KAAK,EAAE,CAAC;gBACxB,MAAM,KAAK,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC;gBACzB,MAAM,QAAQ,GAAG,SAAS,CAAC,MAAM,qBAAqB,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;gBAC/D,QAAQ,EAAE,CAAC,KAAK,CAAC,CAAC;YACpB,CAAC;QACH,CAAC;KACF,CAAC,CAAC;IAEH,OAAO;QACL,KAAK,EAAE,YAAY;QACnB,QAAQ,EAAE,eAAe;QACzB,WAAW,EAAE,GAAG,EAAE,CAAC,eAAe;QAClC,SAAS;QACT,OAAO;KACR,CAAC;AACJ,CAAC"}
@@ -0,0 +1,34 @@
1
+ export type SignalListener = () => void;
2
+ export type SetterPayload<T> = T | ((prev: T) => T);
3
+ export type Signal<T> = {
4
+ readonly value: T;
5
+ setValue(arg: SetterPayload<T>): void;
6
+ subscribe(listener: SignalListener): () => void;
7
+ use(): T;
8
+ };
9
+ export type ComputedSignal<T> = {
10
+ readonly value: T;
11
+ subscribe(listener: SignalListener): () => void;
12
+ readonly snapshotValue: T;
13
+ use(): T;
14
+ cleanup(): void;
15
+ };
16
+ export type Mutations<T> = {
17
+ [K in keyof T as `set${Capitalize<K & string>}`]: (value: T[K] | ((prev: T[K]) => T[K])) => void;
18
+ } & {
19
+ [K in keyof T as `produce${Capitalize<K & string>}`]: (fn: (draft: T[K]) => void) => void;
20
+ } & {
21
+ [K in keyof T as `patch${Capitalize<K & string>}`]: (attrs: Partial<T[K]>) => void;
22
+ } & {
23
+ assigns: (attrs: Partial<T>) => void;
24
+ };
25
+ export type Store<T extends object> = {
26
+ state: T;
27
+ snapshot: T;
28
+ useSnapshot(): T;
29
+ mutations: Mutations<T>;
30
+ signals: {
31
+ [K in keyof T]: Signal<T[K]>;
32
+ };
33
+ };
34
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,cAAc,GAAG,MAAM,IAAI,CAAC;AAExC,MAAM,MAAM,aAAa,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC;AAGpD,MAAM,MAAM,MAAM,CAAC,CAAC,IAAI;IACtB,QAAQ,CAAC,KAAK,EAAE,CAAC,CAAC;IAClB,QAAQ,CAAC,GAAG,EAAE,aAAa,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC;IACtC,SAAS,CAAC,QAAQ,EAAE,cAAc,GAAG,MAAM,IAAI,CAAC;IAChD,GAAG,IAAI,CAAC,CAAC;CACV,CAAC;AAGF,MAAM,MAAM,cAAc,CAAC,CAAC,IAAI;IAC9B,QAAQ,CAAC,KAAK,EAAE,CAAC,CAAC;IAClB,SAAS,CAAC,QAAQ,EAAE,cAAc,GAAG,MAAM,IAAI,CAAC;IAChD,QAAQ,CAAC,aAAa,EAAE,CAAC,CAAC;IAC1B,GAAG,IAAI,CAAC,CAAC;IACT,OAAO,IAAI,IAAI,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,SAAS,CAAC,CAAC,IAAI;KACxB,CAAC,IAAI,MAAM,CAAC,IAAI,MAAM,UAAU,CAAC,CAAC,GAAG,MAAM,CAAC,EAAE,GAAG,CAChD,KAAK,EAAE,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,KACjC,IAAI;CACV,GAAG;KACD,CAAC,IAAI,MAAM,CAAC,IAAI,UAAU,UAAU,CAAC,CAAC,GAAG,MAAM,CAAC,EAAE,GAAG,CACpD,EAAE,EAAE,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,CAAC,KAAK,IAAI,KACtB,IAAI;CACV,GAAG;KACD,CAAC,IAAI,MAAM,CAAC,IAAI,QAAQ,UAAU,CAAC,CAAC,GAAG,MAAM,CAAC,EAAE,GAAG,CAClD,KAAK,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,KACjB,IAAI;CACV,GAAG;IACF,OAAO,EAAE,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC;CACtC,CAAC;AAEF,MAAM,MAAM,KAAK,CAAC,CAAC,SAAS,MAAM,IAAI;IACpC,KAAK,EAAE,CAAC,CAAC;IACT,QAAQ,EAAE,CAAC,CAAC;IACZ,WAAW,IAAI,CAAC,CAAC;IACjB,SAAS,EAAE,SAAS,CAAC,CAAC,CAAC,CAAC;IACxB,OAAO,EAAE;SAAG,CAAC,IAAI,MAAM,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;KAAE,CAAC;CAC3C,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":""}
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "snap-store",
3
+ "version": "0.1.0",
4
+ "description": "An easy-to-use global state management library for React.",
5
+ "author": "yahiro",
6
+ "license": "MIT",
7
+ "type": "module",
8
+ "main": "./dist/esm/index.js",
9
+ "types": "./dist/esm/index.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "types": "./dist/esm/index.d.ts",
13
+ "import": "./dist/esm/index.js"
14
+ }
15
+ },
16
+ "files": [
17
+ "dist",
18
+ "README.md",
19
+ "LICENSE"
20
+ ],
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "git+https://github.com/yahiro07/snap-store.git"
24
+ },
25
+ "bugs": {
26
+ "url": "https://github.com/yahiro07/snap-store/issues"
27
+ },
28
+ "homepage": "https://github.com/yahiro07/snap-store#readme",
29
+ "keywords": [
30
+ "react",
31
+ "state",
32
+ "management",
33
+ "store",
34
+ "signal",
35
+ "reactive",
36
+ "hooks"
37
+ ],
38
+ "scripts": {
39
+ "build:esm": "tsc --project tsconfig.esm.json",
40
+ "build": "npm run build:esm",
41
+ "prepublishOnly": "npm run build"
42
+ },
43
+ "dependencies": {
44
+ "immer": "^10.2.0"
45
+ },
46
+ "peerDependencies": {
47
+ "react": ">=18.0.0"
48
+ },
49
+ "devDependencies": {
50
+ "@types/react": "^19.2.2",
51
+ "typescript": "^5.9.3"
52
+ },
53
+ "engines": {
54
+ "node": ">=16.0.0"
55
+ }
56
+ }