react-shared-states 1.0.11 → 1.0.13

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
@@ -23,7 +23,7 @@ Tiny, ergonomic, convention‑over‑configuration state, async function, and re
23
23
  * Predictable: key + scope ⇒ value. That’s it.
24
24
 
25
25
 
26
- ## 🚀 Install
26
+ ## Install
27
27
 
28
28
  ```sh
29
29
  npm install react-shared-states
@@ -33,7 +33,7 @@ or
33
33
  pnpm add react-shared-states
34
34
  ```
35
35
 
36
- ## 60‑Second TL;DR
36
+ ## 60‑Second TL;DR
37
37
  ```tsx
38
38
  import { useSharedState } from 'react-shared-states';
39
39
 
@@ -196,7 +196,7 @@ export default function App(){
196
196
  ```
197
197
 
198
198
 
199
- ## 🧠 Core Concepts
199
+ ## Core Concepts
200
200
  | Concept | Summary |
201
201
  |------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------|
202
202
  | Global by default | No provider necessary. Same key => shared state. |
@@ -209,433 +209,92 @@ export default function App(){
209
209
  | Static/shared creation | Use `createSharedState`, `createSharedFunction`, `createSharedSubscription` to export reusable, type-safe shared resources that persist across `clearAll()` calls. |
210
210
 
211
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.
212
+ ## Selecting State Slices (`useSharedStateSelector`)
271
213
 
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()));
214
+ When a shared state holds an object, you might only need a small piece of it.
215
+ Using `useSharedState` will cause your component to re-render whenever *any* part of the object changes.
216
+ To optimize performance and avoid unnecessary re-renders, you can use the `useSharedStateSelector` hook.
276
217
 
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.
218
+ This hook allows you to subscribe to a specific, memoized slice of a shared state.
219
+ Your component will only re-render if the selected value changes.
220
+ It uses `react-fast-compare` for efficient deep equality checks.
293
221
 
294
222
  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)
223
+ - `const selectedValue = useSharedStateSelector(key, selector, scopeName?)`
224
+ - `const selectedValue = useSharedStateSelector(sharedStateCreated, selector)`
305
225
 
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';
226
+ The `selector` is a function that receives the full state and returns the desired slice.
312
227
 
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
- );
228
+ ### Example: Subscribing to a slice of a user object
333
229
 
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
- ```
230
+ Imagine a shared state for user settings:
354
231
 
355
- ### Pattern: WebSocket connection
356
232
  ```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
- }
233
+ // settingsState.ts
234
+ import { createSharedState } from 'react-shared-states';
235
+
236
+ export const settingsState = createSharedState({
237
+ theme: 'dark',
238
+ notifications: {
239
+ email: true,
240
+ push: false,
241
+ },
242
+ language: 'en',
243
+ });
393
244
  ```
394
245
 
395
- ### Pattern: Server-Sent Events
396
- ```tsx
397
- import { useEffect } from 'react';
398
- import { useSharedSubscription } from 'react-shared-states';
246
+ A component that only cares about the theme can use `useSharedStateSelector` to avoid re-rendering when, for example, the notification settings change.
399
247
 
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
- );
248
+ ```tsx
249
+ import { useSharedState, useSharedStateSelector } from 'react-shared-states';
250
+ import { settingsState } from './settingsState';
415
251
 
416
- useEffect(() => {
417
- trigger();
418
- }, []);
252
+ function ThemeDisplay() {
253
+ const theme = useSharedStateSelector(settingsState, (settings) => settings.theme);
254
+
255
+ console.log('ThemeDisplay renders'); // This will only log when the theme changes
419
256
 
420
- return <div>Latest: {JSON.stringify(state.data)}</div>;
257
+ return <div>Current theme: {theme}</div>;
421
258
  }
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
259
 
260
+ function NotificationToggle() {
261
+ const [settings, setSettings] = useSharedState(settingsState);
431
262
 
432
- ## 🛰️ Static APIs (outside React)
433
- ## 🏛️ Static/Global Shared Resource Creation
263
+ const togglePush = () => {
264
+ setSettings(s => ({
265
+ ...s,
266
+ notifications: { ...s.notifications, push: !s.notifications.push }
267
+ }));
268
+ };
434
269
 
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);
270
+ return <button onClick={togglePush}>Toggle Push Notifications</button>;
271
+ }
451
272
  ```
452
- Useful for SSR hydration, event listeners, debugging, imperative workflows.
453
273
 
