react-shared-states 1.0.7 → 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._**
@@ -48,7 +47,6 @@ function B(){
48
47
  }
49
48
 
50
49
  function App() {
51
-
52
50
  return (
53
51
  <>
54
52
  <A/>
@@ -69,7 +67,6 @@ function Scoped(){
69
67
  }
70
68
 
71
69
  function App() {
72
-
73
70
  return (
74
71
  <>
75
72
  <A/>
@@ -82,6 +79,33 @@ function App() {
82
79
  }
83
80
  ```
84
81
 
82
+ ---
83
+
84
+ > **Tip:** For large apps, you can also use `createSharedState(initialValue, scopeName?)` to create and export reusable shared states. If you specify a `scopeName`, the state will always be found in that scope; otherwise, it defaults to global. This helps avoid key collisions and ensures type safety.
85
+
86
+ ```tsx
87
+ import { useSharedState } from 'react-shared-states';
88
+ export const sharedCounter = createSharedState(0);
89
+
90
+ function A(){
91
+ const [count, setCount] = useSharedState(sharedCounter);
92
+ return <button onClick={()=>setCount(c=>c+1)}>A {count}</button>;
93
+ }
94
+ function B(){
95
+ const [count] = useSharedState(sharedCounter);
96
+ return <span>B sees {count}</span>;
97
+ }
98
+
99
+ function App() {
100
+ return (
101
+ <>
102
+ <A/>
103
+ <B/>
104
+ </>
105
+ )
106
+ }
107
+ ```
108
+
85
109
  Override / jump to a named scope explicitly:
86
110
  ```tsx
87
111
  useSharedState('counter', 0, 'modal'); // 3rd arg is scopeName override
@@ -173,56 +197,69 @@ export default function App(){
173
197
 
174
198
 
175
199
  ## 🧠 Core Concepts
176
- | Concept | Summary |
177
- |----------------------|---------------------------------------------------------------------------------------------------------------------------------|
178
- | Global by default | No provider necessary. Same key => shared state. |
179
- | Scoping | Wrap with `SharedStatesProvider` to isolate. Nearest provider wins. |
180
- | Named scopes | `scopeName` prop lets distant providers sync (same name ⇒ same bucket). Unnamed providers auto‑generate a random isolated name. |
181
- | Manual override | Third param in `useSharedState` / `useSharedFunction` / `useSharedSubscription` enforces a specific scope ignoring tree search. |
182
- | Shared functions | Encapsulate async logic: single flight + cached result + `error` + `isLoading` + opt‑in refresh. |
183
- | Shared subscriptions | Real-time data streams: automatic cleanup + shared connections + `error` + `isLoading` + subscription state. |
184
- | Static APIs | Access state/functions/subscriptions outside components (`sharedStatesApi`, `sharedFunctionsApi`, `sharedSubscriptionsApi`). |
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. |
185
210
 
186
211
 
187
212
  ## 🏗️ Sharing State (`useSharedState`)
188
- Signature: `const [value, setValue] = useSharedState(key, initialValue, scopeName?);`
213
+ Signature:
214
+ - `const [value, setValue] = useSharedState(key, initialValue, scopeName?)`
215
+ - `const [value, setValue] = useSharedState(sharedStateCreated)`
189
216
 
190
217
  Behavior:
191
218
  * First hook call (per key + scope) seeds with `initialValue`.
192
219
  * Subsequent mounts with same key+scope ignore their `initialValue` (consistent source of truth).
193
220
  * Setter accepts either value or updater `(prev)=>next`.
194
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.
195
223
 
196
224
  ### Examples
197
- 1. Global theme
225
+ 1. Global theme (recommended for large apps)
198
226
  ```tsx
199
- const [theme, setTheme] = useSharedState('theme', 'light');
227
+ // themeState.ts
228
+ export const themeState = createSharedState('light');
229
+ // In components
230
+ const [theme, setTheme] = useSharedState(themeState);
200
231
  ```
201
232
  2. Isolated wizard progress
202
233
  ```tsx
234
+ const wizardProgress = createSharedState(0);
203
235
  <SharedStatesProvider>
204
236
  <Wizard/>
205
237
  </SharedStatesProvider>
238
+ // In Wizard
239
+ const [step, setStep] = useSharedState(wizardProgress);
206
240
  ```
207
241
  3. Forcing cross‑portal sync
208
242
  ```tsx
243
+ const navState = createSharedState('closed', 'nav');
209
244
  <SharedStatesProvider scopeName="nav" children={<PrimaryNav/>} />
210
245
  <Portal>
211
246
  <SharedStatesProvider scopeName="nav" children={<MobileNav/>} />
212
247
  </Portal>
248
+ // In both navs
249
+ const [navOpen, setNavOpen] = useSharedState(navState);
213
250
  ```
214
251
  4. Overriding nearest provider
215
252
  ```tsx
216
253
  // Even if inside a provider, this explicitly binds to global
217
- const [flag, setFlag] = useSharedState('feature-x-enabled', false, '_global');
254
+ const globalFlag = createSharedState(false, '_global');
255
+ const [flag, setFlag] = useSharedState(globalFlag);
218
256
  ```
219
257
 
220
258
 
221
259
  ## ⚡ Shared Async Functions (`useSharedFunction`)
222
260
  Signature:
223
- ```ts
224
- const { state, trigger, forceTrigger, clear } = useSharedFunction(key, asyncFn, scopeName?);
225
- ```
261
+ - `const { state, trigger, forceTrigger, clear } = useSharedFunction(key, asyncFn, scopeName?)`
262
+ - `const { state, trigger, forceTrigger, clear } = useSharedFunction(sharedFunctionCreated)`
226
263
  `state` shape: `{ results?: T; isLoading: boolean; error?: unknown }`
227
264
 
228
265
  Semantics:
@@ -230,15 +267,16 @@ Semantics:
230
267
  * Multiple components with the same key+scope share one execution + result.
231
268
  * `clear()` deletes the cache (next trigger re-runs).
232
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.
233
271
 
234
272
  ### Pattern: lazy load on first render
235
273
  ```tsx
274
+ // profileFunction.ts
275
+ export const profileFunction = createSharedFunction((id: string) => fetch(`/api/p/${id}`).then(r=>r.json()));
276
+
236
277
  function Profile({id}:{id:string}){
237
- const { state, trigger } = useSharedFunction(`profile-${id}`, () => fetch(`/api/p/${id}`).then(r=>r.json()));
238
-
239
- if(!state.results && !state.isLoading) trigger();
240
- if(state.isLoading) return <p>Loading...</p>;
241
- return <pre>{JSON.stringify(state.results,null,2)}</pre>
278
+ const { state, trigger } = useSharedFunction(profileFunction);
279
+ // ...same as before
242
280
  }
243
281
  ```
244
282
 
@@ -254,50 +292,425 @@ Perfect for Firebase listeners, WebSocket connections,
254
292
  Server-Sent Events, or any streaming data source that needs cleanup.
255
293
 
256
294
  Signature:
257
- ```ts
258
- const { state, trigger, unsubscribe } = useSharedSubscription(key, subscriber, scopeName?);
259
- ```
295
+ - `const { state, trigger, unsubscribe } = useSharedSubscription(key, subscriber, scopeName?)`
296
+ - `const { state, trigger, unsubscribe } = useSharedSubscription(sharedSubscriptionCreated)`
260
297
 
261
298
  `state` shape: `{ data?: T; isLoading: boolean; error?: unknown; subscribed: boolean }`
262
299
 
263
300
  The `subscriber` function receives three callbacks:
264
301
  - `set(data)`: Update the shared data
265
- - `onError(error)`: Handle errors
266
- - `onCompletion()`: Mark loading as complete
302
+ - `error(error)`: Handle errors
303
+ - `complete()`: Mark loading as complete
267
304
  - Returns: Optional cleanup function (called on unsubscribe/unmount)
268
305
 
269
306
  ### Pattern: Firebase Firestore real-time listener
270
307
  ```tsx
271
- import { useEffect } from 'react';
308
+ // userSubscription.ts
272
309
  import { onSnapshot, doc } from 'firebase/firestore';
273
- import { useSharedSubscription } from 'react-shared-states';
274
- import { db } from './firebase-config'; // your Firebase config
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
+ );
275
333
 
276
334
  function UserProfile({ userId }: { userId: string }) {
277
- const { state, trigger, unsubscribe } = useSharedSubscription(
278
- `user-${userId}`,
279
- async (set, onError, onCompletion) => {
280
- const userRef = doc(db, 'users', userId);
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');
281
405
 
282
- // Set up the real-time listener
283
- const unsubscribe = onSnapshot(
284
- userRef,
285
- (snapshot) => {
286
- if (snapshot.exists()) {
287
- set({ id: snapshot.id, ...snapshot.data() });
288
- } else {
289
- set(null);
290
- }
291
- },
292
- onError,
293
- onCompletion
294
- );
406
+ eventSource.onopen = () => complete();
407
+ eventSource.onmessage = (event) => {
408
+ set(JSON.parse(event.data));
409
+ };
410
+ eventSource.onerror = error;
295
411
 
296
- // Return cleanup function
297
- return unsubscribe;
412
+ return () => eventSource.close();
298
413
  }
299
414
  );
300
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
+
590
+
591
+
592
+ ## 🏗️ Sharing State (`useSharedState`)
593
+ Signature:
594
+ - `const [value, setValue] = useSharedState(key, initialValue, scopeName?)`
595
+ - `const [value, setValue] = useSharedState(sharedStateCreated)`
596
+
597
+ Behavior:
598
+ * First hook call (per key + scope) seeds with `initialValue`.
599
+ * Subsequent mounts with same key+scope ignore their `initialValue` (consistent source of truth).
600
+ * Setter accepts either value or updater `(prev)=>next`.
601
+ * React batching + equality check: listeners fire only when the value reference actually changes.
602
+
603
+ ### Examples
604
+ 1. Global theme (recommended for large apps)
605
+ ```tsx
606
+ // themeState.ts
607
+ export const themeState = createSharedState('light');
608
+ // In components
609
+ const [theme, setTheme] = useSharedState(themeState);
610
+ ```
611
+ 2. Isolated wizard progress
612
+ ```tsx
613
+ const wizardProgress = createSharedState(0);
614
+ <SharedStatesProvider>
615
+ <Wizard/>
616
+ </SharedStatesProvider>
617
+ // In Wizard
618
+ const [step, setStep] = useSharedState(wizardProgress);
619
+ ```
620
+ 3. Forcing cross‑portal sync
621
+ ```tsx
622
+ const navState = createSharedState('closed', 'nav');
623
+ <SharedStatesProvider scopeName="nav" children={<PrimaryNav/>} />
624
+ <Portal>
625
+ <SharedStatesProvider scopeName="nav" children={<MobileNav/>} />
626
+ </Portal>
627
+ // In both navs
628
+ const [navOpen, setNavOpen] = useSharedState(navState);
629
+ ```
630
+ 4. Overriding nearest provider
631
+ ```tsx
632
+ // Even if inside a provider, this explicitly binds to global
633
+ const globalFlag = createSharedState(false, '_global');
634
+ const [flag, setFlag] = useSharedState(globalFlag);
635
+ ```
636
+
637
+
638
+ ## ⚡ Shared Async Functions (`useSharedFunction`)
639
+ Signature:
640
+ - `const { state, trigger, forceTrigger, clear } = useSharedFunction(key, asyncFn, scopeName?)`
641
+ - `const { state, trigger, forceTrigger, clear } = useSharedFunction(sharedFunctionCreated)`
642
+ `state` shape: `{ results?: T; isLoading: boolean; error?: unknown }`
643
+
644
+ Semantics:
645
+ * First `trigger()` (implicit or manual) runs the function; subsequent calls do nothing while loading or after success (cached) unless you `forceTrigger()`.
646
+ * Multiple components with the same key+scope share one execution + result.
647
+ * `clear()` deletes the cache (next trigger re-runs).
648
+ * You decide when to invoke `trigger` (e.g. on mount, on button click, when dependencies change, etc.).
649
+
650
+ ### Pattern: lazy load on first render
651
+ ```tsx
652
+ // profileFunction.ts
653
+ export const profileFunction = createSharedFunction((id: string) => fetch(`/api/p/${id}`).then(r=>r.json()));
654
+
655
+ function Profile({id}:{id:string}){
656
+ const { state, trigger } = useSharedFunction(profileFunction);
657
+ // ...same as before
658
+ }
659
+ ```
660
+
661
+ ### Pattern: always fetch fresh
662
+ ```tsx
663
+ const { state, forceTrigger } = useSharedFunction('server-time', () => fetch('/time').then(r=>r.text()));
664
+ const refresh = () => forceTrigger();
665
+ ```
666
+
667
+
668
+ ## 📡 Real-time Subscriptions (`useSharedSubscription`)
669
+ Perfect for Firebase listeners, WebSocket connections,
670
+ Server-Sent Events, or any streaming data source that needs cleanup.
671
+
672
+ Signature:
673
+ - `const { state, trigger, unsubscribe } = useSharedSubscription(key, subscriber, scopeName?)`
674
+ - `const { state, trigger, unsubscribe } = useSharedSubscription(sharedSubscriptionCreated)`
675
+
676
+ `state` shape: `{ data?: T; isLoading: boolean; error?: unknown; subscribed: boolean }`
677
+
678
+ The `subscriber` function receives three callbacks:
679
+ - `set(data)`: Update the shared data
680
+ - `error(error)`: Handle errors
681
+ - `complete()`: Mark loading as complete
682
+ - Returns: Optional cleanup function (called on unsubscribe/unmount)
683
+
684
+ ### Pattern: Firebase Firestore real-time listener
685
+ ```tsx
686
+ // userSubscription.ts
687
+ import { onSnapshot, doc } from 'firebase/firestore';
688
+ import { createSharedSubscription } from 'react-shared-states';
689
+ import { db } from './firebase-config';
690
+
691
+ export const userSubscription = createSharedSubscription(
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;
709
+ }
710
+ );
711
+
712
+ function UserProfile({ userId }: { userId: string }) {
713
+ const { state, trigger, unsubscribe } = useSharedSubscription(userSubscription);
301
714
  // Start listening when component mounts
302
715
  useEffect(() => {
303
716
  trigger();
@@ -325,15 +738,15 @@ import { useSharedSubscription } from 'react-shared-states';
325
738
  function ChatRoom({ roomId }: { roomId: string }) {
326
739
  const { state, trigger } = useSharedSubscription(
327
740
  `chat-${roomId}`,
328
- (set, onError, onCompletion) => {
741
+ (set, error, complete) => {
329
742
  const ws = new WebSocket(`ws://chat-server/${roomId}`);
330
743
 
331
- ws.onopen = () => onCompletion();
744
+ ws.onopen = () => complete();
332
745
  ws.onmessage = (event) => {
333
746
  const message = JSON.parse(event.data);
334
747
  set(prev => [...(prev || []), message]);
335
748
  };
336
- ws.onerror = onError;
749
+ ws.onerror = error;
337
750
 
338
751
  return () => ws.close();
339
752
  }
@@ -365,14 +778,14 @@ import { useSharedSubscription } from 'react-shared-states';
365
778
  function LiveUpdates() {
366
779
  const { state, trigger } = useSharedSubscription(
367
780
  'live-updates',
368
- (set, onError, onCompletion) => {
781
+ (set, error, complete) => {
369
782
  const eventSource = new EventSource('/api/live-updates');
370
783
 
371
- eventSource.onopen = () => onCompletion();
784
+ eventSource.onopen = () => complete();
372
785
  eventSource.onmessage = (event) => {
373
786
  set(JSON.parse(event.data));
374
787
  };
375
- eventSource.onerror = onError;
788
+ eventSource.onerror = error;
376
789
 
377
790
  return () => eventSource.close();
378
791
  }
@@ -395,36 +808,62 @@ Subscription semantics:
395
808
 
396
809
 
397
810
  ## 🛰️ Static APIs (outside React)
811
+ ## 🏛️ Static/Global Shared Resource Creation
812
+
813
+ For large apps, you can create and export shared state, function, or subscription objects for type safety and to avoid key collisions. This pattern is similar to Zustand or Jotai stores:
814
+
815
+ ```ts
816
+ import { createSharedState, createSharedFunction, createSharedSubscription, useSharedState, useSharedFunction, useSharedSubscription } from 'react-shared-states';
817
+
818
+ // Create and export shared resources
819
+ export const counterState = createSharedState(0);
820
+ export const fetchUserFunction = createSharedFunction(() => fetch('/api/me').then(r => r.json()));
821
+ export const chatSubscription = createSharedSubscription((set, error, complete) => {/* ... */});
822
+
823
+ // Use anywhere in your app
824
+ const [count, setCount] = useSharedState(counterState);
825
+ const { state, trigger } = useSharedFunction(fetchUserFunction);
826
+ const { state, trigger, unsubscribe } = useSharedSubscription(chatSubscription);
827
+ ```
398
828
  Useful for SSR hydration, event listeners, debugging, imperative workflows.
399
829
 
400
830
  ```ts
401
831
  import { sharedStatesApi, sharedFunctionsApi, sharedSubscriptionsApi } from 'react-shared-states';
402
832
 
403
- // Preload state
833
+ // Preload state (global scope by default)
404
834
  sharedStatesApi.set('bootstrap-data', { user: {...} });
405
835
 
836
+ // Preload state in a named scope
837
+ sharedStatesApi.set('bootstrap-data', { user: {...} }, 'myScope');
838
+
406
839
  // Read later
407
- const user = sharedStatesApi.get('bootstrap-data');
840
+ const user = sharedStatesApi.get('bootstrap-data'); // global
841
+ const userScoped = sharedStatesApi.get('bootstrap-data', 'myScope');
408
842
 
409
- // Inspect all
410
- console.log(sharedStatesApi.getAll()); // Map with prefixed keys
843
+ // Inspect all (returns nested object: { [scope]: { [key]: value } })
844
+ console.log(sharedStatesApi.getAll());
845
+
846
+ // Clear all keys in a scope
847
+ sharedStatesApi.clearScope('myScope');
411
848
 
412
849
  // For shared functions
413
850
  const fnState = sharedFunctionsApi.get('profile-123');
851
+ const fnStateScoped = sharedFunctionsApi.get('profile-123', 'myScope');
414
852
 
415
853
  // For shared subscriptions
416
854
  const subState = sharedSubscriptionsApi.get('live-chat');
855
+ const subStateScoped = sharedSubscriptionsApi.get('live-chat', 'myScope');
417
856
  ```
418
857
 
419
858
  ## API summary:
420
859
 
421
- | API | Methods |
422
- |--------------------------|---------------------------------------------------------------------------------------|
423
- | `sharedStatesApi` | `get(key, scope?)`, `set(key,val,scope?)`, `has`, `clear`, `clearAll`, `getAll()` |
424
- | `sharedFunctionsApi` | `get(key, scope?)` (returns fn state), `set`, `has`, `clear`, `clearAll`, `getAll()` |
425
- | `sharedSubscriptionsApi` | `get(key, scope?)` (returns sub state), `set`, `has`, `clear`, `clearAll`, `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()` |
426
865
 
427
- `scope` defaults to `"_global"`. Internally keys are stored as `${scope}_${key}`.
866
+ `scopeName` defaults to `"_global"`. Internally, keys are stored as `${scope}//${key}`. The `.getAll()` method returns a nested object: `{ [scope]: { [key]: value } }`.
428
867
 
429
868
 
430
869
  ## 🧩 Scoping Rules Deep Dive
@@ -452,14 +891,15 @@ Two providers sharing the same `scopeName` act as a single logical scope even if
452
891
 
453
892
  ## 🧪 Testing Tips
454
893
  * Use static APIs to assert state after component interactions.
455
- * `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.
456
895
  * For async functions: trigger once, await UI stabilization, assert `results` present.
457
896
  * For subscriptions: mock the subscription source (Firebase, WebSocket, etc.) and verify data flow.
458
897
 
459
898
 
460
899
  ## ❓ FAQ
461
900
  **Q: How do I reset a single shared state?**
462
- `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.
463
903
 
464
904
  **Q: Can I pre-hydrate data on the server?**
465
905
  Yes. Call `sharedStatesApi.set(...)` during bootstrap, then first client hook usage will pick it up.
@@ -481,16 +921,25 @@ Currently no built-in Suspense wrappers; wrap `useSharedFunction` yourself if de
481
921
  ### `useSharedState(key, initialValue, scopeName?)`
482
922
  Returns `[value, setValue]`.
483
923
 
924
+ ### `useSharedState(sharedStateCreated)`
925
+ Returns `[value, setValue]`.
926
+
484
927
  ### `useSharedFunction(key, fn, scopeName?)`
485
928
  Returns `{ state, trigger, forceTrigger, clear }`.
486
929
 
930
+ ### `useSharedFunction(sharedFunctionCreated)`
931
+ Returns `{ state, trigger, forceTrigger, clear }`.
932
+
487
933
  ### `useSharedSubscription(key, subscriber, scopeName?)`
488
934
  Returns `{ state, trigger, unsubscribe }`.
489
935
 
936
+ ### `useSharedSubscription(sharedSubscriptionCreated)`
937
+ Returns `{ state, trigger, unsubscribe }`.
938
+
490
939
  ### `<SharedStatesProvider scopeName?>`
491
940
  Wrap children; optional `scopeName` (string). If omitted a random unique one is generated.
492
941
 
493
- ### Static
942
+ ### Static APIs
494
943
  `sharedStatesApi`, `sharedFunctionsApi`, `sharedSubscriptionsApi` (see earlier table).
495
944
 
496
945