react-shared-states 1.0.8 → 1.0.11

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 CHANGED
@@ -1,4 +1,3 @@
1
-
2
1
  # React Shared States
3
2
 
4
3
  **_Global state made as simple as useState, with zero config, built-in async caching, and automatic scoping._**
@@ -198,16 +197,396 @@ export default function App(){
198
197
 
199
198
 
200
199
  ## 🧠 Core Concepts
201
- | Concept | Summary |
202
- |------------------------|---------------------------------------------------------------------------------------------------------------------------------|
203
- | Global by default | No provider necessary. Same key => shared state. |
204
- | Scoping | Wrap with `SharedStatesProvider` to isolate. Nearest provider wins. |
205
- | Named scopes | `scopeName` prop lets distant providers sync (same name ⇒ same bucket). Unnamed providers auto‑generate a random isolated name. |
206
- | Manual override | Third param in `useSharedState` / `useSharedFunction` / `useSharedSubscription` enforces a specific scope ignoring tree search. |
207
- | Shared functions | Encapsulate async logic: single flight + cached result + `error` + `isLoading` + opt‑in refresh. |
208
- | Shared subscriptions | Real-time data streams: automatic cleanup + shared connections + `error` + `isLoading` + subscription state. |
209
- | Static APIs | Access state/functions/subscriptions outside components (`sharedStatesApi`, `sharedFunctionsApi`, `sharedSubscriptionsApi`). |
210
- | Static/shared creation | Use `createSharedState`, `createSharedFunction`, `createSharedSubscription` to export reusable, type-safe shared resources. |
200
+ | Concept | Summary |
201
+ |------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------|
202
+ | Global by default | No provider necessary. Same key => shared state. |
203
+ | Scoping | Wrap with `SharedStatesProvider` to isolate. Nearest provider wins. |
204
+ | Named scopes | `scopeName` prop lets distant providers sync (same name ⇒ same bucket). Unnamed providers auto‑generate a random isolated name. |
205
+ | Manual override | Third param in `useSharedState` / `useSharedFunction` / `useSharedSubscription` enforces a specific scope ignoring tree search. |
206
+ | Shared functions | Encapsulate async logic: single flight + cached result + `error` + `isLoading` + opt‑in refresh. |
207
+ | Shared subscriptions | Real-time data streams: automatic cleanup + shared connections + `error` + `isLoading` + subscription state. |
208
+ | Static APIs | Access state/functions/subscriptions outside components (`sharedStatesApi`, `sharedFunctionsApi`, `sharedSubscriptionsApi`). |
209
+ | Static/shared creation | Use `createSharedState`, `createSharedFunction`, `createSharedSubscription` to export reusable, type-safe shared resources that persist across `clearAll()` calls. |
210
+
211
+
212
+ ## 🏗️ Sharing State (`useSharedState`)
213
+ Signature:
214
+ - `const [value, setValue] = useSharedState(key, initialValue, scopeName?)`
215
+ - `const [value, setValue] = useSharedState(sharedStateCreated)`
216
+
217
+ Behavior:
218
+ * First hook call (per key + scope) seeds with `initialValue`.
219
+ * Subsequent mounts with same key+scope ignore their `initialValue` (consistent source of truth).
220
+ * Setter accepts either value or updater `(prev)=>next`.
221
+ * React batching + equality check: listeners fire only when the value reference actually changes.
222
+ * States created with `createSharedState` are **static** by default and are not removed by `clear()` or `clearAll()`, ensuring they persist.
223
+
224
+ ### Examples
225
+ 1. Global theme (recommended for large apps)
226
+ ```tsx
227
+ // themeState.ts
228
+ export const themeState = createSharedState('light');
229
+ // In components
230
+ const [theme, setTheme] = useSharedState(themeState);
231
+ ```
232
+ 2. Isolated wizard progress
233
+ ```tsx
234
+ const wizardProgress = createSharedState(0);
235
+ <SharedStatesProvider>
236
+ <Wizard/>
237
+ </SharedStatesProvider>
238
+ // In Wizard
239
+ const [step, setStep] = useSharedState(wizardProgress);
240
+ ```
241
+ 3. Forcing cross‑portal sync
242
+ ```tsx
243
+ const navState = createSharedState('closed', 'nav');
244
+ <SharedStatesProvider scopeName="nav" children={<PrimaryNav/>} />
245
+ <Portal>
246
+ <SharedStatesProvider scopeName="nav" children={<MobileNav/>} />
247
+ </Portal>
248
+ // In both navs
249
+ const [navOpen, setNavOpen] = useSharedState(navState);
250
+ ```
251
+ 4. Overriding nearest provider
252
+ ```tsx
253
+ // Even if inside a provider, this explicitly binds to global
254
+ const globalFlag = createSharedState(false, '_global');
255
+ const [flag, setFlag] = useSharedState(globalFlag);
256
+ ```
257
+
258
+
259
+ ## ⚡ Shared Async Functions (`useSharedFunction`)
260
+ Signature:
261
+ - `const { state, trigger, forceTrigger, clear } = useSharedFunction(key, asyncFn, scopeName?)`
262
+ - `const { state, trigger, forceTrigger, clear } = useSharedFunction(sharedFunctionCreated)`
263
+ `state` shape: `{ results?: T; isLoading: boolean; error?: unknown }`
264
+
265
+ Semantics:
266
+ * First `trigger()` (implicit or manual) runs the function; subsequent calls do nothing while loading or after success (cached) unless you `forceTrigger()`.
267
+ * Multiple components with the same key+scope share one execution + result.
268
+ * `clear()` deletes the cache (next trigger re-runs).
269
+ * You decide when to invoke `trigger` (e.g. on mount, on button click, when dependencies change, etc.).
270
+ * Functions created with `createSharedFunction` are **static** and persist across `clearAll()` calls.
271
+
272
+ ### Pattern: lazy load on first render
273
+ ```tsx
274
+ // profileFunction.ts
275
+ export const profileFunction = createSharedFunction((id: string) => fetch(`/api/p/${id}`).then(r=>r.json()));
276
+
277
+ function Profile({id}:{id:string}){
278
+ const { state, trigger } = useSharedFunction(profileFunction);
279
+ // ...same as before
280
+ }
281
+ ```
282
+
283
+ ### Pattern: always fetch fresh
284
+ ```tsx
285
+ const { state, forceTrigger } = useSharedFunction('server-time', () => fetch('/time').then(r=>r.text()));
286
+ const refresh = () => forceTrigger();
287
+ ```
288
+
289
+
290
+ ## 📡 Real-time Subscriptions (`useSharedSubscription`)
291
+ Perfect for Firebase listeners, WebSocket connections,
292
+ Server-Sent Events, or any streaming data source that needs cleanup.
293
+
294
+ Signature:
295
+ - `const { state, trigger, unsubscribe } = useSharedSubscription(key, subscriber, scopeName?)`
296
+ - `const { state, trigger, unsubscribe } = useSharedSubscription(sharedSubscriptionCreated)`
297
+
298
+ `state` shape: `{ data?: T; isLoading: boolean; error?: unknown; subscribed: boolean }`
299
+
300
+ The `subscriber` function receives three callbacks:
301
+ - `set(data)`: Update the shared data
302
+ - `error(error)`: Handle errors
303
+ - `complete()`: Mark loading as complete
304
+ - Returns: Optional cleanup function (called on unsubscribe/unmount)
305
+
306
+ ### Pattern: Firebase Firestore real-time listener
307
+ ```tsx
308
+ // userSubscription.ts
309
+ import { onSnapshot, doc } from 'firebase/firestore';
310
+ import { createSharedSubscription } from 'react-shared-states';
311
+ import { db } from './firebase-config';
312
+
313
+ export const userSubscription = createSharedSubscription(
314
+ (set, error, complete) => {
315
+ const userDocRef = doc(db, 'users', 'some-user-id');
316
+ const unsubscribe = onSnapshot(userDocRef,
317
+ (doc) => {
318
+ if (doc.exists()) {
319
+ set(doc.data());
320
+ } else {
321
+ error(new Error('User not found'));
322
+ }
323
+ complete();
324
+ },
325
+ (err) => {
326
+ error(err);
327
+ complete();
328
+ }
329
+ );
330
+ return unsubscribe;
331
+ }
332
+ );
333
+
334
+ function UserProfile({ userId }: { userId: string }) {
335
+ const { state, trigger, unsubscribe } = useSharedSubscription(userSubscription);
336
+ // Start listening when component mounts
337
+ useEffect(() => {
338
+ trigger();
339
+ }, []);
340
+
341
+ if (state.isLoading) return <div>Connecting...</div>;
342
+ if (state.error) return <div>Error: {state.error.message}</div>;
343
+ if (!state.data) return <div>User not found</div>;
344
+
345
+ return (
346
+ <div>
347
+ <h1>{state.data.name}</h1>
348
+ <p>{state.data.email}</p>
349
+ <button onClick={unsubscribe}>Stop listening</button>
350
+ </div>
351
+ );
352
+ }
353
+ ```
354
+
355
+ ### Pattern: WebSocket connection
356
+ ```tsx
357
+ import { useEffect } from 'react';
358
+ import { useSharedSubscription } from 'react-shared-states';
359
+
360
+ function ChatRoom({ roomId }: { roomId: string }) {
361
+ const { state, trigger } = useSharedSubscription(
362
+ `chat-${roomId}`,
363
+ (set, error, complete) => {
364
+ const ws = new WebSocket(`ws://chat-server/${roomId}`);
365
+
366
+ ws.onopen = () => complete();
367
+ ws.onmessage = (event) => {
368
+ const message = JSON.parse(event.data);
369
+ set(prev => [...(prev || []), message]);
370
+ };
371
+ ws.onerror = error;
372
+
373
+ return () => ws.close();
374
+ }
375
+ );
376
+
377
+ useEffect(() => {
378
+ trigger();
379
+ }, []);
380
+
381
+ return (
382
+ <div>
383
+ {state.isLoading && <p>Connecting to chat...</p>}
384
+ {state.error && <p>Connection failed</p>}
385
+ <div>
386
+ {state.data?.map(msg => (
387
+ <div key={msg.id}>{msg.text}</div>
388
+ ))}
389
+ </div>
390
+ </div>
391
+ );
392
+ }
393
+ ```
394
+
395
+ ### Pattern: Server-Sent Events
396
+ ```tsx
397
+ import { useEffect } from 'react';
398
+ import { useSharedSubscription } from 'react-shared-states';
399
+
400
+ function LiveUpdates() {
401
+ const { state, trigger } = useSharedSubscription(
402
+ 'live-updates',
403
+ (set, error, complete) => {
404
+ const eventSource = new EventSource('/api/live-updates');
405
+
406
+ eventSource.onopen = () => complete();
407
+ eventSource.onmessage = (event) => {
408
+ set(JSON.parse(event.data));
409
+ };
410
+ eventSource.onerror = error;
411
+
412
+ return () => eventSource.close();
413
+ }
414
+ );
415
+
416
+ useEffect(() => {
417
+ trigger();
418
+ }, []);
419
+
420
+ return <div>Latest: {JSON.stringify(state.data)}</div>;
421
+ }
422
+ ```
423
+
424
+ Subscription semantics:
425
+ * First `trigger()` establishes the subscription; subsequent calls do nothing if already subscribed.
426
+ * Multiple components with the same key+scope share one subscription + data stream.
427
+ * `unsubscribe()` closes the connection and clears the subscribed state.
428
+ * Automatic cleanup on component unmount when no other components are listening.
429
+ * Components mounting later instantly get the latest `data` without re-subscribing.
430
+
431
+
432
+ ## 🛰️ Static APIs (outside React)
433
+ ## 🏛️ Static/Global Shared Resource Creation
434
+
435
+ For large apps, you can create and export shared state, function,
436
+ or subscription objects for type safety and to avoid key collisions.
437
+ This pattern is similar to Zustand or Jotai stores:
438
+
439
+ ```ts
440
+ import { createSharedState, createSharedFunction, createSharedSubscription, useSharedState, useSharedFunction, useSharedSubscription } from 'react-shared-states';
441
+
442
+ // Create and export shared resources
443
+ export const counterState = createSharedState(0);
444
+ export const fetchUserFunction = createSharedFunction(() => fetch('/api/me').then(r => r.json()));
445
+ export const chatSubscription = createSharedSubscription((set, error, complete) => {/* ... */});
446
+
447
+ // Use anywhere in your app
448
+ const [count, setCount] = useSharedState(counterState);
449
+ const { state, trigger } = useSharedFunction(fetchUserFunction);
450
+ const { state, trigger, unsubscribe } = useSharedSubscription(chatSubscription);
451
+ ```
452
+ Useful for SSR hydration, event listeners, debugging, imperative workflows.
453
+
454
+ ```ts
455
+ import { sharedStatesApi, sharedFunctionsApi, sharedSubscriptionsApi } from 'react-shared-states';
456
+
457
+ // Preload state (global scope by default)
458
+ sharedStatesApi.set('bootstrap-data', { user: {...} });
459
+
460
+ // Preload state in a named scope
461
+ sharedStatesApi.set('bootstrap-data', { user: {...} }, 'myScope');
462
+
463
+ // Read later
464
+ const user = sharedStatesApi.get('bootstrap-data'); // global
465
+ const userScoped = sharedStatesApi.get('bootstrap-data', 'myScope');
466
+
467
+ // Inspect all (returns nested object: { [scope]: { [key]: value } })
468
+ console.log(sharedStatesApi.getAll());
469
+
470
+ // Clear all keys in a scope
471
+ sharedStatesApi.clearScope('myScope');
472
+
473
+ // For shared functions
474
+ const fnState = sharedFunctionsApi.get('profile-123');
475
+ const fnStateScoped = sharedFunctionsApi.get('profile-123', 'myScope');
476
+
477
+ // For shared subscriptions
478
+ const subState = sharedSubscriptionsApi.get('live-chat');
479
+ const subStateScoped = sharedSubscriptionsApi.get('live-chat', 'myScope');
480
+ ```
481
+
482
+ ## API summary:
483
+
484
+ | API | Methods |
485
+ |--------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
486
+ | `sharedStatesApi` | `get(key, scopeName?)`, `set(key, val, scopeName?)`, `has(key, scopeName?)`, `clear(key, scopeName?)`, `clearAll(withoutListeners?, withStatic?)`, `clearScope(scopeName?)`, `getAll()` |
487
+ | `sharedFunctionsApi` | `get(key, scopeName?)`, `set(key, val, scopeName?)`, `has(key, scopeName?)`, `clear(key, scopeName?)`, `clearAll(withoutListeners?, withStatic?)`, `clearScope(scopeName?)`, `getAll()` |
488
+ | `sharedSubscriptionsApi` | `get(key, scopeName?)`, `set(key, val, scopeName?)`, `has(key, scopeName?)`, `clear(key, scopeName?)`, `clearAll(withoutListeners?, withStatic?)`, `clearScope(scopeName?)`, `getAll()` |
489
+
490
+ `scopeName` defaults to `"_global"`. Internally, keys are stored as `${scope}//${key}`. The `.getAll()` method returns a nested object: `{ [scope]: { [key]: value } }`.
491
+
492
+
493
+ ## 🧩 Scoping Rules Deep Dive
494
+ Resolution order used inside hooks:
495
+ 1. Explicit 3rd parameter (`scopeName`)
496
+ 2. Nearest `SharedStatesProvider` above the component
497
+ 3. The implicit global scope (`_global`)
498
+
499
+ Unnamed providers auto‑generate a random scope name: each mount = isolated island.
500
+
501
+ Two providers sharing the same `scopeName` act as a single logical scope even if they are disjoint in the tree (great for portals / microfrontends).
502
+
503
+
504
+ ## 🆚 Comparison Snapshot
505
+ | Criterion | react-shared-states | Redux Toolkit | Zustand |
506
+ |----------------|------------------------------------------|----------------------|----------------------------------|
507
+ | Setup | Install & call hook | Slice + store config | Create store function |
508
+ | Global state | Yes (by key) | Yes | Yes |
509
+ | Scoped state | Built-in (providers + names + overrides) | Needs custom logic | Needs multiple stores / contexts |
510
+ | Async helper | `useSharedFunction` (cache + status) | Thunks / RTK Query | Manual or middleware |
511
+ | Boilerplate | Near zero | Moderate | Low |
512
+ | Static access | Yes (APIs) | Yes (store) | Yes (store) |
513
+ | Learning curve | Minutes | Higher | Low |
514
+
515
+
516
+ ## 🧪 Testing Tips
517
+ * Use static APIs to assert state after component interactions.
518
+ * `sharedStatesApi.clearAll(false, true)`, `sharedFunctionsApi.clearAll(false, true)`, `sharedSubscriptionsApi.clearAll(false, true)` in `afterEach` to isolate tests and clear static states.
519
+ * For async functions: trigger once, await UI stabilization, assert `results` present.
520
+ * For subscriptions: mock the subscription source (Firebase, WebSocket, etc.) and verify data flow.
521
+
522
+
523
+ ## ❓ FAQ
524
+ **Q: How do I reset a single shared state?**
525
+ `sharedStatesApi.clear('key')`. If the state was created with `createSharedState`, it will reset to its initial value. Otherwise, it will be removed.
526
+
527
+ **Q: Can I pre-hydrate data on the server?**
528
+ Yes. Call `sharedStatesApi.set(...)` during bootstrap, then first client hook usage will pick it up.
529
+
530
+ **Q: How do I avoid accidental key collisions?**
531
+ Prefix keys by domain (e.g. `user:profile`, `cart:items`) or rely on provider scoping.
532
+
533
+ **Q: Why is my async function not re-running?**
534
+ It's cached. Use `forceTrigger()` or `clear()`.
535
+
536
+ **Q: How do I handle subscription cleanup?**
537
+ Subscriptions auto-cleanup when no components are listening. You can also manually call `unsubscribe()`.
538
+
539
+ **Q: Can I use it with Suspense?**
540
+ Currently no built-in Suspense wrappers; wrap `useSharedFunction` yourself if desired.
541
+
542
+
543
+ ## 📚 Full API Reference
544
+ ### `useSharedState(key, initialValue, scopeName?)`
545
+ Returns `[value, setValue]`.
546
+
547
+ ### `useSharedState(sharedStateCreated)`
548
+ Returns `[value, setValue]`.
549
+
550
+ ### `useSharedFunction(key, fn, scopeName?)`
551
+ Returns `{ state, trigger, forceTrigger, clear }`.
552
+
553
+ ### `useSharedFunction(sharedFunctionCreated)`
554
+ Returns `{ state, trigger, forceTrigger, clear }`.
555
+
556
+ ### `useSharedSubscription(key, subscriber, scopeName?)`
557
+ Returns `{ state, trigger, unsubscribe }`.
558
+
559
+ ### `useSharedSubscription(sharedSubscriptionCreated)`
560
+ Returns `{ state, trigger, unsubscribe }`.
561
+
562
+ ### `<SharedStatesProvider scopeName?>`
563
+ Wrap children; optional `scopeName` (string). If omitted a random unique one is generated.
564
+
565
+ ### Static APIs
566
+ `sharedStatesApi`, `sharedFunctionsApi`, `sharedSubscriptionsApi` (see earlier table).
567
+
568
+
569
+
570
+ ## 🤝 Contributions
571
+
572
+ We welcome contributions!
573
+ If you'd like to improve `react-shared-states`,
574
+ feel free to [open an issue](https://github.com/HichemTab-tech/react-shared-states/issues) or [submit a pull request](https://github.com/HichemTab-tech/react-shared-states/pulls).
575
+
576
+
577
+ ## Author
578
+
579
+ - [@HichemTab-tech](https://www.github.com/HichemTab-tech)
580
+
581
+ ## License
582
+
583
+ [MIT](https://github.com/HichemTab-tech/react-shared-states/blob/master/LICENSE)
584
+
585
+ ## 🌟 Acknowledgements
586
+
587
+ Inspired by React's built-in primitives and the ergonomics of modern lightweight state libraries.
588
+ Thanks to early adopters for feedback.
589
+
211
590
 
212
591
 
213
592
  ## 🏗️ Sharing State (`useSharedState`)
@@ -298,8 +677,8 @@ Signature:
298
677
 
299
678
  The `subscriber` function receives three callbacks:
300
679
  - `set(data)`: Update the shared data
301
- - `onError(error)`: Handle errors
302
- - `onCompletion()`: Mark loading as complete
680
+ - `error(error)`: Handle errors
681
+ - `complete()`: Mark loading as complete
303
682
  - Returns: Optional cleanup function (called on unsubscribe/unmount)
304
683
 
305
684
  ### Pattern: Firebase Firestore real-time listener
@@ -310,8 +689,23 @@ import { createSharedSubscription } from 'react-shared-states';
310
689
  import { db } from './firebase-config';
311
690
 
312
691
  export const userSubscription = createSharedSubscription(
313
- async (set, onError, onCompletion) => {
314
- // ...same as before
692
+ (set, error, complete) => {
693
+ const userDocRef = doc(db, 'users', 'some-user-id');
694
+ const unsubscribe = onSnapshot(userDocRef,
695
+ (doc) => {
696
+ if (doc.exists()) {
697
+ set(doc.data());
698
+ } else {
699
+ error(new Error('User not found'));
700
+ }
701
+ complete();
702
+ },
703
+ (err) => {
704
+ error(err);
705
+ complete();
706
+ }
707
+ );
708
+ return unsubscribe;
315
709
  }
316
710
  );
317
711
 
@@ -344,15 +738,15 @@ import { useSharedSubscription } from 'react-shared-states';
344
738
  function ChatRoom({ roomId }: { roomId: string }) {
345
739
  const { state, trigger } = useSharedSubscription(
346
740
  `chat-${roomId}`,
347
- (set, onError, onCompletion) => {
741
+ (set, error, complete) => {
348
742
  const ws = new WebSocket(`ws://chat-server/${roomId}`);
349
743
 
350
- ws.onopen = () => onCompletion();
744
+ ws.onopen = () => complete();
351
745
  ws.onmessage = (event) => {
352
746
  const message = JSON.parse(event.data);
353
747
  set(prev => [...(prev || []), message]);
354
748
  };
355
- ws.onerror = onError;
749
+ ws.onerror = error;
356
750
 
357
751
  return () => ws.close();
358
752
  }
@@ -384,14 +778,14 @@ import { useSharedSubscription } from 'react-shared-states';
384
778
  function LiveUpdates() {
385
779
  const { state, trigger } = useSharedSubscription(
386
780
  'live-updates',
387
- (set, onError, onCompletion) => {
781
+ (set, error, complete) => {
388
782
  const eventSource = new EventSource('/api/live-updates');
389
783
 
390
- eventSource.onopen = () => onCompletion();
784
+ eventSource.onopen = () => complete();
391
785
  eventSource.onmessage = (event) => {
392
786
  set(JSON.parse(event.data));
393
787
  };
394
- eventSource.onerror = onError;
788
+ eventSource.onerror = error;
395
789
 
396
790
  return () => eventSource.close();
397
791
  }
@@ -424,7 +818,7 @@ import { createSharedState, createSharedFunction, createSharedSubscription, useS
424
818
  // Create and export shared resources
425
819
  export const counterState = createSharedState(0);
426
820
  export const fetchUserFunction = createSharedFunction(() => fetch('/api/me').then(r => r.json()));
427
- export const chatSubscription = createSharedSubscription((set, onError, onCompletion) => {/* ... */});
821
+ export const chatSubscription = createSharedSubscription((set, error, complete) => {/* ... */});
428
822
 
429
823
  // Use anywhere in your app
430
824
  const [count, setCount] = useSharedState(counterState);
@@ -463,11 +857,11 @@ const subStateScoped = sharedSubscriptionsApi.get('live-chat', 'myScope');
463
857
 
464
858
  ## API summary:
465
859
 
466
- | API | Methods |
467
- |--------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------|
468
- | `sharedStatesApi` | `get(key, scopeName?)`, `set(key, val, scopeName?)`, `has(key, scopeName?)`, `clear(key, scopeName?)`, `clearAll()`, `clearScope(scopeName?)`, `getAll()` |
469
- | `sharedFunctionsApi` | `get(key, scopeName?)`, `set(key, val, scopeName?)`, `has(key, scopeName?)`, `clear(key, scopeName?)`, `clearAll()`, `clearScope(scopeName?)`, `getAll()` |
470
- | `sharedSubscriptionsApi` | `get(key, scopeName?)`, `set(key, val, scopeName?)`, `has(key, scopeName?)`, `clear(key, scopeName?)`, `clearAll()`, `clearScope(scopeName?)`, `getAll()` |
860
+ | API | Methods |
861
+ |--------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
862
+ | `sharedStatesApi` | `get(key, scopeName?)`, `set(key, val, scopeName?)`, `has(key, scopeName?)`, `clear(key, scopeName?)`, `clearAll(withoutListeners?, withStatic?)`, `clearScope(scopeName?)`, `getAll()` |
863
+ | `sharedFunctionsApi` | `get(key, scopeName?)`, `set(key, val, scopeName?)`, `has(key, scopeName?)`, `clear(key, scopeName?)`, `clearAll(withoutListeners?, withStatic?)`, `clearScope(scopeName?)`, `getAll()` |
864
+ | `sharedSubscriptionsApi` | `get(key, scopeName?)`, `set(key, val, scopeName?)`, `has(key, scopeName?)`, `clear(key, scopeName?)`, `clearAll(withoutListeners?, withStatic?)`, `clearScope(scopeName?)`, `getAll()` |
471
865
 
472
866
  `scopeName` defaults to `"_global"`. Internally, keys are stored as `${scope}//${key}`. The `.getAll()` method returns a nested object: `{ [scope]: { [key]: value } }`.
473
867
 
@@ -497,14 +891,15 @@ Two providers sharing the same `scopeName` act as a single logical scope even if
497
891
 
498
892
  ## 🧪 Testing Tips
499
893
  * Use static APIs to assert state after component interactions.
500
- * `sharedStatesApi.clearAll()`, `sharedFunctionsApi.clearAll()`, `sharedSubscriptionsApi.clearAll()` in `afterEach` to isolate tests.
894
+ * `sharedStatesApi.clearAll(false, true)`, `sharedFunctionsApi.clearAll(false, true)`, `sharedSubscriptionsApi.clearAll(false, true)` in `afterEach` to isolate tests and clear static states.
501
895
  * For async functions: trigger once, await UI stabilization, assert `results` present.
502
896
  * For subscriptions: mock the subscription source (Firebase, WebSocket, etc.) and verify data flow.
503
897
 
504
898
 
505
899
  ## ❓ FAQ
506
900
  **Q: How do I reset a single shared state?**
507
- `sharedStatesApi.clear('key')` or inside component: call a setter with the initial value.
901
+ `sharedStatesApi.clear('key')`. If the state was created with `createSharedState`, it will reset to its initial value.
902
+ Otherwise, it will be removed.
508
903
 
509
904
  **Q: Can I pre-hydrate data on the server?**
510
905
  Yes. Call `sharedStatesApi.set(...)` during bootstrap, then first client hook usage will pick it up.
@@ -1,37 +1,44 @@
1
- import { AFunction, DataMapValue, Prefix, SharedCreated } from './types';
2
- type SharedDataType<T> = DataMapValue & T;
3
- export declare abstract class SharedData<T> {
4
- data: Map<string, SharedDataType<T>>;
5
- defaultValue(): T;
1
+ import { AFunction, Prefix, SharedCreated, SharedValue } from './types';
2
+ export declare const staticStores: SharedCreated[];
3
+ export declare abstract class SharedValuesManager<T extends SharedValue, V> {
4
+ data: Map<string, T>;
5
+ defaultValue(): V;
6
6
  addListener(key: string, prefix: Prefix, listener: AFunction): void;
7
7
  removeListener(key: string, prefix: Prefix, listener: AFunction): void;
8
8
  callListeners(key: string, prefix: Prefix): void;
9
- init(key: string, prefix: Prefix, data: T): void;
10
- clearAll(withoutListeners?: boolean): void;
11
- clear(key: string, prefix: Prefix, withoutListeners?: boolean): void;
12
- get(key: string, prefix: Prefix): SharedDataType<T> | undefined;
13
- setValue(key: string, prefix: Prefix, data: T): void;
9
+ init(key: string, prefix: Prefix, data: V, isStatic?: boolean): void;
10
+ createStatic<X extends SharedCreated>(rest: Omit<X, 'key' | 'prefix'>, scopeName?: Prefix): {
11
+ key: string;
12
+ prefix: Prefix;
13
+ } & Omit<X, "key" | "prefix">;
14
+ initStatic(sharedCreated: SharedCreated): void;
15
+ clearAll(withoutListeners?: boolean, withStatic?: boolean): void;
16
+ clear(key: string, prefix: Prefix, withoutListeners?: boolean, withStatic?: boolean): void;
17
+ get(key: string, prefix: Prefix): T | undefined;
18
+ setValue(key: string, prefix: Prefix, data: V): void;
14
19
  has(key: string, prefix: Prefix): string | undefined;
15
20
  static prefix(key: string, prefix: Prefix): string;
16
21
  static extractPrefix(mapKey: string): string[];
17
22
  useEffect(key: string, prefix: Prefix, unsub?: (() => void) | null): void;
18
23
  }
19
- export declare class SharedApi<T> {
20
- private sharedData;
21
- constructor(sharedData: SharedData<T>);
24
+ export declare class SharedValuesApi<T extends SharedValue, V, R = T> {
25
+ protected sharedData: SharedValuesManager<T, V>;
26
+ constructor(sharedData: SharedValuesManager<T, V>);
22
27
  /**
23
28
  * get a value from the shared data
24
29
  * @param key
25
30
  * @param scopeName
26
31
  */
27
- get<S extends string = string>(key: S, scopeName: Prefix): T;
32
+ get<S extends string = string>(key: S, scopeName: Prefix): R;
33
+ get<S extends string = string>(sharedCreated: SharedCreated): R;
28
34
  /**
29
35
  * set a value in the shared data
30
36
  * @param key
31
37
  * @param value
32
38
  * @param scopeName
33
39
  */
34
- set<S extends string = string>(key: S, value: T, scopeName: Prefix): void;
40
+ set<S extends string = string>(key: S, value: V, scopeName: Prefix): void;
41
+ set<S extends string = string>(sharedCreated: SharedCreated, value: V): void;
35
42
  /**
36
43
  * clear all values from the shared data
37
44
  */
@@ -45,7 +52,12 @@ export declare class SharedApi<T> {
45
52
  * resolve a shared created object to a value
46
53
  * @param sharedCreated
47
54
  */
48
- resolve(sharedCreated: SharedCreated): T;
55
+ resolve(sharedCreated: SharedCreated): R;
56
+ /**
57
+ * clear a value from the shared data
58
+ * @param key
59
+ * @param scopeName
60
+ */
49
61
  clear(key: string, scopeName: Prefix): void;
50
62
  clear(sharedCreated: SharedCreated): void;
51
63
  /**
@@ -59,4 +71,3 @@ export declare class SharedApi<T> {
59
71
  */
60
72
  getAll(): Record<string, Record<string, any>>;
61
73
  }
62
- export {};
@@ -1,15 +1,40 @@
1
- import { AFunction, Prefix, SharedCreated } from '../types';
2
- import { SharedApi } from '../SharedData';
3
- type SharedFunctionsState<T> = {
4
- fnState: {
5
- results?: T;
6
- isLoading: boolean;
7
- error?: unknown;
8
- };
1
+ import { AFunction, Prefix, SharedCreated, SharedValue } from '../types';
2
+ import { SharedValuesApi, SharedValuesManager } from '../SharedValuesManager';
3
+ type SharedFunctionValue<T> = {
4
+ results?: T;
5
+ isLoading: boolean;
6
+ error?: unknown;
9
7
  };
10
- export declare class SharedFunctionsApi extends SharedApi<SharedFunctionsState<unknown>> {
11
- get<T, S extends string = string>(key: S, scopeName?: Prefix): T;
12
- set<T, S extends string = string>(key: S, fnState: SharedFunctionsState<T>, scopeName?: Prefix): void;
8
+ interface SharedFunction<T> extends SharedValue {
9
+ fnState: SharedFunctionValue<T>;
10
+ }
11
+ declare class SharedFunctionsManager extends SharedValuesManager<SharedFunction<unknown>, {
12
+ fnState: SharedFunctionValue<unknown>;
13
+ }> {
14
+ defaultValue(): {
15
+ fnState: {
16
+ results: undefined;
17
+ isLoading: boolean;
18
+ error: undefined;
19
+ };
20
+ };
21
+ initValue(key: string, prefix: Prefix, isStatic?: boolean): void;
22
+ setValue<T>(key: string, prefix: Prefix, data: {
23
+ fnState: SharedFunctionValue<T>;
24
+ }): void;
25
+ }
26
+ export declare class SharedFunctionsApi extends SharedValuesApi<SharedFunction<unknown>, {
27
+ fnState: SharedFunctionValue<unknown>;
28
+ }, SharedFunctionValue<unknown>> {
29
+ constructor(sharedFunctionManager: SharedFunctionsManager);
30
+ get<T, S extends string = string>(key: S, scopeName?: Prefix): SharedFunctionValue<T>;
31
+ get<T, Args extends unknown[]>(sharedFunctionCreated: SharedFunctionCreated<T, Args>): SharedFunctionValue<T>;
32
+ set<T, S extends string = string>(key: S, value: {
33
+ fnState: SharedFunctionValue<T>;
34
+ }, scopeName?: Prefix): void;
35
+ set<T, Args extends unknown[]>(sharedFunctionCreated: SharedFunctionCreated<T, Args>, value: {
36
+ fnState: SharedFunctionValue<T>;
37
+ }): void;
13
38
  }
14
39
  export declare const sharedFunctionsApi: SharedFunctionsApi;
15
40
  interface SharedFunctionCreated<T, Args extends unknown[]> extends SharedCreated {
@@ -17,7 +42,7 @@ interface SharedFunctionCreated<T, Args extends unknown[]> extends SharedCreated
17
42
  }
18
43
  export declare const createSharedFunction: <T, Args extends unknown[]>(fn: AFunction<T, Args>, scopeName?: Prefix) => SharedFunctionCreated<T, Args>;
19
44
  export type SharedFunctionStateReturn<T, Args extends unknown[]> = {
20
- readonly state: NonNullable<SharedFunctionsState<T>['fnState']>;
45
+ readonly state: NonNullable<SharedFunctionValue<T>>;
21
46
  readonly trigger: (...args: Args) => void;
22
47
  readonly forceTrigger: (...args: Args) => void;
23
48
  readonly clear: () => void;