454
- ```ts
455
- import { sharedStatesApi, sharedFunctionsApi, sharedSubscriptionsApi } from 'react-shared-states';
274
+ In this example, clicking the `NotificationToggle` button will **not** cause `ThemeDisplay` to re-render.
456
275
 
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');
276
+ ### A Note on Type Safety
462
277
 
463
- // Read later
464
- const user = sharedStatesApi.get('bootstrap-data'); // global
465
- const userScoped = sharedStatesApi.get('bootstrap-data', 'myScope');
278
+ For the best developer experience and full type safety,
279
+ it is **highly recommended** to use `useSharedStateSelector` with a statically created shared state object from `createSharedState`.
466
280
 
467
- // Inspect all (returns nested object: { [scope]: { [key]: value } })
468
- console.log(sharedStatesApi.getAll());
281
+ When you use a string key directly, the hook cannot infer the type of the state object.
282
+ You would have to provide the types explicitly as generic arguments, or they will default to `any`.
469
283
 
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');
284
+ ```tsx
285
+ // Less safe: using a string key
286
+ // You have to specify the types manually.
287
+ const theme = useSharedStateSelector<{ theme: string; /*...other props*/ }, 'settings', string>(
288
+ 'settings',
289
+ (settings) => settings.theme
290
+ );
476
291
 
477
- // For shared subscriptions
478
- const subState = sharedSubscriptionsApi.get('live-chat');
479
- const subStateScoped = sharedSubscriptionsApi.get('live-chat', 'myScope');
292
+ // Recommended: using a created state object
293
+ // Types are inferred automatically!
294
+ const theme = useSharedStateSelector(settingsState, (settings) => settings.theme);
480
295
  ```
481
296
 
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`)
297
+ ## Shared Async Functions (`useSharedFunction`)
639
298
  Signature:
640
299
  - `const { state, trigger, forceTrigger, clear } = useSharedFunction(key, asyncFn, scopeName?)`
641
300
  - `const { state, trigger, forceTrigger, clear } = useSharedFunction(sharedFunctionCreated)`
@@ -646,6 +305,7 @@ Semantics:
646
305
  * Multiple components with the same key+scope share one execution + result.
647
306
  * `clear()` deletes the cache (next trigger re-runs).
648
307
  * You decide when to invoke `trigger` (e.g. on mount, on button click, when dependencies change, etc.).
308
+ * Functions created with `createSharedFunction` are **static** and persist across `clearAll()` calls.
649
309
 
650
310
  ### Pattern: lazy load on first render
651
311
  ```tsx
@@ -665,7 +325,7 @@ const refresh = () => forceTrigger();
665
325
  ```
666
326
 
667
327
 
668
- ## 📡 Real-time Subscriptions (`useSharedSubscription`)
328
+ ## Real-time Subscriptions (`useSharedSubscription`)
669
329
  Perfect for Firebase listeners, WebSocket connections,
670
330
  Server-Sent Events, or any streaming data source that needs cleanup.
671
331
 
@@ -807,10 +467,12 @@ Subscription semantics:
807
467
  * Components mounting later instantly get the latest `data` without re-subscribing.
808
468
 
809
469
 
810
- ## 🛰️ Static APIs (outside React)
811
- ## 🏛️ Static/Global Shared Resource Creation
470
+ ## Static APIs (outside React)
471
+ ## Static/Global Shared Resource Creation
812
472
 
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:
473
+ For large apps, you can create and export shared state, function,
474
+ or subscription objects for type safety and to avoid key collisions.
475
+ This pattern is similar to Zustand or Jotai stores:
814
476
 
815
477
  ```ts
816
478
  import { createSharedState, createSharedFunction, createSharedSubscription, useSharedState, useSharedFunction, useSharedSubscription } from 'react-shared-states';
@@ -866,7 +528,7 @@ const subStateScoped = sharedSubscriptionsApi.get('live-chat', 'myScope');
866
528
  `scopeName` defaults to `"_global"`. Internally, keys are stored as `${scope}//${key}`. The `.getAll()` method returns a nested object: `{ [scope]: { [key]: value } }`.
867
529
 
868
530
 
869
- ## 🧩 Scoping Rules Deep Dive
531
+ ## Scoping Rules Deep Dive
870
532
  Resolution order used inside hooks:
871
533
  1. Explicit 3rd parameter (`scopeName`)
872
534
  2. Nearest `SharedStatesProvider` above the component
@@ -877,7 +539,7 @@ Unnamed providers auto‑generate a random scope name: each mount = isolated isl
877
539
  Two providers sharing the same `scopeName` act as a single logical scope even if they are disjoint in the tree (great for portals / microfrontends).
878
540
 
879
541
 
880
- ## 🆚 Comparison Snapshot
542
+ ## Comparison Snapshot
881
543
  | Criterion | react-shared-states | Redux Toolkit | Zustand |
882
544
  |----------------|------------------------------------------|----------------------|----------------------------------|
883
545
  | Setup | Install & call hook | Slice + store config | Create store function |
@@ -889,7 +551,7 @@ Two providers sharing the same `scopeName` act as a single logical scope even if
889
551
  | Learning curve | Minutes | Higher | Low |
