@@ -630,373 +630,6 @@ feel free to [open an issue](https://github.com/HichemTab-tech/react-shared-stat
630630
631631Inspired by React's built-in primitives and the ergonomics of modern lightweight state libraries.
632632Thanks to early adopters for feedback.
633-
634-
635-
636- ## 🏗️ Sharing State (` useSharedState ` )
637- Signature:
638- - ` const [value, setValue] = useSharedState(key, initialValue, scopeName?) `
639- - ` const [value, setValue] = useSharedState(sharedStateCreated) `
640-
641- Behavior:
642- * First hook call (per key + scope) seeds with ` initialValue ` .
643- * Subsequent mounts with same key+scope ignore their ` initialValue ` (consistent source of truth).
644- * Setter accepts either value or updater ` (prev)=>next ` .
645- * React batching + equality check: listeners fire only when the value reference actually changes.
646-
647- ### Examples
648- 1 . Global theme (recommended for large apps)
649- ``` tsx
650- // themeState.ts
651- export const themeState = createSharedState (' light' );
652- // In components
653- const [theme, setTheme] = useSharedState (themeState );
654- ```
655- 2. Isolated wizard progress
656- ` ` ` tsx
657- const wizardProgress = createSharedState(0);
658- <SharedStatesProvider>
659- <Wizard/>
660- </SharedStatesProvider>
661- // In Wizard
662- const [step, setStep] = useSharedState(wizardProgress);
663- ` ` `
664- 3. Forcing cross ‑portal sync
665- ` ` ` tsx
666- const navState = createSharedState('closed', 'nav');
667- <SharedStatesProvider scopeName="nav" children={<PrimaryNav/>} />
668- <Portal>
669- <SharedStatesProvider scopeName="nav" children={<MobileNav/>} />
670- </Portal>
671- // In both navs
672- const [navOpen, setNavOpen] = useSharedState(navState);
673- ` ` `
674- 4. Overriding nearest provider
675- ` ` ` tsx
676- // Even if inside a provider, this explicitly binds to global
677- const globalFlag = createSharedState(false, '_global');
678- const [flag, setFlag] = useSharedState(globalFlag);
679- ` ` `
680-
681-
682- ## ⚡ Shared Async Functions (` useSharedFunction ` )
683- Signature :
684- - ` const { state, trigger, forceTrigger, clear } = useSharedFunction(key, asyncFn, scopeName?) `
685- - ` const { state, trigger, forceTrigger, clear } = useSharedFunction(sharedFunctionCreated) `
686- ` state ` shape : ` { results?: T; isLoading: boolean; error?: unknown } `
687-
688- Semantics :
689- * First ` trigger() ` (implicit or manual ) runs the function ; subsequent calls do nothing while loading or after success (cached ) unless you ` forceTrigger() ` .
690- * Multiple components with the same key + scope share one execution + result .
691- * ` clear() ` deletes the cache (next trigger re - runs ).
692- * You decide when to invoke ` trigger ` (e .g . on mount , on button click , when dependencies change , etc .).
693-
694- ### Pattern : lazy load on first render
695- ` ` ` tsx
696- // profileFunction.ts
697- export const profileFunction = createSharedFunction((id: string) => fetch( ` / api / p / $ {id }` ).then(r=>r.json()));
698-
699- function Profile({id}:{id:string}){
700- const { state, trigger } = useSharedFunction(profileFunction);
701- // ...same as before
702- }
703- ` ` `
704-
705- ### Pattern : always fetch fresh
706- ` ` ` tsx
707- const { state, forceTrigger } = useSharedFunction('server-time', () => fetch('/time').then(r=>r.text()));
708- const refresh = () => forceTrigger();
709- ` ` `
710-
711-
712- ## 📡 Real - time Subscriptions (` useSharedSubscription ` )
713- Perfect for Firebase listeners , WebSocket connections ,
714- Server - Sent Events , or any streaming data source that needs cleanup .
715-
716- Signature :
717- - ` const { state, trigger, unsubscribe } = useSharedSubscription(key, subscriber, scopeName?) `
718- - ` const { state, trigger, unsubscribe } = useSharedSubscription(sharedSubscriptionCreated) `
719-
720- ` state ` shape : ` { data?: T; isLoading: boolean; error?: unknown; subscribed: boolean } `
721-
722- The ` subscriber ` function receives three callbacks:
723- - `set(data )`: Update the shared data
724- - `error(error )`: Handle errors
725- - `complete()`: Mark loading as complete
726- - Returns: Optional cleanup function (called on unsubscribe /unmount )
727-
728- ### Pattern : Firebase Firestore real - time listener
729- ` ` ` tsx
730- // userSubscription.ts
731- import { onSnapshot, doc } from 'firebase/firestore';
732- import { createSharedSubscription } from 'react-shared-states';
733- import { db } from './firebase-config';
734-
735- export const userSubscription = createSharedSubscription(
736- (set, error, complete) => {
737- const userDocRef = doc(db, 'users', 'some-user-id');
738- const unsubscribe = onSnapshot(userDocRef,
739- (doc) => {
740- if (doc.exists()) {
741- set(doc.data());
742- } else {
743- error(new Error('User not found'));
744- }
745- complete();
746- },
747- (err) => {
748- error(err);
749- complete();
750- }
751- );
752- return unsubscribe;
753- }
754- );
755-
756- function UserProfile({ userId }: { userId: string }) {
757- const { state, trigger, unsubscribe } = useSharedSubscription(userSubscription);
758- // Start listening when component mounts
759- useEffect(() => {
760- trigger();
761- }, []);
762-
763- if (state.isLoading) return <div>Connecting...</div>;
764- if (state.error) return <div>Error: {state.error.message}</div>;
765- if (!state.data) return <div>User not found</div>;
766-
767- return (
768- <div>
769- <h1>{state.data.name}</h1>
770- <p>{state.data.email}</p>
771- <button onClick={unsubscribe}>Stop listening</button>
772- </div>
773- );
774- }
775- ` ` `
776-
777- ### Pattern : WebSocket connection
778- ` ` ` tsx
779- import { useEffect } from 'react';
780- import { useSharedSubscription } from 'react-shared-states';
781-
782- function ChatRoom({ roomId }: { roomId: string }) {
783- const { state, trigger } = useSharedSubscription(
784- ` chat - $ {roomId }` ,
785- (set, error, complete) => {
786- const ws = new WebSocket( ` ws :// chat-server/${roomId}`);
787-
788- ws .onopen = () => complete ();
789- ws .onmessage = (event ) => {
790- const message = JSON .parse (event .data );
791- set (prev => [... (prev || []), message ]);
792- };
793- ws .onerror = error ;
794-
795- return () => ws .close ();
796- }
797- );
798-
799- useEffect (() => {
800- trigger ();
801- }, []);
802-
803- return (
804- <div >
805- { state .isLoading && <p >Connecting to chat...</p >}
806- { state .error && <p >Connection failed</p >}
807- <div >
808- { state .data ?.map (msg => (
809- <div key = { msg .id } >{ msg .text } </div >
810- ))}
811- </div >
812- </div >
813- );
814- }
815- ```
816-
817- ### Pattern: Server-Sent Events
818- ``` tsx
819- import { useEffect } from ' react' ;
820- import { useSharedSubscription } from ' react-shared-states' ;
821-
822- function LiveUpdates() {
823- const { state, trigger } = useSharedSubscription (
824- ' live-updates' ,
825- (set , error , complete ) => {
826- const eventSource = new EventSource (' /api/live-updates' );
827-
828- eventSource .onopen = () => complete ();
829- eventSource .onmessage = (event ) => {
830- set (JSON .parse (event .data ));
831- };
832- eventSource .onerror = error ;
833-
834- return () => eventSource .close ();
835- }
836- );
837-
838- useEffect (() => {
839- trigger ();
840- }, []);
841-
842- return <div >Latest: { JSON .stringify (state .data )} </div >;
843- }
844- ```
845-
846- Subscription semantics:
847- * First ` trigger() ` establishes the subscription; subsequent calls do nothing if already subscribed.
848- * Multiple components with the same key+scope share one subscription + data stream.
849- * ` unsubscribe() ` closes the connection and clears the subscribed state.
850- * Automatic cleanup on component unmount when no other components are listening.
851- * Components mounting later instantly get the latest ` data ` without re-subscribing.
852-
853-
854- ## 🛰️ Static APIs (outside React)
855- ## 🏛️ Static/Global Shared Resource Creation
856-
857- 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:
858-
859- ``` ts
860- import { createSharedState , createSharedFunction , createSharedSubscription , useSharedState , useSharedFunction , useSharedSubscription } from ' react-shared-states' ;
861-
862- // Create and export shared resources
863- export const counterState = createSharedState (0 );
864- export const fetchUserFunction = createSharedFunction (() => fetch (' /api/me' ).then (r => r .json ()));
865- export const chatSubscription = createSharedSubscription ((set , error , complete ) => {/* ... */ });
866-
867- // Use anywhere in your app
868- const [count, setCount] = useSharedState (counterState );
869- const { state, trigger } = useSharedFunction (fetchUserFunction );
870- const { state, trigger, unsubscribe } = useSharedSubscription (chatSubscription );
871- ```
872- Useful for SSR hydration, event listeners, debugging, imperative workflows.
873-
874- ``` ts
875- import { sharedStatesApi , sharedFunctionsApi , sharedSubscriptionsApi } from ' react-shared-states' ;
876-
877- // Preload state (global scope by default)
878- sharedStatesApi .set (' bootstrap-data' , { user: {... } });
879-
880- // Preload state in a named scope
881- sharedStatesApi .set (' bootstrap-data' , { user: {... } }, ' myScope' );
882-
883- // Read later
884- const user = sharedStatesApi .get (' bootstrap-data' ); // global
885- const userScoped = sharedStatesApi .get (' bootstrap-data' , ' myScope' );
886-
887- // Inspect all (returns nested object: { [scope]: { [key]: value } })
888- console .log (sharedStatesApi .getAll ());
889-
890- // Clear all keys in a scope
891- sharedStatesApi .clearScope (' myScope' );
892-
893- // For shared functions
894- const fnState = sharedFunctionsApi .get (' profile-123' );
895- const fnStateScoped = sharedFunctionsApi .get (' profile-123' , ' myScope' );
896-
897- // For shared subscriptions
898- const subState = sharedSubscriptionsApi .get (' live-chat' );
899- const subStateScoped = sharedSubscriptionsApi .get (' live-chat' , ' myScope' );
900- ```
901-
902- ## API summary:
903-
904- | API | Methods |
905- | --------------------------| -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
906- | ` sharedStatesApi ` | ` get(key, scopeName?) ` , ` set(key, val, scopeName?) ` , ` has(key, scopeName?) ` , ` clear(key, scopeName?) ` , ` clearAll(withoutListeners?, withStatic?) ` , ` clearScope(scopeName?) ` , ` getAll() ` |
907- | ` sharedFunctionsApi ` | ` get(key, scopeName?) ` , ` set(key, val, scopeName?) ` , ` has(key, scopeName?) ` , ` clear(key, scopeName?) ` , ` clearAll(withoutListeners?, withStatic?) ` , ` clearScope(scopeName?) ` , ` getAll() ` |
908- | ` sharedSubscriptionsApi ` | ` get(key, scopeName?) ` , ` set(key, val, scopeName?) ` , ` has(key, scopeName?) ` , ` clear(key, scopeName?) ` , ` clearAll(withoutListeners?, withStatic?) ` , ` clearScope(scopeName?) ` , ` getAll() ` |
909-
910- ` scopeName ` defaults to ` "_global" ` . Internally, keys are stored as ` ${scope}//${key} ` . The ` .getAll() ` method returns a nested object: ` { [scope]: { [key]: value } } ` .
911-
912-
913- ## 🧩 Scoping Rules Deep Dive
914- Resolution order used inside hooks:
915- 1 . Explicit 3rd parameter (` scopeName ` )
916- 2 . Nearest ` SharedStatesProvider ` above the component
917- 3 . The implicit global scope (` _global ` )
918-
919- Unnamed providers auto‑generate a random scope name: each mount = isolated island.
920-
921- Two providers sharing the same ` scopeName ` act as a single logical scope even if they are disjoint in the tree (great for portals / microfrontends).
922-
923-
924- ## 🆚 Comparison Snapshot
925- | Criterion | react-shared-states | Redux Toolkit | Zustand |
926- | ----------------| ------------------------------------------| ----------------------| ----------------------------------|
927- | Setup | Install & call hook | Slice + store config | Create store function |
928- | Global state | Yes (by key) | Yes | Yes |
929- | Scoped state | Built-in (providers + names + overrides) | Needs custom logic | Needs multiple stores / contexts |
930- | Async helper | ` useSharedFunction ` (cache + status) | Thunks / RTK Query | Manual or middleware |
931- | Boilerplate | Near zero | Moderate | Low |
932- | Static access | Yes (APIs) | Yes (store) | Yes (store) |
933- | Learning curve | Minutes | Higher | Low |
934-
935-
936- ## 🧪 Testing Tips
937- * Use static APIs to assert state after component interactions.
938- * ` sharedStatesApi.clearAll(false, true) ` , ` sharedFunctionsApi.clearAll(false, true) ` , ` sharedSubscriptionsApi.clearAll(false, true) ` in ` afterEach ` to isolate tests and clear static states.
939- * For async functions: trigger once, await UI stabilization, assert ` results ` present.
940- * For subscriptions: mock the subscription source (Firebase, WebSocket, etc.) and verify data flow.
941-
942-
943- ## ❓ FAQ
944- ** Q: How do I reset a single shared state?**
945- ` sharedStatesApi.clear('key') ` . If the state was created with ` createSharedState ` , it will reset to its initial value.
946- Otherwise, it will be removed.
947-
948- ** Q: Can I pre-hydrate data on the server?**
949- Yes. Call ` sharedStatesApi.set(...) ` during bootstrap, then first client hook usage will pick it up.
950-
951- ** Q: How do I avoid accidental key collisions?**
952- Prefix keys by domain (e.g. ` user:profile ` , ` cart:items ` ) or rely on provider scoping.
953-
954- ** Q: Why is my async function not re-running?**
955- It's cached. Use ` forceTrigger() ` or ` clear() ` .
956-
957- ** Q: How do I handle subscription cleanup?**
958- Subscriptions auto-cleanup when no components are listening. You can also manually call ` unsubscribe() ` .
959-
960- ** Q: Can I use it with Suspense?**
961- Currently no built-in Suspense wrappers; wrap ` useSharedFunction ` yourself if desired.
962-
963-
964- ## 📚 Full API Reference
965- ### ` useSharedState(key, initialValue, scopeName?) `
966- Returns ` [value, setValue] ` .
967-
968- ### ` useSharedState(sharedStateCreated) `
969- Returns ` [value, setValue] ` .
970-
971- ### ` useSharedStateSelector(key, selector, scopeName?) `
972- Returns the selected value.
973-
974- ### ` useSharedStateSelector(sharedStateCreated, selector) `
975- Returns the selected value.
976-
977- ### ` useSharedFunction(key, fn, scopeName?) `
978- Returns ` { state, trigger, forceTrigger, clear } ` .
979-
980- ### ` useSharedFunction(sharedFunctionCreated) `
981- Returns ` { state, trigger, forceTrigger, clear } ` .
982-
983- ### ` useSharedSubscription(key, subscriber, scopeName?) `
984- Returns ` { state, trigger, unsubscribe } ` .
985-
986- ### ` useSharedSubscription(sharedSubscriptionCreated) `
987- Returns ` { state, trigger, unsubscribe } ` .
988-
989- ### ` <SharedStatesProvider scopeName?> `
990- Wrap children; optional ` scopeName ` (string). If omitted a random unique one is generated.
991-
992- ### Static APIs
993- ` sharedStatesApi ` , ` sharedFunctionsApi ` , ` sharedSubscriptionsApi ` (see earlier table).
994-
995-
996-
997- ## 🤝 Contributions
998-
999- We welcome contributions!
1000633If you'd like to improve ` react-shared-states ` ,
1001634feel 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 ) .
1002635
0 commit comments