@pitboxdev/dynamic-store-zustand 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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Pitboxdev
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,482 @@
1
+ # @pitboxdev/dynamic-store-zustand
2
+
3
+ > Two complementary approaches to scalable, type-safe state management in React — both built on top of [Zustand](https://github.com/pmndrs/zustand).
4
+
5
+ [![npm version](https://img.shields.io/npm/v/@pitboxdev/dynamic-store-zustand.svg)](https://www.npmjs.com/package/@pitboxdev/dynamic-store-zustand)
6
+ [![license](https://img.shields.io/npm/l/@pitboxdev/dynamic-store-zustand.svg)](./LICENSE)
7
+ [![TypeScript](https://img.shields.io/badge/TypeScript-strict-blue.svg)](https://www.typescriptlang.org/)
8
+
9
+ ---
10
+
11
+ ## Table of Contents
12
+
13
+ - [Overview](#overview)
14
+ - [Installation](#installation)
15
+ - [API 1 — `createDynamicStore`](#api-1--createdynamicstore)
16
+ - [Quick Start](#quick-start)
17
+ - [Outside React](#outside-react)
18
+ - [TypeScript](#typescript)
19
+ - [API 2 — `useDynamicStore`](#api-2--usedynamicstore)
20
+ - [Quick Start](#quick-start-1)
21
+ - [Functional updater (`setData`)](#functional-updater-setdata)
22
+ - [Auto-cleanup with `useDynamicStoreWithCleanup`](#auto-cleanup-with-usedynamicstorewithcleanup)
23
+ - [Imperative helpers (outside React)](#imperative-helpers-outside-react)
24
+ - [Config options](#config-options)
25
+ - [TypeScript](#typescript-1)
26
+ - [When to use which API](#when-to-use-which-api)
27
+ - [Full API Reference](#full-api-reference)
28
+ - [License](#license)
29
+
30
+ ---
31
+
32
+ ## Overview
33
+
34
+ The package ships **two independent APIs** that can be used separately or together:
35
+
36
+ | | `createDynamicStore` | `useDynamicStore` |
37
+ |---|---|---|
38
+ | Style | Factory function | React hook |
39
+ | Stores | One Zustand store per call | All stores live in a single registry |
40
+ | Actions | Explicit, typed | Implicit via `setData` |
41
+ | Functional updater | Via `set((s) => …)` in actions | Built-in `setData((prev) => …)` |
42
+ | Auto-cleanup | — | `useDynamicStoreWithCleanup` |
43
+ | Navigation persistence | — | `persistOnNavigation` config flag |
44
+ | Best for | Permanent, feature-level stores | Page/form/modal scoped state |
45
+
46
+ ---
47
+
48
+ ## Installation
49
+
50
+ ```bash
51
+ npm install @pitboxdev/dynamic-store-zustand zustand
52
+ # or
53
+ yarn add @pitboxdev/dynamic-store-zustand zustand
54
+ # or
55
+ pnpm add @pitboxdev/dynamic-store-zustand zustand
56
+ ```
57
+
58
+ > **Peer dependencies:** `react >= 18` and `zustand >= 5` must be installed in your project.
59
+
60
+ ---
61
+
62
+ ## API 1 — `createDynamicStore`
63
+
64
+ A factory function that creates a standalone, fully typed Zustand store from a configuration object with explicit actions.
65
+
66
+ ### Quick Start
67
+
68
+ ```tsx
69
+ import { createDynamicStore } from "@pitboxdev/dynamic-store-zustand";
70
+
71
+ const { useStore } = createDynamicStore({
72
+ initialState: { count: 0 },
73
+ actions: (set) => ({
74
+ increment: () => set((s) => ({ count: s.count + 1 })),
75
+ decrement: () => set((s) => ({ count: s.count - 1 })),
76
+ reset: () => set({ count: 0 }),
77
+ }),
78
+ });
79
+
80
+ function Counter() {
81
+ const count = useStore((s) => s.count);
82
+ const { increment, decrement, reset } = useStore();
83
+
84
+ return (
85
+ <div>
86
+ <button onClick={decrement}>-</button>
87
+ <span>{count}</span>
88
+ <button onClick={increment}>+</button>
89
+ <button onClick={reset}>Reset</button>
90
+ </div>
91
+ );
92
+ }
93
+ ```
94
+
95
+ ### Outside React
96
+
97
+ ```ts
98
+ const { store } = createDynamicStore({ initialState: { count: 0 }, actions: (set) => ({
99
+ increment: () => set((s) => ({ count: s.count + 1 })),
100
+ }) });
101
+
102
+ // Read
103
+ const count = store.getState().count;
104
+
105
+ // Write
106
+ store.setState({ count: 42 });
107
+
108
+ // Subscribe
109
+ const unsub = store.subscribe((state) => console.log(state.count));
110
+ unsub();
111
+ ```
112
+
113
+ ### TypeScript
114
+
115
+ All types are inferred automatically. You can also define them explicitly:
116
+
117
+ ```ts
118
+ import {
119
+ createDynamicStore,
120
+ type DynamicStoreConfig,
121
+ } from "@pitboxdev/dynamic-store-zustand";
122
+
123
+ interface CounterState { count: number }
124
+ interface CounterActions {
125
+ increment: () => void;
126
+ decrement: () => void;
127
+ reset: () => void;
128
+ }
129
+
130
+ const config: DynamicStoreConfig<CounterState, CounterActions> = {
131
+ initialState: { count: 0 },
132
+ actions: (set) => ({
133
+ increment: () => set((s) => ({ count: s.count + 1 })),
134
+ decrement: () => set((s) => ({ count: s.count - 1 })),
135
+ reset: () => set({ count: 0 }),
136
+ }),
137
+ };
138
+
139
+ const { useStore, store } = createDynamicStore(config);
140
+ ```
141
+
142
+ Using multiple selectors with shallow equality:
143
+
144
+ ```ts
145
+ import { useShallow } from "zustand/react/shallow";
146
+
147
+ const { count, increment } = useStore(
148
+ useShallow((s) => ({ count: s.count, increment: s.increment }))
149
+ );
150
+ ```
151
+
152
+ ---
153
+
154
+ ## API 2 — `useDynamicStore`
155
+
156
+ A hook that stores state in a single shared registry keyed by a string `storeId`. `setData` works exactly like React's `useState` setter — it accepts either a partial object or a function that receives the previous state.
157
+
158
+ ### Quick Start
159
+
160
+ ```tsx
161
+ import { useDynamicStore } from "@pitboxdev/dynamic-store-zustand";
162
+
163
+ interface CounterState {
164
+ value: number;
165
+ step: number;
166
+ }
167
+
168
+ const initial: CounterState = { value: 0, step: 1 };
169
+
170
+ function Counter() {
171
+ const { data, setData, reset } = useDynamicStore<CounterState>("counter", {
172
+ initialState: initial,
173
+ });
174
+
175
+ return (
176
+ <div>
177
+ <p>Count: {data.value}</p>
178
+
179
+ {/* Object update */}
180
+ <button onClick={() => setData({ value: data.value + data.step })}>
181
+ + (simple)
182
+ </button>
183
+
184
+ {/* Functional update — always reads the latest state */}
185
+ <button onClick={() => setData((prev) => ({ value: prev.value + prev.step }))}>
186
+ + (functional)
187
+ </button>
188
+
189
+ <button onClick={reset}>Reset</button>
190
+ </div>
191
+ );
192
+ }
193
+ ```
194
+
195
+ ### Functional updater (`setData`)
196
+
197
+ `setData` accepts two forms:
198
+
199
+ ```ts
200
+ // 1. Partial object — merges into current state
201
+ setData({ value: 42 });
202
+
203
+ // 2. Updater function — receives the latest state, returns a partial update
204
+ setData((prev) => ({ value: prev.value + 1 }));
205
+ ```
206
+
207
+ **Why use the functional form?**
208
+
209
+ Without it, rapid successive calls all see the same snapshot:
210
+
211
+ ```ts
212
+ // ❌ race condition — both calls read the same stale value
213
+ setData({ value: data.value + 1 });
214
+ setData({ value: data.value + 1 }); // data.value is still the old value
215
+
216
+ // ✅ functional — each call receives the result of the previous one
217
+ setData((prev) => ({ value: prev.value + 1 }));
218
+ setData((prev) => ({ value: prev.value + 1 }));
219
+ ```
220
+
221
+ Always prefer the functional form when the new state depends on the old state.
222
+
223
+ #### Todo list example
224
+
225
+ ```tsx
226
+ interface Todo { id: string; text: string; done: boolean }
227
+ interface TodosState { items: Todo[]; filter: "all" | "active" | "done" }
228
+
229
+ function TodoList() {
230
+ const { data, setData } = useDynamicStore<TodosState>("todos", {
231
+ initialState: { items: [], filter: "all" },
232
+ });
233
+
234
+ const addTodo = (text: string) => {
235
+ setData((prev) => ({
236
+ items: [...prev.items, { id: Date.now().toString(), text, done: false }],
237
+ }));
238
+ };
239
+
240
+ const toggle = (id: string) => {
241
+ setData((prev) => ({
242
+ items: prev.items.map((t) =>
243
+ t.id === id ? { ...t, done: !t.done } : t
244
+ ),
245
+ }));
246
+ };
247
+
248
+ const clearDone = () => {
249
+ setData((prev) => ({
250
+ items: prev.items.filter((t) => !t.done),
251
+ filter: "all",
252
+ }));
253
+ };
254
+
255
+ // ...
256
+ }
257
+ ```
258
+
259
+ #### Shopping cart example
260
+
261
+ ```tsx
262
+ interface CartItem { id: string; name: string; price: number; quantity: number }
263
+ interface CartState { items: CartItem[]; discount: number }
264
+
265
+ function Cart() {
266
+ const { data, setData } = useDynamicStore<CartState>("cart", {
267
+ initialState: { items: [], discount: 0 },
268
+ persistOnNavigation: true,
269
+ });
270
+
271
+ const addItem = (product: Omit<CartItem, "quantity">) => {
272
+ setData((prev) => {
273
+ const exists = prev.items.find((i) => i.id === product.id);
274
+ return {
275
+ items: exists
276
+ ? prev.items.map((i) =>
277
+ i.id === product.id ? { ...i, quantity: i.quantity + 1 } : i
278
+ )
279
+ : [...prev.items, { ...product, quantity: 1 }],
280
+ };
281
+ });
282
+ };
283
+
284
+ const total = data.items.reduce(
285
+ (sum, i) => sum + i.price * i.quantity,
286
+ 0
287
+ ) * (1 - data.discount / 100);
288
+
289
+ // ...
290
+ }
291
+ ```
292
+
293
+ ### Auto-cleanup with `useDynamicStoreWithCleanup`
294
+
295
+ `useDynamicStoreWithCleanup` works identically to `useDynamicStore` but resets the store when the component unmounts — useful for modal dialogs, wizard steps, or edit forms.
296
+
297
+ ```tsx
298
+ import { useDynamicStoreWithCleanup } from "@pitboxdev/dynamic-store-zustand";
299
+
300
+ function EditModal() {
301
+ const { data, setData } = useDynamicStoreWithCleanup<FormState>(
302
+ "editForm",
303
+ { initialState: { name: "", email: "" }, resetOnUnmount: true }
304
+ );
305
+
306
+ // State is automatically reset when the modal closes
307
+ }
308
+ ```
309
+
310
+ ### Imperative helpers (outside React)
311
+
312
+ All helpers call into the shared manager directly — no hook required.
313
+
314
+ ```ts
315
+ import {
316
+ updateDynamicStore,
317
+ resetDynamicStore,
318
+ resetAllDynamicStores,
319
+ resetNonPersistentDynamicStores,
320
+ } from "@pitboxdev/dynamic-store-zustand";
321
+
322
+ // Merge data into a store
323
+ updateDynamicStore("cart", { discount: 20 });
324
+
325
+ // Reset one store to its initial state
326
+ resetDynamicStore("editForm");
327
+
328
+ // Reset every store
329
+ resetAllDynamicStores();
330
+
331
+ // Reset only stores without persistOnNavigation: true (e.g. on route change)
332
+ resetNonPersistentDynamicStores();
333
+ ```
334
+
335
+ ### Config options
336
+
337
+ | Option | Type | Default | Description |
338
+ |---|---|---|---|
339
+ | `initialState` | `T` | `{}` | Initial values; also used when `reset()` is called |
340
+ | `persistOnNavigation` | `boolean` | `false` | Skip reset when `resetNonPersistentDynamicStores()` is called |
341
+ | `resetOnUnmount` | `boolean` | `false` | Auto-reset when the component unmounts (`useDynamicStoreWithCleanup` only) |
342
+
343
+ ### TypeScript
344
+
345
+ ```ts
346
+ import {
347
+ useDynamicStore,
348
+ type StoreConfig,
349
+ type SetStateAction,
350
+ type UseDynamicStoreReturn,
351
+ } from "@pitboxdev/dynamic-store-zustand";
352
+
353
+ interface FormState {
354
+ firstName: string;
355
+ lastName: string;
356
+ age: number;
357
+ agreed: boolean;
358
+ }
359
+
360
+ const config: StoreConfig<FormState> = {
361
+ initialState: { firstName: "", lastName: "", age: 0, agreed: false },
362
+ resetOnUnmount: true,
363
+ };
364
+
365
+ function RegistrationForm() {
366
+ const { data, setData, reset }: UseDynamicStoreReturn<FormState> =
367
+ useDynamicStore<FormState>("regForm", config);
368
+
369
+ // TypeScript will error on unknown keys:
370
+ // setData({ unknown: true }); ❌
371
+
372
+ const setAge = (age: number) => {
373
+ setData((prev) => ({
374
+ age,
375
+ // Clear consent when user is under 18
376
+ agreed: age < 18 ? false : prev.agreed,
377
+ }));
378
+ };
379
+
380
+ // ...
381
+ }
382
+ ```
383
+
384
+ ---
385
+
386
+ ## When to use which API
387
+
388
+ ### Use `createDynamicStore` when you need:
389
+
390
+ - A **permanent, feature-level store** (auth, theme, user profile, global UI)
391
+ - **Explicit, named actions** that encapsulate business logic
392
+ - **Fine-grained selectors** and subscriptions outside React
393
+ - The full Zustand API (middleware, persist, devtools, subscribe)
394
+
395
+ ```ts
396
+ // auth.store.ts
397
+ export const { useStore: useAuthStore, store: authStore } = createDynamicStore({
398
+ initialState: { user: null as User | null, token: "" },
399
+ actions: (set) => ({
400
+ login: (user: User, token: string) => set({ user, token }),
401
+ logout: () => set({ user: null, token: "" }),
402
+ }),
403
+ });
404
+ ```
405
+
406
+ ### Use `useDynamicStore` when you need:
407
+
408
+ - **Page / route / modal scoped state** with optional auto-cleanup
409
+ - **useState-like ergonomics** without boilerplate action definitions
410
+ - **Multiple stores** managed centrally with shared reset helpers
411
+ - Quick iteration when action interfaces aren't stable yet
412
+
413
+ ```tsx
414
+ // Inside a wizard step component
415
+ const { data, setData } = useDynamicStoreWithCleanup<StepState>(
416
+ "wizard-step-2",
417
+ { initialState: { selection: null }, resetOnUnmount: true }
418
+ );
419
+ ```
420
+
421
+ ---
422
+
423
+ ## Full API Reference
424
+
425
+ ### `createDynamicStore(config)`
426
+
427
+ | Parameter | Type | Description |
428
+ |---|---|---|
429
+ | `config.initialState` | `TState` | Plain object representing the initial state |
430
+ | `config.actions` | `(set, get) => TActions` | Factory that returns named action implementations |
431
+
432
+ Returns `{ useStore, store }`.
433
+
434
+ ---
435
+
436
+ ### `useDynamicStore<T>(storeId, config?)`
437
+
438
+ | Parameter | Type | Description |
439
+ |---|---|---|
440
+ | `storeId` | `string` | Unique key identifying this store in the registry |
441
+ | `config` | `StoreConfig<T>` | Optional config (see [Config options](#config-options)) |
442
+
443
+ Returns `{ data: T, setData, reset }`.
444
+
445
+ ---
446
+
447
+ ### `useDynamicStoreWithCleanup<T>(storeId, config?)`
448
+
449
+ Same signature as `useDynamicStore`. Calls `reset()` on component unmount when `config.resetOnUnmount` is `true`.
450
+
451
+ ---
452
+
453
+ ### Imperative helpers
454
+
455
+ | Function | Signature | Description |
456
+ |---|---|---|
457
+ | `updateDynamicStore` | `(storeId, data) => void` | Merge data into a store from outside React |
458
+ | `resetDynamicStore` | `(storeId) => void` | Reset one store to its `initialState` |
459
+ | `resetAllDynamicStores` | `() => void` | Reset every registered store |
460
+ | `resetNonPersistentDynamicStores` | `() => void` | Reset stores where `persistOnNavigation` is not `true` |
461
+
462
+ ---
463
+
464
+ ### Exported types
465
+
466
+ | Type | Description |
467
+ |---|---|
468
+ | `StoreState` | `Record<string, unknown>` — base constraint for state objects |
469
+ | `StoreActions` | `Record<string, (...args) => unknown>` — base constraint for action maps |
470
+ | `DynamicStoreConfig<TState, TActions>` | Config type for `createDynamicStore` |
471
+ | `DynamicStore<TState, TActions>` | Return type of `createDynamicStore` |
472
+ | `StoreSlice<TState, TActions>` | Merged state + actions type |
473
+ | `StoreConfig<T>` | Config type for `useDynamicStore` |
474
+ | `SetStateAction<T>` | `Partial<T> \| ((prev: T) => Partial<T>)` — setter argument type |
475
+ | `DynamicStoreRegistry` | Internal registry entry (advanced use) |
476
+ | `UseDynamicStoreReturn<T>` | Return type of `useDynamicStore` |
477
+
478
+ ---
479
+
480
+ ## License
481
+
482
+ [MIT](./LICENSE) © Pitboxdev
@@ -0,0 +1,133 @@
1
+ import { UseBoundStore, StoreApi } from 'zustand';
2
+
3
+ /**
4
+ * Updater argument for setData — either a partial object or a function
5
+ * that receives the previous state and returns a partial object.
6
+ * Mirrors the React useState updater pattern.
7
+ */
8
+ type SetStateAction<T> = Partial<T> | ((prevState: T) => Partial<T>);
9
+ /**
10
+ * Configuration options for useDynamicStore / useDynamicStoreWithCleanup.
11
+ */
12
+ interface StoreConfig<T extends StoreState = StoreState> {
13
+ /** Keep state alive across navigation (not reset on resetNonPersistentStores). */
14
+ persistOnNavigation?: boolean;
15
+ /** Automatically reset state when the component unmounts. */
16
+ resetOnUnmount?: boolean;
17
+ /** Initial state values used on first mount and on reset. */
18
+ initialState?: T;
19
+ }
20
+ /**
21
+ * Internal registry entry stored per storeId in the manager.
22
+ */
23
+ interface DynamicStoreRegistry {
24
+ data: StoreState;
25
+ config: StoreConfig;
26
+ initialState: StoreState;
27
+ }
28
+ /**
29
+ * A plain object that can serve as store state.
30
+ * Keys are strings, values can be anything.
31
+ */
32
+ type StoreState = Record<string, unknown>;
33
+ /**
34
+ * Actions are functions attached to the store.
35
+ */
36
+ type StoreActions = Record<string, (...args: unknown[]) => unknown>;
37
+ /**
38
+ * Full store slice = state + actions.
39
+ */
40
+ type StoreSlice<TState extends StoreState = StoreState, TActions extends StoreActions = StoreActions> = TState & TActions;
41
+ /**
42
+ * Configuration object passed to createDynamicStore.
43
+ */
44
+ interface DynamicStoreConfig<TState extends StoreState, TActions extends StoreActions> {
45
+ /** Initial state values */
46
+ initialState: TState;
47
+ /** Factory that receives `set` and `get` and returns action implementations */
48
+ actions: (set: StoreApi<StoreSlice<TState, TActions>>["setState"], get: StoreApi<StoreSlice<TState, TActions>>["getState"]) => TActions;
49
+ }
50
+ /**
51
+ * The return type of createDynamicStore.
52
+ */
53
+ interface DynamicStore<TState extends StoreState, TActions extends StoreActions> {
54
+ /** Zustand React hook */
55
+ useStore: UseBoundStore<StoreApi<StoreSlice<TState, TActions>>>;
56
+ /** Direct access to the underlying Zustand store API */
57
+ store: StoreApi<StoreSlice<TState, TActions>>;
58
+ }
59
+
60
+ /**
61
+ * Creates a dynamic Zustand store from a configuration object.
62
+ *
63
+ * @example
64
+ * ```ts
65
+ * const { useStore } = createDynamicStore({
66
+ * initialState: { count: 0 },
67
+ * actions: (set) => ({
68
+ * increment: () => set((s) => ({ count: s.count + 1 })),
69
+ * decrement: () => set((s) => ({ count: s.count - 1 })),
70
+ * reset: () => set({ count: 0 }),
71
+ * }),
72
+ * });
73
+ * ```
74
+ */
75
+ declare function createDynamicStore<TState extends StoreState, TActions extends StoreActions>(config: DynamicStoreConfig<TState, TActions>): DynamicStore<TState, TActions>;
76
+
77
+ interface UseDynamicStoreReturn<T extends StoreState> {
78
+ data: T;
79
+ setData: (updater: SetStateAction<T>) => void;
80
+ reset: () => void;
81
+ }
82
+ /**
83
+ * Hook-based dynamic store keyed by `storeId`.
84
+ *
85
+ * `setData` accepts either a partial object **or** a function that receives
86
+ * the previous state and returns a partial object — exactly like React's
87
+ * `useState` setter.
88
+ *
89
+ * @example
90
+ * ```tsx
91
+ * const { data, setData, reset } = useDynamicStore<CounterState>('counter', {
92
+ * initialState: { value: 0, step: 1 },
93
+ * });
94
+ *
95
+ * // object update
96
+ * setData({ value: 42 });
97
+ *
98
+ * // functional update — safe for rapid successive calls
99
+ * setData((prev) => ({ value: prev.value + prev.step }));
100
+ * ```
101
+ */
102
+ declare function useDynamicStore<T extends StoreState>(storeId: string, config?: StoreConfig<T>): UseDynamicStoreReturn<T>;
103
+ /**
104
+ * Same as `useDynamicStore` but automatically resets state when the
105
+ * component unmounts (when `config.resetOnUnmount` is `true`).
106
+ *
107
+ * @example
108
+ * ```tsx
109
+ * const { data, setData, reset } = useDynamicStoreWithCleanup<FormState>(
110
+ * 'editForm',
111
+ * { initialState, resetOnUnmount: true },
112
+ * );
113
+ * ```
114
+ */
115
+ declare function useDynamicStoreWithCleanup<T extends StoreState>(storeId: string, config?: StoreConfig<T>): UseDynamicStoreReturn<T>;
116
+ /**
117
+ * Update a dynamic store from outside a React component.
118
+ */
119
+ declare const updateDynamicStore: (storeId: string, data: StoreState) => void;
120
+ /**
121
+ * Reset a single dynamic store to its initial state from outside React.
122
+ */
123
+ declare const resetDynamicStore: (storeId: string) => void;
124
+ /**
125
+ * Reset all dynamic stores to their initial states from outside React.
126
+ */
127
+ declare const resetAllDynamicStores: () => void;
128
+ /**
129
+ * Reset only stores that do not have `persistOnNavigation: true`.
130
+ */
131
+ declare const resetNonPersistentDynamicStores: () => void;
132
+
133
+ export { type DynamicStore, type DynamicStoreConfig, type DynamicStoreRegistry, type SetStateAction, type StoreActions, type StoreConfig, type StoreSlice, type StoreState, type UseDynamicStoreReturn, createDynamicStore, resetAllDynamicStores, resetDynamicStore, resetNonPersistentDynamicStores, updateDynamicStore, useDynamicStore, useDynamicStoreWithCleanup };
@@ -0,0 +1,133 @@
1
+ import { UseBoundStore, StoreApi } from 'zustand';
2
+
3
+ /**
4
+ * Updater argument for setData — either a partial object or a function
5
+ * that receives the previous state and returns a partial object.
6
+ * Mirrors the React useState updater pattern.
7
+ */
8
+ type SetStateAction<T> = Partial<T> | ((prevState: T) => Partial<T>);
9
+ /**
10
+ * Configuration options for useDynamicStore / useDynamicStoreWithCleanup.
11
+ */
12
+ interface StoreConfig<T extends StoreState = StoreState> {
13
+ /** Keep state alive across navigation (not reset on resetNonPersistentStores). */
14
+ persistOnNavigation?: boolean;
15
+ /** Automatically reset state when the component unmounts. */
16
+ resetOnUnmount?: boolean;
17
+ /** Initial state values used on first mount and on reset. */
18
+ initialState?: T;
19
+ }
20
+ /**
21
+ * Internal registry entry stored per storeId in the manager.
22
+ */
23
+ interface DynamicStoreRegistry {
24
+ data: StoreState;
25
+ config: StoreConfig;
26
+ initialState: StoreState;
27
+ }
28
+ /**
29
+ * A plain object that can serve as store state.
30
+ * Keys are strings, values can be anything.
31
+ */
32
+ type StoreState = Record<string, unknown>;
33
+ /**
34
+ * Actions are functions attached to the store.
35
+ */
36
+ type StoreActions = Record<string, (...args: unknown[]) => unknown>;
37
+ /**
38
+ * Full store slice = state + actions.
39
+ */
40
+ type StoreSlice<TState extends StoreState = StoreState, TActions extends StoreActions = StoreActions> = TState & TActions;
41
+ /**
42
+ * Configuration object passed to createDynamicStore.
43
+ */
44
+ interface DynamicStoreConfig<TState extends StoreState, TActions extends StoreActions> {
45
+ /** Initial state values */
46
+ initialState: TState;
47
+ /** Factory that receives `set` and `get` and returns action implementations */
48
+ actions: (set: StoreApi<StoreSlice<TState, TActions>>["setState"], get: StoreApi<StoreSlice<TState, TActions>>["getState"]) => TActions;
49
+ }
50
+ /**
51
+ * The return type of createDynamicStore.
52
+ */
53
+ interface DynamicStore<TState extends StoreState, TActions extends StoreActions> {
54
+ /** Zustand React hook */
55
+ useStore: UseBoundStore<StoreApi<StoreSlice<TState, TActions>>>;
56
+ /** Direct access to the underlying Zustand store API */
57
+ store: StoreApi<StoreSlice<TState, TActions>>;
58
+ }
59
+
60
+ /**
61
+ * Creates a dynamic Zustand store from a configuration object.
62
+ *
63
+ * @example
64
+ * ```ts
65
+ * const { useStore } = createDynamicStore({
66
+ * initialState: { count: 0 },
67
+ * actions: (set) => ({
68
+ * increment: () => set((s) => ({ count: s.count + 1 })),
69
+ * decrement: () => set((s) => ({ count: s.count - 1 })),
70
+ * reset: () => set({ count: 0 }),
71
+ * }),
72
+ * });
73
+ * ```
74
+ */
75
+ declare function createDynamicStore<TState extends StoreState, TActions extends StoreActions>(config: DynamicStoreConfig<TState, TActions>): DynamicStore<TState, TActions>;
76
+
77
+ interface UseDynamicStoreReturn<T extends StoreState> {
78
+ data: T;
79
+ setData: (updater: SetStateAction<T>) => void;
80
+ reset: () => void;
81
+ }
82
+ /**
83
+ * Hook-based dynamic store keyed by `storeId`.
84
+ *
85
+ * `setData` accepts either a partial object **or** a function that receives
86
+ * the previous state and returns a partial object — exactly like React's
87
+ * `useState` setter.
88
+ *
89
+ * @example
90
+ * ```tsx
91
+ * const { data, setData, reset } = useDynamicStore<CounterState>('counter', {
92
+ * initialState: { value: 0, step: 1 },
93
+ * });
94
+ *
95
+ * // object update
96
+ * setData({ value: 42 });
97
+ *
98
+ * // functional update — safe for rapid successive calls
99
+ * setData((prev) => ({ value: prev.value + prev.step }));
100
+ * ```
101
+ */
102
+ declare function useDynamicStore<T extends StoreState>(storeId: string, config?: StoreConfig<T>): UseDynamicStoreReturn<T>;
103
+ /**
104
+ * Same as `useDynamicStore` but automatically resets state when the
105
+ * component unmounts (when `config.resetOnUnmount` is `true`).
106
+ *
107
+ * @example
108
+ * ```tsx
109
+ * const { data, setData, reset } = useDynamicStoreWithCleanup<FormState>(
110
+ * 'editForm',
111
+ * { initialState, resetOnUnmount: true },
112
+ * );
113
+ * ```
114
+ */
115
+ declare function useDynamicStoreWithCleanup<T extends StoreState>(storeId: string, config?: StoreConfig<T>): UseDynamicStoreReturn<T>;
116
+ /**
117
+ * Update a dynamic store from outside a React component.
118
+ */
119
+ declare const updateDynamicStore: (storeId: string, data: StoreState) => void;
120
+ /**
121
+ * Reset a single dynamic store to its initial state from outside React.
122
+ */
123
+ declare const resetDynamicStore: (storeId: string) => void;
124
+ /**
125
+ * Reset all dynamic stores to their initial states from outside React.
126
+ */
127
+ declare const resetAllDynamicStores: () => void;
128
+ /**
129
+ * Reset only stores that do not have `persistOnNavigation: true`.
130
+ */
131
+ declare const resetNonPersistentDynamicStores: () => void;
132
+
133
+ export { type DynamicStore, type DynamicStoreConfig, type DynamicStoreRegistry, type SetStateAction, type StoreActions, type StoreConfig, type StoreSlice, type StoreState, type UseDynamicStoreReturn, createDynamicStore, resetAllDynamicStores, resetDynamicStore, resetNonPersistentDynamicStores, updateDynamicStore, useDynamicStore, useDynamicStoreWithCleanup };
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ 'use strict';var zustand=require('zustand'),middleware=require('zustand/middleware'),react=require('react');function d(r){let{initialState:t,actions:e}=r,o=zustand.create()((n,s)=>({...t,...e(n,s)}));return {useStore:o,store:o}}var i=zustand.create()(middleware.devtools(r=>({stores:{},setStoreData:(t,e,o)=>{r(n=>{let s=n.stores[t],a={data:{...s?.data??o?.initialState??{},...e},config:o??s?.config??{},initialState:o?.initialState??s?.initialState??{}};return {stores:{...n.stores,[t]:a}}});},resetStore:t=>{r(e=>{let o=e.stores[t];return o?{stores:{...e.stores,[t]:{...o,data:{...o.initialState}}}}:e});},resetAllStores:()=>{r(t=>{let e={};for(let[o,n]of Object.entries(t.stores))e[o]={...n,data:{...n.initialState}};return {stores:e}});},resetNonPersistentStores:()=>{r(t=>{let e={};for(let[o,n]of Object.entries(t.stores))e[o]=n.config.persistOnNavigation===true?n:{...n,data:{...n.initialState}};return {stores:e}});}}),{name:"DynamicStoresManager"}));function c(r,t){let e=i(),o=e.stores[r];return react.useEffect(()=>{!o&&t?.initialState!==void 0&&e.setStoreData(r,{},t);},[r]),{data:o?.data??t?.initialState??{},setData:a=>{if(typeof a=="function"){let y=e.stores[r]?.data??t?.initialState??{},D=a(y);e.setStoreData(r,D,t);}else e.setStoreData(r,a,t);},reset:()=>{e.resetStore(r);}}}function g(r,t){let{data:e,setData:o,reset:n}=c(r,t);return react.useEffect(()=>()=>{t?.resetOnUnmount===true&&n();},[r,t?.resetOnUnmount]),{data:e,setData:o,reset:n}}var l=(r,t)=>{i.getState().setStoreData(r,t);},x=r=>{i.getState().resetStore(r);},T=()=>{i.getState().resetAllStores();},A=()=>{i.getState().resetNonPersistentStores();};exports.createDynamicStore=d;exports.resetAllDynamicStores=T;exports.resetDynamicStore=x;exports.resetNonPersistentDynamicStores=A;exports.updateDynamicStore=l;exports.useDynamicStore=c;exports.useDynamicStoreWithCleanup=g;//# sourceMappingURL=index.js.map
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/createDynamicStore.ts","../src/dynamicStore.ts"],"names":["createDynamicStore","config","initialState","actions","store","create","set","get","useDynamicStoresManager","devtools","storeId","data","state","existingStore","entry","next","id","useDynamicStore","storesManager","storeRegistry","useEffect","updater","currentData","updates","useDynamicStoreWithCleanup","setData","reset","updateDynamicStore","resetDynamicStore","resetAllDynamicStores","resetNonPersistentDynamicStores"],"mappings":"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","file":"index.js","sourcesContent":["import { create } from \"zustand\";\nimport type {\n DynamicStore,\n DynamicStoreConfig,\n StoreActions,\n StoreState,\n StoreSlice,\n} from \"./types\";\n\n/**\n * Creates a dynamic Zustand store from a configuration object.\n *\n * @example\n * ```ts\n * const { useStore } = createDynamicStore({\n * initialState: { count: 0 },\n * actions: (set) => ({\n * increment: () => set((s) => ({ count: s.count + 1 })),\n * decrement: () => set((s) => ({ count: s.count - 1 })),\n * reset: () => set({ count: 0 }),\n * }),\n * });\n * ```\n */\nexport function createDynamicStore<\n TState extends StoreState,\n TActions extends StoreActions,\n>(\n config: DynamicStoreConfig<TState, TActions>,\n): DynamicStore<TState, TActions> {\n const { initialState, actions } = config;\n\n const store = create<StoreSlice<TState, TActions>>()((set, get) => ({\n ...initialState,\n ...actions(set, get),\n }));\n\n return {\n useStore: store,\n store,\n };\n}\n","import { create } from \"zustand\";\nimport { devtools } from \"zustand/middleware\";\nimport { useEffect } from \"react\";\nimport type {\n SetStateAction,\n StoreConfig,\n DynamicStoreRegistry,\n StoreState,\n} from \"./types\";\n\n// ─── Internal manager state ───────────────────────────────────────────────────\n\ninterface DynamicStoresState {\n stores: Record<string, DynamicStoreRegistry>;\n setStoreData: (\n storeId: string,\n data: StoreState,\n config?: StoreConfig,\n ) => void;\n resetStore: (storeId: string) => void;\n resetAllStores: () => void;\n resetNonPersistentStores: () => void;\n}\n\nconst useDynamicStoresManager = create<DynamicStoresState>()(\n devtools(\n (set) => ({\n stores: {},\n\n setStoreData: (storeId, data, config) => {\n set((state) => {\n const existingStore = state.stores[storeId];\n const currentData: StoreState =\n existingStore?.data ?? config?.initialState ?? {};\n\n const entry: DynamicStoreRegistry = {\n data: { ...currentData, ...data },\n config: config ?? existingStore?.config ?? {},\n initialState:\n config?.initialState ?? existingStore?.initialState ?? {},\n };\n\n return {\n stores: { ...state.stores, [storeId]: entry },\n };\n });\n },\n\n resetStore: (storeId) => {\n set((state) => {\n const store = state.stores[storeId];\n if (!store) return state;\n\n return {\n stores: {\n ...state.stores,\n [storeId]: { ...store, data: { ...store.initialState } },\n },\n };\n });\n },\n\n resetAllStores: () => {\n set((state) => {\n const next: Record<string, DynamicStoreRegistry> = {};\n\n for (const [id, store] of Object.entries(state.stores)) {\n next[id] = { ...store, data: { ...store.initialState } };\n }\n\n return { stores: next };\n });\n },\n\n resetNonPersistentStores: () => {\n set((state) => {\n const next: Record<string, DynamicStoreRegistry> = {};\n\n for (const [id, store] of Object.entries(state.stores)) {\n next[id] =\n store.config.persistOnNavigation === true\n ? store\n : { ...store, data: { ...store.initialState } };\n }\n\n return { stores: next };\n });\n },\n }),\n { name: \"DynamicStoresManager\" },\n ),\n);\n\n// ─── Return types ─────────────────────────────────────────────────────────────\n\nexport interface UseDynamicStoreReturn<T extends StoreState> {\n data: T;\n setData: (updater: SetStateAction<T>) => void;\n reset: () => void;\n}\n\n// ─── useDynamicStore ──────────────────────────────────────────────────────────\n\n/**\n * Hook-based dynamic store keyed by `storeId`.\n *\n * `setData` accepts either a partial object **or** a function that receives\n * the previous state and returns a partial object — exactly like React's\n * `useState` setter.\n *\n * @example\n * ```tsx\n * const { data, setData, reset } = useDynamicStore<CounterState>('counter', {\n * initialState: { value: 0, step: 1 },\n * });\n *\n * // object update\n * setData({ value: 42 });\n *\n * // functional update — safe for rapid successive calls\n * setData((prev) => ({ value: prev.value + prev.step }));\n * ```\n */\nexport function useDynamicStore<T extends StoreState>(\n storeId: string,\n config?: StoreConfig<T>,\n): UseDynamicStoreReturn<T> {\n const storesManager = useDynamicStoresManager();\n const storeRegistry = storesManager.stores[storeId];\n\n // Initialize the store entry on first use\n useEffect(() => {\n if (!storeRegistry && config?.initialState !== undefined) {\n storesManager.setStoreData(storeId, {}, config as StoreConfig);\n }\n // Only run on mount / storeId change — intentional dep list\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [storeId]);\n\n const data = (storeRegistry?.data ?? config?.initialState ?? {}) as T;\n\n const setData = (updater: SetStateAction<T>): void => {\n if (typeof updater === \"function\") {\n const currentData = (\n storesManager.stores[storeId]?.data ?? config?.initialState ?? {}\n ) as T;\n const updates = updater(currentData);\n storesManager.setStoreData(\n storeId,\n updates as StoreState,\n config as StoreConfig | undefined,\n );\n } else {\n storesManager.setStoreData(\n storeId,\n updater as StoreState,\n config as StoreConfig | undefined,\n );\n }\n };\n\n const reset = (): void => {\n storesManager.resetStore(storeId);\n };\n\n return { data, setData, reset };\n}\n\n// ─── useDynamicStoreWithCleanup ───────────────────────────────────────────────\n\n/**\n * Same as `useDynamicStore` but automatically resets state when the\n * component unmounts (when `config.resetOnUnmount` is `true`).\n *\n * @example\n * ```tsx\n * const { data, setData, reset } = useDynamicStoreWithCleanup<FormState>(\n * 'editForm',\n * { initialState, resetOnUnmount: true },\n * );\n * ```\n */\nexport function useDynamicStoreWithCleanup<T extends StoreState>(\n storeId: string,\n config?: StoreConfig<T>,\n): UseDynamicStoreReturn<T> {\n const { data, setData, reset } = useDynamicStore<T>(storeId, config);\n\n useEffect(() => {\n return () => {\n if (config?.resetOnUnmount === true) {\n reset();\n }\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [storeId, config?.resetOnUnmount]);\n\n return { data, setData, reset };\n}\n\n// ─── Imperative helpers (outside React) ──────────────────────────────────────\n\n/**\n * Update a dynamic store from outside a React component.\n */\nexport const updateDynamicStore = (\n storeId: string,\n data: StoreState,\n): void => {\n useDynamicStoresManager.getState().setStoreData(storeId, data);\n};\n\n/**\n * Reset a single dynamic store to its initial state from outside React.\n */\nexport const resetDynamicStore = (storeId: string): void => {\n useDynamicStoresManager.getState().resetStore(storeId);\n};\n\n/**\n * Reset all dynamic stores to their initial states from outside React.\n */\nexport const resetAllDynamicStores = (): void => {\n useDynamicStoresManager.getState().resetAllStores();\n};\n\n/**\n * Reset only stores that do not have `persistOnNavigation: true`.\n */\nexport const resetNonPersistentDynamicStores = (): void => {\n useDynamicStoresManager.getState().resetNonPersistentStores();\n};\n"]}
package/dist/index.mjs ADDED
@@ -0,0 +1,2 @@
1
+ import {create}from'zustand';import {devtools}from'zustand/middleware';import {useEffect}from'react';function d(r){let{initialState:t,actions:e}=r,o=create()((n,s)=>({...t,...e(n,s)}));return {useStore:o,store:o}}var i=create()(devtools(r=>({stores:{},setStoreData:(t,e,o)=>{r(n=>{let s=n.stores[t],a={data:{...s?.data??o?.initialState??{},...e},config:o??s?.config??{},initialState:o?.initialState??s?.initialState??{}};return {stores:{...n.stores,[t]:a}}});},resetStore:t=>{r(e=>{let o=e.stores[t];return o?{stores:{...e.stores,[t]:{...o,data:{...o.initialState}}}}:e});},resetAllStores:()=>{r(t=>{let e={};for(let[o,n]of Object.entries(t.stores))e[o]={...n,data:{...n.initialState}};return {stores:e}});},resetNonPersistentStores:()=>{r(t=>{let e={};for(let[o,n]of Object.entries(t.stores))e[o]=n.config.persistOnNavigation===true?n:{...n,data:{...n.initialState}};return {stores:e}});}}),{name:"DynamicStoresManager"}));function c(r,t){let e=i(),o=e.stores[r];return useEffect(()=>{!o&&t?.initialState!==void 0&&e.setStoreData(r,{},t);},[r]),{data:o?.data??t?.initialState??{},setData:a=>{if(typeof a=="function"){let y=e.stores[r]?.data??t?.initialState??{},D=a(y);e.setStoreData(r,D,t);}else e.setStoreData(r,a,t);},reset:()=>{e.resetStore(r);}}}function g(r,t){let{data:e,setData:o,reset:n}=c(r,t);return useEffect(()=>()=>{t?.resetOnUnmount===true&&n();},[r,t?.resetOnUnmount]),{data:e,setData:o,reset:n}}var l=(r,t)=>{i.getState().setStoreData(r,t);},x=r=>{i.getState().resetStore(r);},T=()=>{i.getState().resetAllStores();},A=()=>{i.getState().resetNonPersistentStores();};export{d as createDynamicStore,T as resetAllDynamicStores,x as resetDynamicStore,A as resetNonPersistentDynamicStores,l as updateDynamicStore,c as useDynamicStore,g as useDynamicStoreWithCleanup};//# sourceMappingURL=index.mjs.map
2
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/createDynamicStore.ts","../src/dynamicStore.ts"],"names":["createDynamicStore","config","initialState","actions","store","create","set","get","useDynamicStoresManager","devtools","storeId","data","state","existingStore","entry","next","id","useDynamicStore","storesManager","storeRegistry","useEffect","updater","currentData","updates","useDynamicStoreWithCleanup","setData","reset","updateDynamicStore","resetDynamicStore","resetAllDynamicStores","resetNonPersistentDynamicStores"],"mappings":"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","file":"index.mjs","sourcesContent":["import { create } from \"zustand\";\nimport type {\n DynamicStore,\n DynamicStoreConfig,\n StoreActions,\n StoreState,\n StoreSlice,\n} from \"./types\";\n\n/**\n * Creates a dynamic Zustand store from a configuration object.\n *\n * @example\n * ```ts\n * const { useStore } = createDynamicStore({\n * initialState: { count: 0 },\n * actions: (set) => ({\n * increment: () => set((s) => ({ count: s.count + 1 })),\n * decrement: () => set((s) => ({ count: s.count - 1 })),\n * reset: () => set({ count: 0 }),\n * }),\n * });\n * ```\n */\nexport function createDynamicStore<\n TState extends StoreState,\n TActions extends StoreActions,\n>(\n config: DynamicStoreConfig<TState, TActions>,\n): DynamicStore<TState, TActions> {\n const { initialState, actions } = config;\n\n const store = create<StoreSlice<TState, TActions>>()((set, get) => ({\n ...initialState,\n ...actions(set, get),\n }));\n\n return {\n useStore: store,\n store,\n };\n}\n","import { create } from \"zustand\";\nimport { devtools } from \"zustand/middleware\";\nimport { useEffect } from \"react\";\nimport type {\n SetStateAction,\n StoreConfig,\n DynamicStoreRegistry,\n StoreState,\n} from \"./types\";\n\n// ─── Internal manager state ───────────────────────────────────────────────────\n\ninterface DynamicStoresState {\n stores: Record<string, DynamicStoreRegistry>;\n setStoreData: (\n storeId: string,\n data: StoreState,\n config?: StoreConfig,\n ) => void;\n resetStore: (storeId: string) => void;\n resetAllStores: () => void;\n resetNonPersistentStores: () => void;\n}\n\nconst useDynamicStoresManager = create<DynamicStoresState>()(\n devtools(\n (set) => ({\n stores: {},\n\n setStoreData: (storeId, data, config) => {\n set((state) => {\n const existingStore = state.stores[storeId];\n const currentData: StoreState =\n existingStore?.data ?? config?.initialState ?? {};\n\n const entry: DynamicStoreRegistry = {\n data: { ...currentData, ...data },\n config: config ?? existingStore?.config ?? {},\n initialState:\n config?.initialState ?? existingStore?.initialState ?? {},\n };\n\n return {\n stores: { ...state.stores, [storeId]: entry },\n };\n });\n },\n\n resetStore: (storeId) => {\n set((state) => {\n const store = state.stores[storeId];\n if (!store) return state;\n\n return {\n stores: {\n ...state.stores,\n [storeId]: { ...store, data: { ...store.initialState } },\n },\n };\n });\n },\n\n resetAllStores: () => {\n set((state) => {\n const next: Record<string, DynamicStoreRegistry> = {};\n\n for (const [id, store] of Object.entries(state.stores)) {\n next[id] = { ...store, data: { ...store.initialState } };\n }\n\n return { stores: next };\n });\n },\n\n resetNonPersistentStores: () => {\n set((state) => {\n const next: Record<string, DynamicStoreRegistry> = {};\n\n for (const [id, store] of Object.entries(state.stores)) {\n next[id] =\n store.config.persistOnNavigation === true\n ? store\n : { ...store, data: { ...store.initialState } };\n }\n\n return { stores: next };\n });\n },\n }),\n { name: \"DynamicStoresManager\" },\n ),\n);\n\n// ─── Return types ─────────────────────────────────────────────────────────────\n\nexport interface UseDynamicStoreReturn<T extends StoreState> {\n data: T;\n setData: (updater: SetStateAction<T>) => void;\n reset: () => void;\n}\n\n// ─── useDynamicStore ──────────────────────────────────────────────────────────\n\n/**\n * Hook-based dynamic store keyed by `storeId`.\n *\n * `setData` accepts either a partial object **or** a function that receives\n * the previous state and returns a partial object — exactly like React's\n * `useState` setter.\n *\n * @example\n * ```tsx\n * const { data, setData, reset } = useDynamicStore<CounterState>('counter', {\n * initialState: { value: 0, step: 1 },\n * });\n *\n * // object update\n * setData({ value: 42 });\n *\n * // functional update — safe for rapid successive calls\n * setData((prev) => ({ value: prev.value + prev.step }));\n * ```\n */\nexport function useDynamicStore<T extends StoreState>(\n storeId: string,\n config?: StoreConfig<T>,\n): UseDynamicStoreReturn<T> {\n const storesManager = useDynamicStoresManager();\n const storeRegistry = storesManager.stores[storeId];\n\n // Initialize the store entry on first use\n useEffect(() => {\n if (!storeRegistry && config?.initialState !== undefined) {\n storesManager.setStoreData(storeId, {}, config as StoreConfig);\n }\n // Only run on mount / storeId change — intentional dep list\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [storeId]);\n\n const data = (storeRegistry?.data ?? config?.initialState ?? {}) as T;\n\n const setData = (updater: SetStateAction<T>): void => {\n if (typeof updater === \"function\") {\n const currentData = (\n storesManager.stores[storeId]?.data ?? config?.initialState ?? {}\n ) as T;\n const updates = updater(currentData);\n storesManager.setStoreData(\n storeId,\n updates as StoreState,\n config as StoreConfig | undefined,\n );\n } else {\n storesManager.setStoreData(\n storeId,\n updater as StoreState,\n config as StoreConfig | undefined,\n );\n }\n };\n\n const reset = (): void => {\n storesManager.resetStore(storeId);\n };\n\n return { data, setData, reset };\n}\n\n// ─── useDynamicStoreWithCleanup ───────────────────────────────────────────────\n\n/**\n * Same as `useDynamicStore` but automatically resets state when the\n * component unmounts (when `config.resetOnUnmount` is `true`).\n *\n * @example\n * ```tsx\n * const { data, setData, reset } = useDynamicStoreWithCleanup<FormState>(\n * 'editForm',\n * { initialState, resetOnUnmount: true },\n * );\n * ```\n */\nexport function useDynamicStoreWithCleanup<T extends StoreState>(\n storeId: string,\n config?: StoreConfig<T>,\n): UseDynamicStoreReturn<T> {\n const { data, setData, reset } = useDynamicStore<T>(storeId, config);\n\n useEffect(() => {\n return () => {\n if (config?.resetOnUnmount === true) {\n reset();\n }\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [storeId, config?.resetOnUnmount]);\n\n return { data, setData, reset };\n}\n\n// ─── Imperative helpers (outside React) ──────────────────────────────────────\n\n/**\n * Update a dynamic store from outside a React component.\n */\nexport const updateDynamicStore = (\n storeId: string,\n data: StoreState,\n): void => {\n useDynamicStoresManager.getState().setStoreData(storeId, data);\n};\n\n/**\n * Reset a single dynamic store to its initial state from outside React.\n */\nexport const resetDynamicStore = (storeId: string): void => {\n useDynamicStoresManager.getState().resetStore(storeId);\n};\n\n/**\n * Reset all dynamic stores to their initial states from outside React.\n */\nexport const resetAllDynamicStores = (): void => {\n useDynamicStoresManager.getState().resetAllStores();\n};\n\n/**\n * Reset only stores that do not have `persistOnNavigation: true`.\n */\nexport const resetNonPersistentDynamicStores = (): void => {\n useDynamicStoresManager.getState().resetNonPersistentStores();\n};\n"]}
package/package.json ADDED
@@ -0,0 +1,62 @@
1
+ {
2
+ "name": "@pitboxdev/dynamic-store-zustand",
3
+ "version": "0.0.1",
4
+ "description": "Dynamic store factory built on top of Zustand for scalable state management",
5
+ "main": "dist/index.js",
6
+ "module": "dist/index.mjs",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.mjs",
12
+ "require": "./dist/index.js"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "scripts": {
19
+ "build": "tsup",
20
+ "dev": "tsup --watch",
21
+ "type-check": "tsc --noEmit",
22
+ "lint": "eslint src --ext .ts,.tsx",
23
+ "test": "vitest run",
24
+ "prepublishOnly": "npm run build",
25
+ "release": "npm version patch && git push origin main --follow-tags && npm publish"
26
+ },
27
+ "keywords": [
28
+ "zustand",
29
+ "pitboxdev",
30
+ "state-management",
31
+ "react",
32
+ "typescript"
33
+ ],
34
+ "author": "Pitboxdev",
35
+ "license": "MIT",
36
+ "peerDependencies": {
37
+ "react": ">=18.0.0",
38
+ "zustand": ">=5.0.0"
39
+ },
40
+ "devDependencies": {
41
+ "@types/react": "^18.3.12",
42
+ "@typescript-eslint/eslint-plugin": "^8.0.0",
43
+ "@typescript-eslint/parser": "^8.0.0",
44
+ "eslint": "^9.0.0",
45
+ "react": "^18.3.1",
46
+ "tsup": "^8.3.5",
47
+ "typescript": "^5.7.2",
48
+ "vitest": "^2.1.8",
49
+ "zustand": "^5.0.2"
50
+ },
51
+ "repository": {
52
+ "type": "git",
53
+ "url": "git+https://github.com/pitboxdev/dynamic-store-zustand.git"
54
+ },
55
+ "bugs": {
56
+ "url": "https://github.com/pitboxdev/dynamic-store-zustand/issues"
57
+ },
58
+ "homepage": "https://github.com/pitboxdev/dynamic-store-zustand#readme",
59
+ "publishConfig": {
60
+ "access": "public"
61
+ }
62
+ }