890
552
 
891
553
 
892
- ## 🧪 Testing Tips
554
+ ## Testing Tips
893
555
  * Use static APIs to assert state after component interactions.
894
556
  * `sharedStatesApi.clearAll(false, true)`, `sharedFunctionsApi.clearAll(false, true)`, `sharedSubscriptionsApi.clearAll(false, true)` in `afterEach` to isolate tests and clear static states.
895
557
  * For async functions: trigger once, await UI stabilization, assert `results` present.
@@ -898,8 +560,7 @@ Two providers sharing the same `scopeName` act as a single logical scope even if
898
560
 
899
561
  ## ❓ FAQ
900
562
  **Q: How do I reset a single shared state?**
901
- `sharedStatesApi.clear('key')`. If the state was created with `createSharedState`, it will reset to its initial value.
902
- Otherwise, it will be removed.
563
+ `sharedStatesApi.clear('key')`. If the state was created with `createSharedState`, it will reset to its initial value. Otherwise, it will be removed.
903
564
 
904
565
  **Q: Can I pre-hydrate data on the server?**
905
566
  Yes. Call `sharedStatesApi.set(...)` during bootstrap, then first client hook usage will pick it up.
@@ -917,13 +578,19 @@ Subscriptions auto-cleanup when no components are listening. You can also manual
917
578
  Currently no built-in Suspense wrappers; wrap `useSharedFunction` yourself if desired.
918
579
 
919
580
 
920
- ## 📚 Full API Reference
581
+ ## Full API Reference
921
582
  ### `useSharedState(key, initialValue, scopeName?)`
922
583
  Returns `[value, setValue]`.
923
584
 
924
585
  ### `useSharedState(sharedStateCreated)`
925
586
  Returns `[value, setValue]`.
926
587
 
588
+ ### `useSharedStateSelector(key, selector, scopeName?)`
589
+ Returns the selected value.
590
+
591
+ ### `useSharedStateSelector(sharedStateCreated, selector)`
592
+ Returns the selected value.
593
+
927
594
  ### `useSharedFunction(key, fn, scopeName?)`
928
595
  Returns `{ state, trigger, forceTrigger, clear }`.
929
596
 
@@ -944,7 +611,7 @@ Wrap children; optional `scopeName` (string). If omitted a random unique one is
944
611
 
945
612
 
946
613
 
947
- ## 🤝 Contributions
614
+ ## Contributions
948
615
 
949
616
  We welcome contributions!
950
617
  If you'd like to improve `react-shared-states`,
@@ -962,4 +629,6 @@ feel free to [open an issue](https://github.com/HichemTab-tech/react-shared-stat
962
629
  ## 🌟 Acknowledgements
963
630
 
964
631
  Inspired by React's built-in primitives and the ergonomics of modern lightweight state libraries.
965
- Thanks to early adopters for feedback.
632
+ Thanks to early adopters for feedback.
633
+ If you'd like to improve `react-shared-states`,
634
+ 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).
@@ -1,5 +1,5 @@
1
- export { useSharedState, sharedStatesApi, createSharedState, SharedStatesApi } from './use-shared-state';
2
- export type { SharedStateCreated } from './use-shared-state';
1
+ export { useSharedState, sharedStatesApi, createSharedState, SharedStatesApi, useSharedStateSelector } from './use-shared-state';
2
+ export type { SharedStateCreated, SharedStateSelector } from './use-shared-state';
3
3
  export { useSharedFunction, sharedFunctionsApi, createSharedFunction, SharedFunctionsApi } from './use-shared-function';
4
4
  export type { SharedFunctionStateReturn } from './use-shared-function';
5
5
  export { useSharedSubscription, sharedSubscriptionsApi, createSharedSubscription, SharedSubscriptionsApi } from './use-shared-subscription';
@@ -28,4 +28,7 @@ export interface SharedStateCreated<T> extends SharedCreated {
28
28
  export declare const createSharedState: <T>(initialValue: T, scopeName?: Prefix) => SharedStateCreated<T>;
29
29
  export declare function useSharedState<T, S extends string>(key: S, initialValue: T, scopeName?: Prefix): readonly [T, (v: T | ((prev: T) => T)) => void];
30
30
  export declare function useSharedState<T>(sharedStateCreated: SharedStateCreated<T>): readonly [T, (v: T | ((prev: T) => T)) => void];
31
+ export type SharedStateSelector<S, T = S> = (original: S) => T;
32
+ export declare function useSharedStateSelector<T, S extends string, R>(key: S, selector: SharedStateSelector<T, R>, scopeName?: Prefix): Readonly<R>;
33
+ export declare function useSharedStateSelector<T, R>(sharedStateCreated: SharedStateCreated<T>, selector: SharedStateSelector<T, R>): Readonly<R>;
31
34
  export {};