Skip to content

Commit f0acd50

Browse files
Merge pull request #8 from HichemTab-tech/add-static-states-creation
Add reusable shared resource factory functions
2 parents af2f342 + 7917530 commit f0acd50

File tree

12 files changed

+324
-126
lines changed

12 files changed

+324
-126
lines changed

.editorconfig

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,7 @@ indent_size = 4
99
indent_size = 2
1010

1111
[*.yml]
12+
indent_size = 2
13+
14+
[*.json]
1215
indent_size = 2

README.md

Lines changed: 99 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,6 @@ function B(){
4848
}
4949

5050
function App() {
51-
5251
return (
5352
<>
5453
<A/>
@@ -69,7 +68,6 @@ function Scoped(){
6968
}
7069

7170
function App() {
72-
7371
return (
7472
<>
7573
<A/>
@@ -82,6 +80,33 @@ function App() {
8280
}
8381
```
8482

83+
---
84+
85+
> **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.
86+
87+
```tsx
88+
import { useSharedState } from 'react-shared-states';
89+
export const sharedCounter = createSharedState(0);
90+
91+
function A(){
92+
const [count, setCount] = useSharedState(sharedCounter);
93+
return <button onClick={()=>setCount(c=>c+1)}>A {count}</button>;
94+
}
95+
function B(){
96+
const [count] = useSharedState(sharedCounter);
97+
return <span>B sees {count}</span>;
98+
}
99+
100+
function App() {
101+
return (
102+
<>
103+
<A/>
104+
<B/>
105+
</>
106+
)
107+
}
108+
```
109+
85110
Override / jump to a named scope explicitly:
86111
```tsx
87112
useSharedState('counter', 0, 'modal'); // 3rd arg is scopeName override
@@ -173,19 +198,22 @@ export default function App(){
173198

174199

175200
## 🧠 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`). |
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. |
185211

186212

187213
## 🏗️ Sharing State (`useSharedState`)
188-
Signature: `const [value, setValue] = useSharedState(key, initialValue, scopeName?);`
214+
Signature:
215+
- `const [value, setValue] = useSharedState(key, initialValue, scopeName?)`
216+
- `const [value, setValue] = useSharedState(sharedStateCreated)`
189217

190218
Behavior:
191219
* First hook call (per key + scope) seeds with `initialValue`.
@@ -194,35 +222,44 @@ Behavior:
194222
* React batching + equality check: listeners fire only when the value reference actually changes.
195223

196224
### Examples
197-
1. Global theme
225+
1. Global theme (recommended for large apps)
198226
```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);
200231
```
201232
2. Isolated wizard progress
202233
```tsx
234+
const wizardProgress = createSharedState(0);
203235
<SharedStatesProvider>
204236
<Wizard/>
205237
</SharedStatesProvider>
238+
// In Wizard
239+
const [step, setStep] = useSharedState(wizardProgress);
206240
```
207241
3. Forcing crossportal sync
208242
```tsx
243+
const navState = createSharedState('closed', 'nav');
209244
<SharedStatesProvider scopeName="nav" children={<PrimaryNav/>} />
210245
<Portal>
211246
<SharedStatesProvider scopeName="nav" children={<MobileNav/>} />
212247
</Portal>
248+
// In both navs
249+
const [navOpen, setNavOpen] = useSharedState(navState);
213250
```
214251
4. Overriding nearest provider
215252
```tsx
216253
// 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);
218256
```
219257

220258

221259
## ⚡ Shared Async Functions (`useSharedFunction`)
222260
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)`
226263
`state` shape: `{ results?: T; isLoading: boolean; error?: unknown }`
227264

228265
Semantics:
@@ -233,12 +270,12 @@ Semantics:
233270

234271
### Pattern: lazy load on first render
235272
```tsx
273+
// profileFunction.ts
274+
export const profileFunction = createSharedFunction((id: string) => fetch(`/api/p/${id}`).then(r=>r.json()));
275+
236276
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>
277+
const { state, trigger } = useSharedFunction(profileFunction);
278+
// ...same as before
242279
}
243280
```
244281

@@ -254,9 +291,8 @@ Perfect for Firebase listeners, WebSocket connections,
254291
Server-Sent Events, or any streaming data source that needs cleanup.
255292

256293
Signature:
257-
```ts
258-
const { state, trigger, unsubscribe } = useSharedSubscription(key, subscriber, scopeName?);
259-
```
294+
- `const { state, trigger, unsubscribe } = useSharedSubscription(key, subscriber, scopeName?)`
295+
- `const { state, trigger, unsubscribe } = useSharedSubscription(sharedSubscriptionCreated)`
260296

261297
`state` shape: `{ data?: T; isLoading: boolean; error?: unknown; subscribed: boolean }`
262298

@@ -268,36 +304,19 @@ The `subscriber` function receives three callbacks:
268304

269305
### Pattern: Firebase Firestore real-time listener
270306
```tsx
271-
import { useEffect } from 'react';
307+
// userSubscription.ts
272308
import { onSnapshot, doc } from 'firebase/firestore';
273-
import { useSharedSubscription } from 'react-shared-states';
274-
import { db } from './firebase-config'; // your Firebase config
309+
import { createSharedSubscription } from 'react-shared-states';
310+
import { db } from './firebase-config';
275311
276-
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);
281-
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-
);
295-
296-
// Return cleanup function
297-
return unsubscribe;
298-
}
299-
);
312+
export const userSubscription = createSharedSubscription(
313+
async (set, onError, onCompletion) => {
314+
// ...same as before
315+
}
316+
);
300317
318+
function UserProfile({ userId }: { userId: string }) {
319+
const { state, trigger, unsubscribe } = useSharedSubscription(userSubscription);
301320
// Start listening when component mounts
302321
useEffect(() => {
303322
trigger();
@@ -395,6 +414,23 @@ Subscription semantics:
395414

396415

397416
## 🛰️ Static APIs (outside React)
417+
## 🏛️ Static/Global Shared Resource Creation
418+
419+
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:
420+
421+
```ts
422+
import { createSharedState, createSharedFunction, createSharedSubscription, useSharedState, useSharedFunction, useSharedSubscription } from 'react-shared-states';
423+
424+
// Create and export shared resources
425+
export const counterState = createSharedState(0);
426+
export const fetchUserFunction = createSharedFunction(() => fetch('/api/me').then(r => r.json()));
427+
export const chatSubscription = createSharedSubscription((set, onError, onCompletion) => {/* ... */});
428+
429+
// Use anywhere in your app
430+
const [count, setCount] = useSharedState(counterState);
431+
const { state, trigger } = useSharedFunction(fetchUserFunction);
432+
const { state, trigger, unsubscribe } = useSharedSubscription(chatSubscription);
433+
```
398434
Useful for SSR hydration, event listeners, debugging, imperative workflows.
399435

400436
```ts
@@ -490,16 +526,25 @@ Currently no built-in Suspense wrappers; wrap `useSharedFunction` yourself if de
490526
### `useSharedState(key, initialValue, scopeName?)`
491527
Returns `[value, setValue]`.
492528

529+
### `useSharedState(sharedStateCreated)`
530+
Returns `[value, setValue]`.
531+
493532
### `useSharedFunction(key, fn, scopeName?)`
494533
Returns `{ state, trigger, forceTrigger, clear }`.
495534

535+
### `useSharedFunction(sharedFunctionCreated)`
536+
Returns `{ state, trigger, forceTrigger, clear }`.
537+
496538
### `useSharedSubscription(key, subscriber, scopeName?)`
497539
Returns `{ state, trigger, unsubscribe }`.
498540

541+
### `useSharedSubscription(sharedSubscriptionCreated)`
542+
Returns `{ state, trigger, unsubscribe }`.
543+
499544
### `<SharedStatesProvider scopeName?>`
500545
Wrap children; optional `scopeName` (string). If omitted a random unique one is generated.
501546

502-
### Static
547+
### Static APIs
503548
`sharedStatesApi`, `sharedFunctionsApi`, `sharedSubscriptionsApi` (see earlier table).
504549

505550

demo/app.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
import './FakeSharedEmitter';
1111
import {FakeSharedEmitter} from "./FakeSharedEmitter";
1212
import {useEffect, useState} from "react";
13+
import {createSharedState} from "react-shared-states";
1314

1415
FakeSharedEmitter.intervalDuration = 3000;
1516
window.sharedSubscriptionsApi = sharedSubscriptionsApi;
@@ -18,8 +19,12 @@ window.sharedStatesApi = sharedStatesApi;
1819

1920
sharedStatesApi.set("x", 55);
2021

22+
const counterGlobal = createSharedState(0);
23+
2124
const Comp1 = () => {
22-
const [x, setX] = useSharedState('x', 0);
25+
//const [x, setX] = useSharedState('x', 0);
26+
//const [x, setX] = useSharedState(counterGlobal);
27+
const [x, setX] = useSharedState("counterGlobal", "");
2328
const handle = (by = 1) => {
2429
setX(x+by)
2530
}
@@ -77,7 +82,8 @@ const Comp2 = () => {
7782
const App = () => {
7883

7984
const [hide, setHide] = useState(false);
80-
const [x, setX] = useSharedState('x', 0);
85+
const [x, setX] = useSharedState(counterGlobal);
86+
//const [x, setX] = useSharedState('x', 0);
8187
//const [x, setX] = useState(0);
8288
const handle = () => {
8389
setX(x+1)

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,9 @@
1616
"module": "dist/main.esm.js",
1717
"types": "dist/index.d.ts",
1818
"scripts": {
19+
"tsc": "tsc",
1920
"bump": "node scripts/bumper.js",
20-
"build": "vite build",
21+
"build": "tsc && vite build",
2122
"test": "vitest",
2223
"preview": "vite preview",
2324
"dev:demo": "vite --config demo/vite.config.demo.ts",

src/SharedData.ts

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type {AFunction, DataMapValue, Prefix} from "./types";
1+
import type {AFunction, DataMapValue, Prefix, SharedCreated} from "./types";
22
import {useEffect} from "react";
33
import {ensureNonEmptyString, log} from "./lib/utils";
44

@@ -149,14 +149,34 @@ export class SharedApi<T>{
149149
});
150150
}
151151

152+
/**
153+
* resolve a shared created object to a value
154+
* @param sharedCreated
155+
*/
156+
resolve(sharedCreated: SharedCreated) {
157+
const {key, prefix} = sharedCreated;
158+
return this.get(key, prefix);
159+
}
160+
161+
clear(key: string, scopeName: Prefix): void;
162+
clear(sharedCreated: SharedCreated): void;
152163
/**
153164
* clear a value from the shared data
154165
* @param key
155166
* @param scopeName
156167
*/
157-
clear(key: string, scopeName: Prefix) {
158-
const prefix: Prefix = scopeName || "_global";
159-
this.sharedData.clear(key, prefix);
168+
clear(key: string|SharedCreated, scopeName?: Prefix) {
169+
let keyStr!: string;
170+
let prefixStr!: string;
171+
if (typeof key === "string") {
172+
keyStr = key;
173+
prefixStr = scopeName || "_global";
174+
}
175+
else{
176+
keyStr = key.key;
177+
prefixStr = key.prefix;
178+
}
179+
this.sharedData.clear(keyStr, prefixStr);
160180
}
161181

162182
/**

src/context/SharedStatesContext.tsx

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,21 @@
11
import {createContext, type PropsWithChildren, useContext, useMemo} from 'react';
2-
import type {NonEmptyString} from "../types";
2+
import type {Prefix} from "../types";
3+
import {random} from "../lib/utils";
34

45
export interface SharedStatesType {
56
scopeName: string
67
}
78

8-
const SharedStatesContext = createContext<SharedStatesType | undefined>(undefined);
9+
export const SharedStatesContext = createContext<SharedStatesType | undefined>(undefined);
910

10-
interface SharedStatesProviderProps<T extends string = string> extends PropsWithChildren {
11-
scopeName?: '__global' extends NonEmptyString<T> ? never : NonEmptyString<T>;
11+
interface SharedStatesProviderProps extends PropsWithChildren {
12+
scopeName?: Prefix;
1213
}
1314

14-
export const SharedStatesProvider = <T extends string = string>({ children, scopeName }: SharedStatesProviderProps<T>) => {
15+
export const SharedStatesProvider = ({ children, scopeName }: SharedStatesProviderProps) => {
1516
if (scopeName && scopeName.includes("//")) throw new Error("scopeName cannot contain '//'");
1617

17-
if (!scopeName) scopeName = useMemo(() => Math.random().toString(36).substring(2, 15) as NonNullable<SharedStatesProviderProps<T>['scopeName']>, []);
18+
if (!scopeName) scopeName = useMemo(() => random() as NonNullable<SharedStatesProviderProps['scopeName']>, []);
1819

1920
return (
2021
<SharedStatesContext.Provider value={{scopeName}}>

0 commit comments

Comments
 (0)