@dvashim/store 1.1.3 → 1.2.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 ADDED
@@ -0,0 +1,196 @@
1
+ # @dvashim/store
2
+
3
+ [![npm version](https://img.shields.io/npm/v/@dvashim/store.svg?logo=npm&style=flat-square&color2=07c&label=@dvashim/store)](https://www.npmjs.com/package/@dvashim/store) [![npm downloads](https://img.shields.io/npm/dm/@dvashim/store?logo=npm&style=flat-square&color=07c)](https://www.npmjs.com/package/@dvashim/store) [![Checked with Biome](https://img.shields.io/badge/Checked_with-Biome-60a5fa?style=flat-square&logo=biome&color=07c&logoColor=fff)](https://biomejs.dev)
4
+
5
+ A minimal, lightweight React state management library built on `useSyncExternalStore`.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install @dvashim/store
11
+ # or
12
+ pnpm add @dvashim/store
13
+ ```
14
+
15
+ **Peer dependencies:** `react >= 18`
16
+
17
+ ## Quick Start
18
+
19
+ ```tsx
20
+ import { createStore, useStore } from '@dvashim/store'
21
+
22
+ const count$ = createStore(0)
23
+
24
+ function Counter() {
25
+ const count = useStore(count$)
26
+
27
+ return (
28
+ <button onClick={() => count$.update((n) => n + 1)}>
29
+ Count: {count}
30
+ </button>
31
+ )
32
+ }
33
+ ```
34
+
35
+ ## API
36
+
37
+ ### `createStore(initialState?)`
38
+
39
+ Creates a new `Store` instance.
40
+
41
+ ```ts
42
+ const count$ = createStore(0)
43
+ const user$ = createStore({ name: 'Alice', age: 30 })
44
+
45
+ // Without initial state — type defaults to T | undefined
46
+ const data$ = createStore<string>()
47
+ ```
48
+
49
+ ### `Store`
50
+
51
+ Reactive state container with subscription-based change notification.
52
+
53
+ #### `store.get()`
54
+
55
+ Returns the current state.
56
+
57
+ ```ts
58
+ const count$ = createStore(10)
59
+ count$.get() // 10
60
+ ```
61
+
62
+ #### `store.set(state, options?)`
63
+
64
+ Replaces the state. Skipped if the value is identical (`Object.is`), unless `{ force: true }` is passed.
65
+
66
+ ```ts
67
+ count$.set(5)
68
+
69
+ // Force notify subscribers even if the value hasn't changed
70
+ count$.set(5, { force: true })
71
+ ```
72
+
73
+ #### `store.update(updater, options?)`
74
+
75
+ Derives the next state via an updater function. Re-entrant calls from within a subscriber are queued and flushed in FIFO order.
76
+
77
+ ```ts
78
+ count$.update((n) => n + 1)
79
+
80
+ // With objects — always return a new reference
81
+ const todos$ = createStore([{ text: 'Buy milk', done: false }])
82
+ todos$.update((todos) => [...todos, { text: 'Walk dog', done: false }])
83
+ ```
84
+
85
+ #### `store.subscribe(fn)`
86
+
87
+ Registers a callback invoked on each state change. Returns an unsubscribe function.
88
+
89
+ ```ts
90
+ const unsubscribe = count$.subscribe(() => {
91
+ console.log('Count changed:', count$.get())
92
+ })
93
+
94
+ // Later...
95
+ unsubscribe()
96
+ ```
97
+
98
+ ### `useStore(store, selector?)`
99
+
100
+ React hook that subscribes a component to a store.
101
+
102
+ ```tsx
103
+ function Counter() {
104
+ const count = useStore(count$)
105
+ return <p>{count}</p>
106
+ }
107
+ ```
108
+
109
+ #### With a selector
110
+
111
+ Derive a value from the store state. The selector should return a referentially stable value (primitive or existing object reference) to avoid unnecessary re-renders.
112
+
113
+ ```tsx
114
+ const user$ = createStore({ name: 'Alice', age: 30 })
115
+
116
+ function UserName() {
117
+ const name = useStore(user$, (user) => user.name)
118
+ return <p>{name}</p>
119
+ }
120
+ ```
121
+
122
+ ## Patterns
123
+
124
+ ### Shared stores across components
125
+
126
+ Define stores outside of components and import them where needed.
127
+
128
+ ```ts
129
+ // stores/auth.ts
130
+ import { createStore } from '@dvashim/store'
131
+
132
+ export const token$ = createStore<string | null>(null)
133
+
134
+ export function login(token: string) {
135
+ token$.set(token)
136
+ }
137
+
138
+ export function logout() {
139
+ token$.set(null)
140
+ }
141
+ ```
142
+
143
+ ```tsx
144
+ // components/Profile.tsx
145
+ import { useStore } from '@dvashim/store'
146
+ import { token$, logout } from '../stores/auth'
147
+
148
+ function Profile() {
149
+ const token = useStore(token$)
150
+
151
+ if (!token) return <p>Not logged in</p>
152
+
153
+ return <button onClick={logout}>Log out</button>
154
+ }
155
+ ```
156
+
157
+ ### Combining multiple stores
158
+
159
+ ```tsx
160
+ import { createStore, useStore } from '@dvashim/store'
161
+
162
+ const firstName$ = createStore('Alice')
163
+ const lastName$ = createStore('Smith')
164
+
165
+ function FullName() {
166
+ const firstName = useStore(firstName$)
167
+ const lastName = useStore(lastName$)
168
+
169
+ return <p>{firstName} {lastName}</p>
170
+ }
171
+ ```
172
+
173
+ ### Using the Store class directly
174
+
175
+ ```ts
176
+ import { Store } from '@dvashim/store'
177
+
178
+ class TimerService {
179
+ readonly seconds$ = new Store(0)
180
+ #interval: ReturnType<typeof setInterval> | undefined
181
+
182
+ start() {
183
+ this.#interval = setInterval(() => {
184
+ this.seconds$.update((s) => s + 1)
185
+ }, 1000)
186
+ }
187
+
188
+ stop() {
189
+ clearInterval(this.#interval)
190
+ }
191
+ }
192
+ ```
193
+
194
+ ## License
195
+
196
+ MIT
package/dist/Store.d.ts CHANGED
@@ -3,14 +3,35 @@ type Updater<T> = (prevValue: T) => T;
3
3
  type UpdateOptions = {
4
4
  force?: boolean;
5
5
  };
6
+ /**
7
+ * Reactive state container with subscription-based change notification.
8
+ * @typeParam T - The type of the stored state.
9
+ */
6
10
  export declare class Store<T> {
7
11
  #private;
8
12
  constructor(initialState: T);
9
- get state(): T;
10
- get(): T;
11
- set(state: T, options?: UpdateOptions): boolean;
12
- update(updater: Updater<T>, options?: UpdateOptions): boolean;
13
+ /**
14
+ * Registers a subscriber that is called whenever the state changes.
15
+ * @param fn - Callback invoked on each state change.
16
+ * @returns An unsubscribe function that removes the subscriber.
17
+ */
13
18
  subscribe(fn: Subscriber): () => void;
19
+ /** Returns the current state. */
20
+ get(): T;
21
+ /**
22
+ * Replaces the state and notifies subscribers.
23
+ * Skipped if the value is identical (`Object.is`), unless `force` is set.
24
+ * @param state - The new state value.
25
+ * @param options - Pass `{ force: true }` to notify even when unchanged.
26
+ */
27
+ set(state: T, options?: UpdateOptions): void;
28
+ /**
29
+ * Derives the next state via an updater function and notifies subscribers.
30
+ * Re-entrant calls from within a subscriber are queued and flushed in FIFO order.
31
+ * @param updater - Receives the current state and returns the next state.
32
+ * @param options - Pass `{ force: true }` to notify even when unchanged.
33
+ */
34
+ update(updater: Updater<T>, options?: UpdateOptions): void;
14
35
  }
15
36
  export {};
16
37
  //# sourceMappingURL=Store.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"Store.d.ts","sourceRoot":"","sources":["../src/Store.ts"],"names":[],"mappings":"AAAA,KAAK,UAAU,GAAG,MAAM,IAAI,CAAA;AAC5B,KAAK,OAAO,CAAC,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC,KAAK,CAAC,CAAA;AACrC,KAAK,aAAa,GAAG;IAAE,KAAK,CAAC,EAAE,OAAO,CAAA;CAAE,CAAA;AAExC,qBAAa,KAAK,CAAC,CAAC;;gBAIN,YAAY,EAAE,CAAC;IAI3B,IAAI,KAAK,MAER;IAED,GAAG,IAAI,CAAC;IAIR,GAAG,CAAC,KAAK,EAAE,CAAC,EAAE,OAAO,CAAC,EAAE,aAAa,GAAG,OAAO;IAI/C,MAAM,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,CAAC,EAAE,aAAa,GAAG,OAAO;IAI7D,SAAS,CAAC,EAAE,EAAE,UAAU,GAAG,MAAM,IAAI;CAqBtC"}
1
+ {"version":3,"file":"Store.d.ts","sourceRoot":"","sources":["../src/Store.ts"],"names":[],"mappings":"AAAA,KAAK,UAAU,GAAG,MAAM,IAAI,CAAA;AAC5B,KAAK,OAAO,CAAC,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC,KAAK,CAAC,CAAA;AACrC,KAAK,aAAa,GAAG;IAAE,KAAK,CAAC,EAAE,OAAO,CAAA;CAAE,CAAA;AAMxC;;;GAGG;AACH,qBAAa,KAAK,CAAC,CAAC;;gBAMN,YAAY,EAAE,CAAC;IAI3B;;;;OAIG;IACH,SAAS,CAAC,EAAE,EAAE,UAAU,GAAG,MAAM,IAAI;IAKrC,iCAAiC;IACjC,GAAG,IAAI,CAAC;IAIR;;;;;OAKG;IACH,GAAG,CAAC,KAAK,EAAE,CAAC,EAAE,OAAO,CAAC,EAAE,aAAa;IAKrC;;;;;OAKG;IACH,MAAM,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,CAAC,EAAE,aAAa;CA0CpD"}
package/dist/Store.js CHANGED
@@ -1,37 +1,82 @@
1
+ /**
2
+ * Reactive state container with subscription-based change notification.
3
+ * @typeParam T - The type of the stored state.
4
+ */
1
5
  export class Store {
2
6
  #subscribers = new Set();
7
+ #queue = [];
8
+ #notifying = false;
3
9
  #state;
4
10
  constructor(initialState) {
5
11
  this.#state = initialState;
6
12
  }
7
- get state() {
8
- return this.#state;
13
+ /**
14
+ * Registers a subscriber that is called whenever the state changes.
15
+ * @param fn - Callback invoked on each state change.
16
+ * @returns An unsubscribe function that removes the subscriber.
17
+ */
18
+ subscribe(fn) {
19
+ this.#subscribers.add(fn);
20
+ return () => this.#subscribers.delete(fn);
9
21
  }
22
+ /** Returns the current state. */
10
23
  get() {
11
24
  return this.#state;
12
25
  }
26
+ /**
27
+ * Replaces the state and notifies subscribers.
28
+ * Skipped if the value is identical (`Object.is`), unless `force` is set.
29
+ * @param state - The new state value.
30
+ * @param options - Pass `{ force: true }` to notify even when unchanged.
31
+ */
13
32
  set(state, options) {
14
- return this.#notify(state, options);
33
+ this.#queue.push({ updater: () => state, options });
34
+ return this.#flush();
15
35
  }
36
+ /**
37
+ * Derives the next state via an updater function and notifies subscribers.
38
+ * Re-entrant calls from within a subscriber are queued and flushed in FIFO order.
39
+ * @param updater - Receives the current state and returns the next state.
40
+ * @param options - Pass `{ force: true }` to notify even when unchanged.
41
+ */
16
42
  update(updater, options) {
17
- return this.#notify(updater(this.#state), options);
43
+ this.#queue.push({ updater, options });
44
+ return this.#flush();
18
45
  }
19
- subscribe(fn) {
20
- this.#subscribers.add(fn);
21
- return () => this.#subscribers.delete(fn);
46
+ #flush() {
47
+ if (this.#notifying) {
48
+ return;
49
+ }
50
+ this.#notifying = true;
51
+ try {
52
+ let q = this.#queue.shift();
53
+ while (q) {
54
+ const state = q.updater(this.#state);
55
+ this.#commit(state, q.options);
56
+ q = this.#queue.shift();
57
+ }
58
+ }
59
+ catch (error) {
60
+ this.#queue.length = 0;
61
+ throw error;
62
+ }
63
+ finally {
64
+ this.#notifying = false;
65
+ }
22
66
  }
23
- /**
24
- * @returns `true` if listeners were notified
25
- */
26
- #notify(nextState, options) {
67
+ #commit(nextState, options) {
27
68
  if (!options?.force && Object.is(nextState, this.#state)) {
28
- return false;
69
+ return;
29
70
  }
30
71
  this.#state = nextState;
31
72
  for (const listener of [...this.#subscribers]) {
32
- listener();
73
+ try {
74
+ listener();
75
+ }
76
+ catch (error) {
77
+ console.error('Error in subscriber', error);
78
+ }
33
79
  }
34
- return true;
35
80
  }
36
81
  }
37
82
  //# sourceMappingURL=Store.js.map
package/dist/Store.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"Store.js","sourceRoot":"","sources":["../src/Store.ts"],"names":[],"mappings":"AAIA,MAAM,OAAO,KAAK;IACP,YAAY,GAAG,IAAI,GAAG,EAAc,CAAA;IAC7C,MAAM,CAAG;IAET,YAAY,YAAe;QACzB,IAAI,CAAC,MAAM,GAAG,YAAY,CAAA;IAC5B,CAAC;IAED,IAAI,KAAK;QACP,OAAO,IAAI,CAAC,MAAM,CAAA;IACpB,CAAC;IAED,GAAG;QACD,OAAO,IAAI,CAAC,MAAM,CAAA;IACpB,CAAC;IAED,GAAG,CAAC,KAAQ,EAAE,OAAuB;QACnC,OAAO,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,OAAO,CAAC,CAAA;IACrC,CAAC;IAED,MAAM,CAAC,OAAmB,EAAE,OAAuB;QACjD,OAAO,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC,CAAA;IACpD,CAAC;IAED,SAAS,CAAC,EAAc;QACtB,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,EAAE,CAAC,CAAA;QACzB,OAAO,GAAG,EAAE,CAAC,IAAI,CAAC,YAAY,CAAC,MAAM,CAAC,EAAE,CAAC,CAAA;IAC3C,CAAC;IAED;;OAEG;IACH,OAAO,CAAC,SAAY,EAAE,OAAuB;QAC3C,IAAI,CAAC,OAAO,EAAE,KAAK,IAAI,MAAM,CAAC,EAAE,CAAC,SAAS,EAAE,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC;YACzD,OAAO,KAAK,CAAA;QACd,CAAC;QAED,IAAI,CAAC,MAAM,GAAG,SAAS,CAAA;QAEvB,KAAK,MAAM,QAAQ,IAAI,CAAC,GAAG,IAAI,CAAC,YAAY,CAAC,EAAE,CAAC;YAC9C,QAAQ,EAAE,CAAA;QACZ,CAAC;QAED,OAAO,IAAI,CAAA;IACb,CAAC;CACF"}
1
+ {"version":3,"file":"Store.js","sourceRoot":"","sources":["../src/Store.ts"],"names":[],"mappings":"AAQA;;;GAGG;AACH,MAAM,OAAO,KAAK;IACP,YAAY,GAAG,IAAI,GAAG,EAAc,CAAA;IACpC,MAAM,GAAmB,EAAE,CAAA;IACpC,UAAU,GAAG,KAAK,CAAA;IAClB,MAAM,CAAG;IAET,YAAY,YAAe;QACzB,IAAI,CAAC,MAAM,GAAG,YAAY,CAAA;IAC5B,CAAC;IAED;;;;OAIG;IACH,SAAS,CAAC,EAAc;QACtB,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,EAAE,CAAC,CAAA;QACzB,OAAO,GAAG,EAAE,CAAC,IAAI,CAAC,YAAY,CAAC,MAAM,CAAC,EAAE,CAAC,CAAA;IAC3C,CAAC;IAED,iCAAiC;IACjC,GAAG;QACD,OAAO,IAAI,CAAC,MAAM,CAAA;IACpB,CAAC;IAED;;;;;OAKG;IACH,GAAG,CAAC,KAAQ,EAAE,OAAuB;QACnC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,CAAC,CAAA;QACnD,OAAO,IAAI,CAAC,MAAM,EAAE,CAAA;IACtB,CAAC;IAED;;;;;OAKG;IACH,MAAM,CAAC,OAAmB,EAAE,OAAuB;QACjD,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC,CAAA;QACtC,OAAO,IAAI,CAAC,MAAM,EAAE,CAAA;IACtB,CAAC;IAED,MAAM;QACJ,IAAI,IAAI,CAAC,UAAU,EAAE,CAAC;YACpB,OAAM;QACR,CAAC;QAED,IAAI,CAAC,UAAU,GAAG,IAAI,CAAA;QAEtB,IAAI,CAAC;YACH,IAAI,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,CAAA;YAC3B,OAAO,CAAC,EAAE,CAAC;gBACT,MAAM,KAAK,GAAG,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,CAAA;gBACpC,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC,CAAC,OAAO,CAAC,CAAA;gBAC9B,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,CAAA;YACzB,CAAC;QACH,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAA;YACtB,MAAM,KAAK,CAAA;QACb,CAAC;gBAAS,CAAC;YACT,IAAI,CAAC,UAAU,GAAG,KAAK,CAAA;QACzB,CAAC;IACH,CAAC;IAED,OAAO,CAAC,SAAY,EAAE,OAAuB;QAC3C,IAAI,CAAC,OAAO,EAAE,KAAK,IAAI,MAAM,CAAC,EAAE,CAAC,SAAS,EAAE,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC;YACzD,OAAM;QACR,CAAC;QAED,IAAI,CAAC,MAAM,GAAG,SAAS,CAAA;QAEvB,KAAK,MAAM,QAAQ,IAAI,CAAC,GAAG,IAAI,CAAC,YAAY,CAAC,EAAE,CAAC;YAC9C,IAAI,CAAC;gBACH,QAAQ,EAAE,CAAA;YACZ,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,OAAO,CAAC,KAAK,CAAC,qBAAqB,EAAE,KAAK,CAAC,CAAA;YAC7C,CAAC;QACH,CAAC;IACH,CAAC;CACF"}
@@ -1,5 +1,5 @@
1
1
  import { Store } from './Store';
2
- declare function createStore<T>(state: T): Store<T>;
3
2
  declare function createStore<T = undefined>(): Store<T | undefined>;
3
+ declare function createStore<T>(state: T): Store<T>;
4
4
  export { createStore };
5
5
  //# sourceMappingURL=createStore.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"createStore.d.ts","sourceRoot":"","sources":["../src/createStore.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAA;AAE/B,iBAAS,WAAW,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC,CAAA;AAC3C,iBAAS,WAAW,CAAC,CAAC,GAAG,SAAS,KAAK,KAAK,CAAC,CAAC,GAAG,SAAS,CAAC,CAAA;AAK3D,OAAO,EAAE,WAAW,EAAE,CAAA"}
1
+ {"version":3,"file":"createStore.d.ts","sourceRoot":"","sources":["../src/createStore.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAA;AAE/B,iBAAS,WAAW,CAAC,CAAC,GAAG,SAAS,KAAK,KAAK,CAAC,CAAC,GAAG,SAAS,CAAC,CAAA;AAC3D,iBAAS,WAAW,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC,CAAA;AAK3C,OAAO,EAAE,WAAW,EAAE,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"createStore.js","sourceRoot":"","sources":["../src/createStore.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAA;AAI/B,SAAS,WAAW,CAAI,KAAqB;IAC3C,OAAO,IAAI,KAAK,CAAC,KAAK,CAAC,CAAA;AACzB,CAAC;AAED,OAAO,EAAE,WAAW,EAAE,CAAA"}
1
+ {"version":3,"file":"createStore.js","sourceRoot":"","sources":["../src/createStore.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAA;AAI/B,SAAS,WAAW,CAAI,KAAS;IAC/B,OAAO,IAAI,KAAK,CAAC,KAAK,CAAC,CAAA;AACzB,CAAC;AAED,OAAO,EAAE,WAAW,EAAE,CAAA"}
@@ -1,6 +1,25 @@
1
1
  import type { Store } from './Store';
2
2
  type Selector<T, U> = (state: T) => U;
3
+ /**
4
+ * Subscribes a React component to a {@link Store} and returns its current state.
5
+ *
6
+ * @param store - The store to subscribe to.
7
+ * @returns The current state of the store.
8
+ */
3
9
  declare function useStore<T>(store: Store<T>): T;
10
+ /**
11
+ * Subscribes a React component to a {@link Store} and returns a derived value
12
+ * computed by the selector.
13
+ *
14
+ * The selector should return a referentially stable value (e.g. a primitive or
15
+ * an existing object reference) to avoid unnecessary re-renders, since snapshots
16
+ * are compared with `Object.is`.
17
+ *
18
+ * @param store - The store to subscribe to.
19
+ * @param selector - A function that derives a value from the store state.
20
+ * @returns The value returned by the selector.
21
+ */
4
22
  declare function useStore<T, U>(store: Store<T>, selector: Selector<T, U>): U;
5
23
  export { useStore };
24
+ export type { Selector };
6
25
  //# sourceMappingURL=useStore.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"useStore.d.ts","sourceRoot":"","sources":["../src/useStore.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAA;AAEpC,KAAK,QAAQ,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC,KAAK,CAAC,CAAA;AAErC,iBAAS,QAAQ,CAAC,CAAC,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,CAAA;AACxC,iBAAS,QAAQ,CAAC,CAAC,EAAE,CAAC,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,QAAQ,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,CAAC,CAAA;AAkBrE,OAAO,EAAE,QAAQ,EAAE,CAAA"}
1
+ {"version":3,"file":"useStore.d.ts","sourceRoot":"","sources":["../src/useStore.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAA;AAEpC,KAAK,QAAQ,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC,KAAK,CAAC,CAAA;AAErC;;;;;GAKG;AACH,iBAAS,QAAQ,CAAC,CAAC,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,CAAA;AAExC;;;;;;;;;;;GAWG;AACH,iBAAS,QAAQ,CAAC,CAAC,EAAE,CAAC,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,QAAQ,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,CAAC,CAAA;AA0BrE,OAAO,EAAE,QAAQ,EAAE,CAAA;AACnB,YAAY,EAAE,QAAQ,EAAE,CAAA"}
package/dist/useStore.js CHANGED
@@ -1,13 +1,13 @@
1
- import { useDebugValue, useMemo, useSyncExternalStore } from 'react';
1
+ import { useDebugValue, useMemo, useRef, useSyncExternalStore } from 'react';
2
2
  function useStore(store, selector) {
3
- const value = useSyncExternalStore(...useMemo(() => {
4
- const getSnapshot = () => (selector ? selector(store.get()) : store.get());
5
- return [
6
- (onChange) => store.subscribe(onChange),
7
- getSnapshot,
8
- getSnapshot,
9
- ];
10
- }, [store, selector]));
3
+ const selectorRef = useRef(undefined);
4
+ selectorRef.current = selector;
5
+ const args = useMemo(() => {
6
+ const subscribe = store.subscribe.bind(store);
7
+ const getSnapshot = () => selectorRef.current ? selectorRef.current(store.get()) : store.get();
8
+ return { subscribe, getSnapshot };
9
+ }, [store]);
10
+ const value = useSyncExternalStore(args.subscribe, args.getSnapshot, args.getSnapshot);
11
11
  useDebugValue(value);
12
12
  return value;
13
13
  }
@@ -1 +1 @@
1
- {"version":3,"file":"useStore.js","sourceRoot":"","sources":["../src/useStore.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,OAAO,EAAE,oBAAoB,EAAE,MAAM,OAAO,CAAA;AAOpE,SAAS,QAAQ,CAAW,KAAe,EAAE,QAAyB;IACpE,MAAM,KAAK,GAAG,oBAAoB,CAChC,GAAG,OAAO,CAAC,GAAG,EAAE;QACd,MAAM,WAAW,GAAG,GAAG,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,CAAA;QAC1E,OAAO;YACL,CAAC,QAAoB,EAAE,EAAE,CAAC,KAAK,CAAC,SAAS,CAAC,QAAQ,CAAC;YACnD,WAAW;YACX,WAAW;SACsC,CAAA;IACrD,CAAC,EAAE,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAC,CACtB,CAAA;IAED,aAAa,CAAC,KAAK,CAAC,CAAA;IAEpB,OAAO,KAAK,CAAA;AACd,CAAC;AAED,OAAO,EAAE,QAAQ,EAAE,CAAA"}
1
+ {"version":3,"file":"useStore.js","sourceRoot":"","sources":["../src/useStore.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,OAAO,EAAE,MAAM,EAAE,oBAAoB,EAAE,MAAM,OAAO,CAAA;AA2B5E,SAAS,QAAQ,CAAW,KAAe,EAAE,QAAyB;IACpE,MAAM,WAAW,GAAG,MAAM,CAAkB,SAAS,CAAC,CAAA;IACtD,WAAW,CAAC,OAAO,GAAG,QAAQ,CAAA;IAE9B,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,EAAE;QACxB,MAAM,SAAS,GAAG,KAAK,CAAC,SAAS,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;QAE7C,MAAM,WAAW,GAAG,GAAG,EAAE,CACvB,WAAW,CAAC,OAAO,CAAC,CAAC,CAAC,WAAW,CAAC,OAAO,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAA;QAEtE,OAAO,EAAE,SAAS,EAAE,WAAW,EAAE,CAAA;IACnC,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC,CAAA;IAEX,MAAM,KAAK,GAAG,oBAAoB,CAChC,IAAI,CAAC,SAAS,EACd,IAAI,CAAC,WAAW,EAChB,IAAI,CAAC,WAAW,CACjB,CAAA;IAED,aAAa,CAAC,KAAK,CAAC,CAAA;IAEpB,OAAO,KAAK,CAAA;AACd,CAAC;AAED,OAAO,EAAE,QAAQ,EAAE,CAAA"}
package/package.json CHANGED
@@ -1,41 +1,54 @@
1
1
  {
2
2
  "description": "Yet another state management in React",
3
- "keywords": [],
4
3
  "license": "MIT",
5
- "main": "./dist/index.js",
6
- "module": "./dist/index.js",
7
4
  "name": "@dvashim/store",
8
5
  "type": "module",
9
6
  "types": "./dist/index.d.ts",
10
- "version": "1.1.3",
7
+ "version": "1.2.0",
11
8
  "author": {
12
9
  "email": "aleksei@dvashim.dev",
13
10
  "name": "Aleksei Reznichenko"
14
11
  },
15
- "dependencies": {
16
- "react": "^19.2.4",
17
- "react-dom": "^19.2.4"
18
- },
19
12
  "devDependencies": {
20
- "@biomejs/biome": "^2.4.4",
21
- "@changesets/changelog-github": "^0.5.2",
22
- "@changesets/cli": "^2.29.8",
23
- "@dvashim/biome-config": "^1.2.1",
13
+ "@biomejs/biome": "^2.4.6",
14
+ "@changesets/changelog-github": "^0.6.0",
15
+ "@changesets/cli": "^2.30.0",
16
+ "@dvashim/biome-config": "^1.3.1",
24
17
  "@dvashim/typescript-config": "^1.1.9",
18
+ "@testing-library/react": "^16.3.2",
19
+ "@types/node": "^25.3.5",
25
20
  "@types/react": "^19.2.14",
26
21
  "@types/react-dom": "^19.2.3",
22
+ "jsdom": "^28.1.0",
23
+ "react": "^19.2.4",
24
+ "react-dom": "^19.2.4",
27
25
  "rimraf": "^6.1.3",
28
- "typescript": "^5.9.3"
26
+ "typescript": "^5.9.3",
27
+ "vitest": "^4.0.18"
29
28
  },
30
29
  "exports": {
31
30
  ".": {
31
+ "types": "./dist/index.d.ts",
32
32
  "import": "./dist/index.js",
33
- "types": "./dist/index.d.ts"
33
+ "default": "./dist/index.js"
34
34
  }
35
35
  },
36
36
  "files": [
37
37
  "dist"
38
38
  ],
39
+ "keywords": [
40
+ "react",
41
+ "state-manager",
42
+ "useSyncExternalStore",
43
+ "store",
44
+ "minimal",
45
+ "lightweight",
46
+ "typescript"
47
+ ],
48
+ "peerDependencies": {
49
+ "react": "^18.0.0 || ^19.0.0",
50
+ "react-dom": "^18.0.0 || ^19.0.0"
51
+ },
39
52
  "publishConfig": {
40
53
  "access": "public",
41
54
  "provenance": true
@@ -45,12 +58,14 @@
45
58
  "url": "https://github.com/dvashim/store.git"
46
59
  },
47
60
  "scripts": {
48
- "build": "tsc --build",
61
+ "build": "tsc --project tsconfig.dev.json",
49
62
  "changeset": "changeset",
50
- "check": "pnpm clean && pnpm run \"/^check:.*/\"",
63
+ "check": "pnpm run \"/^check:.*/\"",
51
64
  "check:biome": "biome check",
52
65
  "check:ts": "tsc --noemit",
53
66
  "clean": "rm -rf dist *.tsbuildinfo",
67
+ "test": "vitest run",
68
+ "test:watch": "vitest",
54
69
  "watch": "pnpm build --watch"
55
70
  }
56
71
  }