Skip to content

Commit 7917530

Browse files
Expand README with reusable shared resource examples
- Added documentation for `createSharedState`, `createSharedFunction`, and `createSharedSubscription` with examples to encourage best practices in large apps. - Updated examples to demonstrate explicit scope naming for shared resources, avoiding collisions and enhancing type safety. - Enhanced API reference with detailed usage for static/shared resource creation and integration.
1 parent 5d5ea5f commit 7917530

File tree

1 file changed

+99
-54
lines changed

1 file changed

+99
-54
lines changed

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

0 commit comments

Comments
 (0)