react-shared-states 1.0.8 → 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 +424 -29
- package/dist/{SharedData.d.ts → SharedValuesManager.d.ts} +28 -17
- package/dist/hooks/use-shared-function.d.ts +37 -12
- package/dist/hooks/use-shared-state.d.ts +19 -3
- package/dist/hooks/use-shared-subscription.d.ts +44 -16
- package/dist/main.esm.js +372 -369
- package/dist/main.min.js +5 -5
- package/dist/types.d.ts +2 -1
- package/package.json +1 -1
- package/tests/index.test.tsx +173 -5
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._**
|
|
@@ -198,16 +197,396 @@ export default function App(){
|
|
|
198
197
|
|
|
199
198
|
|
|
200
199
|
## 🧠 Core Concepts
|
|
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.
|
|
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. |
|
|
210
|
+
|
|
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.
|
|
271
|
+
|
|
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()));
|
|
276
|
+
|
|
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.
|
|
293
|
+
|
|
294
|
+
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)
|
|
305
|
+
|
|
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';
|
|
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
|
+
);
|
|
333
|
+
|
|
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
|
+
```
|
|
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');
|
|
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
|
+
);
|
|
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
|
+
|
|
211
590
|
|
|
212
591
|
|
|
213
592
|
## 🏗️ Sharing State (`useSharedState`)
|
|
@@ -298,8 +677,8 @@ Signature:
|
|
|
298
677
|
|
|
299
678
|
The `subscriber` function receives three callbacks:
|
|
300
679
|
- `set(data)`: Update the shared data
|
|
301
|
-
- `
|
|
302
|
-
- `
|
|
680
|
+
- `error(error)`: Handle errors
|
|
681
|
+
- `complete()`: Mark loading as complete
|
|
303
682
|
- Returns: Optional cleanup function (called on unsubscribe/unmount)
|
|
304
683
|
|
|
305
684
|
### Pattern: Firebase Firestore real-time listener
|
|
@@ -310,8 +689,23 @@ import { createSharedSubscription } from 'react-shared-states';
|
|
|
310
689
|
import { db } from './firebase-config';
|
|
311
690
|
|
|
312
691
|
export const userSubscription = createSharedSubscription(
|
|
313
|
-
|
|
314
|
-
|
|
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;
|
|
315
709
|
}
|
|
316
710
|
);
|
|
317
711
|
|
|
@@ -344,15 +738,15 @@ import { useSharedSubscription } from 'react-shared-states';
|
|
|
344
738
|
function ChatRoom({ roomId }: { roomId: string }) {
|
|
345
739
|
const { state, trigger } = useSharedSubscription(
|
|
346
740
|
`chat-${roomId}`,
|
|
347
|
-
(set,
|
|
741
|
+
(set, error, complete) => {
|
|
348
742
|
const ws = new WebSocket(`ws://chat-server/${roomId}`);
|
|
349
743
|
|
|
350
|
-
ws.onopen = () =>
|
|
744
|
+
ws.onopen = () => complete();
|
|
351
745
|
ws.onmessage = (event) => {
|
|
352
746
|
const message = JSON.parse(event.data);
|
|
353
747
|
set(prev => [...(prev || []), message]);
|
|
354
748
|
};
|
|
355
|
-
ws.onerror =
|
|
749
|
+
ws.onerror = error;
|
|
356
750
|
|
|
357
751
|
return () => ws.close();
|
|
358
752
|
}
|
|
@@ -384,14 +778,14 @@ import { useSharedSubscription } from 'react-shared-states';
|
|
|
384
778
|
function LiveUpdates() {
|
|
385
779
|
const { state, trigger } = useSharedSubscription(
|
|
386
780
|
'live-updates',
|
|
387
|
-
(set,
|
|
781
|
+
(set, error, complete) => {
|
|
388
782
|
const eventSource = new EventSource('/api/live-updates');
|
|
389
783
|
|
|
390
|
-
eventSource.onopen = () =>
|
|
784
|
+
eventSource.onopen = () => complete();
|
|
391
785
|
eventSource.onmessage = (event) => {
|
|
392
786
|
set(JSON.parse(event.data));
|
|
393
787
|
};
|
|
394
|
-
eventSource.onerror =
|
|
788
|
+
eventSource.onerror = error;
|
|
395
789
|
|
|
396
790
|
return () => eventSource.close();
|
|
397
791
|
}
|
|
@@ -424,7 +818,7 @@ import { createSharedState, createSharedFunction, createSharedSubscription, useS
|
|
|
424
818
|
// Create and export shared resources
|
|
425
819
|
export const counterState = createSharedState(0);
|
|
426
820
|
export const fetchUserFunction = createSharedFunction(() => fetch('/api/me').then(r => r.json()));
|
|
427
|
-
export const chatSubscription = createSharedSubscription((set,
|
|
821
|
+
export const chatSubscription = createSharedSubscription((set, error, complete) => {/* ... */});
|
|
428
822
|
|
|
429
823
|
// Use anywhere in your app
|
|
430
824
|
const [count, setCount] = useSharedState(counterState);
|
|
@@ -463,11 +857,11 @@ const subStateScoped = sharedSubscriptionsApi.get('live-chat', 'myScope');
|
|
|
463
857
|
|
|
464
858
|
## API summary:
|
|
465
859
|
|
|
466
|
-
| API | Methods
|
|
467
|
-
|
|
468
|
-
| `sharedStatesApi` | `get(key, scopeName?)`, `set(key, val, scopeName?)`, `has(key, scopeName?)`, `clear(key, scopeName?)`, `clearAll()`, `clearScope(scopeName?)`, `getAll()` |
|
|
469
|
-
| `sharedFunctionsApi` | `get(key, scopeName?)`, `set(key, val, scopeName?)`, `has(key, scopeName?)`, `clear(key, scopeName?)`, `clearAll()`, `clearScope(scopeName?)`, `getAll()` |
|
|
470
|
-
| `sharedSubscriptionsApi` | `get(key, scopeName?)`, `set(key, val, scopeName?)`, `has(key, scopeName?)`, `clear(key, scopeName?)`, `clearAll()`, `clearScope(scopeName?)`, `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()` |
|
|
471
865
|
|
|
472
866
|
`scopeName` defaults to `"_global"`. Internally, keys are stored as `${scope}//${key}`. The `.getAll()` method returns a nested object: `{ [scope]: { [key]: value } }`.
|
|
473
867
|
|
|
@@ -497,14 +891,15 @@ Two providers sharing the same `scopeName` act as a single logical scope even if
|
|
|
497
891
|
|
|
498
892
|
## 🧪 Testing Tips
|
|
499
893
|
* Use static APIs to assert state after component interactions.
|
|
500
|
-
* `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.
|
|
501
895
|
* For async functions: trigger once, await UI stabilization, assert `results` present.
|
|
502
896
|
* For subscriptions: mock the subscription source (Firebase, WebSocket, etc.) and verify data flow.
|
|
503
897
|
|
|
504
898
|
|
|
505
899
|
## ❓ FAQ
|
|
506
900
|
**Q: How do I reset a single shared state?**
|
|
507
|
-
`sharedStatesApi.clear('key')
|
|
901
|
+
`sharedStatesApi.clear('key')`. If the state was created with `createSharedState`, it will reset to its initial value.
|
|
902
|
+
Otherwise, it will be removed.
|
|
508
903
|
|
|
509
904
|
**Q: Can I pre-hydrate data on the server?**
|
|
510
905
|
Yes. Call `sharedStatesApi.set(...)` during bootstrap, then first client hook usage will pick it up.
|
|
@@ -1,37 +1,44 @@
|
|
|
1
|
-
import { AFunction,
|
|
2
|
-
|
|
3
|
-
export declare abstract class
|
|
4
|
-
data: Map<string,
|
|
5
|
-
defaultValue():
|
|
1
|
+
import { AFunction, Prefix, SharedCreated, SharedValue } from './types';
|
|
2
|
+
export declare const staticStores: SharedCreated[];
|
|
3
|
+
export declare abstract class SharedValuesManager<T extends SharedValue, V> {
|
|
4
|
+
data: Map<string, T>;
|
|
5
|
+
defaultValue(): V;
|
|
6
6
|
addListener(key: string, prefix: Prefix, listener: AFunction): void;
|
|
7
7
|
removeListener(key: string, prefix: Prefix, listener: AFunction): void;
|
|
8
8
|
callListeners(key: string, prefix: Prefix): void;
|
|
9
|
-
init(key: string, prefix: Prefix, data:
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
9
|
+
init(key: string, prefix: Prefix, data: V, isStatic?: boolean): void;
|
|
10
|
+
createStatic<X extends SharedCreated>(rest: Omit<X, 'key' | 'prefix'>, scopeName?: Prefix): {
|
|
11
|
+
key: string;
|
|
12
|
+
prefix: Prefix;
|
|
13
|
+
} & Omit<X, "key" | "prefix">;
|
|
14
|
+
initStatic(sharedCreated: SharedCreated): void;
|
|
15
|
+
clearAll(withoutListeners?: boolean, withStatic?: boolean): void;
|
|
16
|
+
clear(key: string, prefix: Prefix, withoutListeners?: boolean, withStatic?: boolean): void;
|
|
17
|
+
get(key: string, prefix: Prefix): T | undefined;
|
|
18
|
+
setValue(key: string, prefix: Prefix, data: V): void;
|
|
14
19
|
has(key: string, prefix: Prefix): string | undefined;
|
|
15
20
|
static prefix(key: string, prefix: Prefix): string;
|
|
16
21
|
static extractPrefix(mapKey: string): string[];
|
|
17
22
|
useEffect(key: string, prefix: Prefix, unsub?: (() => void) | null): void;
|
|
18
23
|
}
|
|
19
|
-
export declare class
|
|
20
|
-
|
|
21
|
-
constructor(sharedData:
|
|
24
|
+
export declare class SharedValuesApi<T extends SharedValue, V, R = T> {
|
|
25
|
+
protected sharedData: SharedValuesManager<T, V>;
|
|
26
|
+
constructor(sharedData: SharedValuesManager<T, V>);
|
|
22
27
|
/**
|
|
23
28
|
* get a value from the shared data
|
|
24
29
|
* @param key
|
|
25
30
|
* @param scopeName
|
|
26
31
|
*/
|
|
27
|
-
get<S extends string = string>(key: S, scopeName: Prefix):
|
|
32
|
+
get<S extends string = string>(key: S, scopeName: Prefix): R;
|
|
33
|
+
get<S extends string = string>(sharedCreated: SharedCreated): R;
|
|
28
34
|
/**
|
|
29
35
|
* set a value in the shared data
|
|
30
36
|
* @param key
|
|
31
37
|
* @param value
|
|
32
38
|
* @param scopeName
|
|
33
39
|
*/
|
|
34
|
-
set<S extends string = string>(key: S, value:
|
|
40
|
+
set<S extends string = string>(key: S, value: V, scopeName: Prefix): void;
|
|
41
|
+
set<S extends string = string>(sharedCreated: SharedCreated, value: V): void;
|
|
35
42
|
/**
|
|
36
43
|
* clear all values from the shared data
|
|
37
44
|
*/
|
|
@@ -45,7 +52,12 @@ export declare class SharedApi<T> {
|
|
|
45
52
|
* resolve a shared created object to a value
|
|
46
53
|
* @param sharedCreated
|
|
47
54
|
*/
|
|
48
|
-
resolve(sharedCreated: SharedCreated):
|
|
55
|
+
resolve(sharedCreated: SharedCreated): R;
|
|
56
|
+
/**
|
|
57
|
+
* clear a value from the shared data
|
|
58
|
+
* @param key
|
|
59
|
+
* @param scopeName
|
|
60
|
+
*/
|
|
49
61
|
clear(key: string, scopeName: Prefix): void;
|
|
50
62
|
clear(sharedCreated: SharedCreated): void;
|
|
51
63
|
/**
|
|
@@ -59,4 +71,3 @@ export declare class SharedApi<T> {
|
|
|
59
71
|
*/
|
|
60
72
|
getAll(): Record<string, Record<string, any>>;
|
|
61
73
|
}
|
|
62
|
-
export {};
|
|
@@ -1,15 +1,40 @@
|
|
|
1
|
-
import { AFunction, Prefix, SharedCreated } from '../types';
|
|
2
|
-
import {
|
|
3
|
-
type
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
error?: unknown;
|
|
8
|
-
};
|
|
1
|
+
import { AFunction, Prefix, SharedCreated, SharedValue } from '../types';
|
|
2
|
+
import { SharedValuesApi, SharedValuesManager } from '../SharedValuesManager';
|
|
3
|
+
type SharedFunctionValue<T> = {
|
|
4
|
+
results?: T;
|
|
5
|
+
isLoading: boolean;
|
|
6
|
+
error?: unknown;
|
|
9
7
|
};
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
8
|
+
interface SharedFunction<T> extends SharedValue {
|
|
9
|
+
fnState: SharedFunctionValue<T>;
|
|
10
|
+
}
|
|
11
|
+
declare class SharedFunctionsManager extends SharedValuesManager<SharedFunction<unknown>, {
|
|
12
|
+
fnState: SharedFunctionValue<unknown>;
|
|
13
|
+
}> {
|
|
14
|
+
defaultValue(): {
|
|
15
|
+
fnState: {
|
|
16
|
+
results: undefined;
|
|
17
|
+
isLoading: boolean;
|
|
18
|
+
error: undefined;
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
initValue(key: string, prefix: Prefix, isStatic?: boolean): void;
|
|
22
|
+
setValue<T>(key: string, prefix: Prefix, data: {
|
|
23
|
+
fnState: SharedFunctionValue<T>;
|
|
24
|
+
}): void;
|
|
25
|
+
}
|
|
26
|
+
export declare class SharedFunctionsApi extends SharedValuesApi<SharedFunction<unknown>, {
|
|
27
|
+
fnState: SharedFunctionValue<unknown>;
|
|
28
|
+
}, SharedFunctionValue<unknown>> {
|
|
29
|
+
constructor(sharedFunctionManager: SharedFunctionsManager);
|
|
30
|
+
get<T, S extends string = string>(key: S, scopeName?: Prefix): SharedFunctionValue<T>;
|
|
31
|
+
get<T, Args extends unknown[]>(sharedFunctionCreated: SharedFunctionCreated<T, Args>): SharedFunctionValue<T>;
|
|
32
|
+
set<T, S extends string = string>(key: S, value: {
|
|
33
|
+
fnState: SharedFunctionValue<T>;
|
|
34
|
+
}, scopeName?: Prefix): void;
|
|
35
|
+
set<T, Args extends unknown[]>(sharedFunctionCreated: SharedFunctionCreated<T, Args>, value: {
|
|
36
|
+
fnState: SharedFunctionValue<T>;
|
|
37
|
+
}): void;
|
|
13
38
|
}
|
|
14
39
|
export declare const sharedFunctionsApi: SharedFunctionsApi;
|
|
15
40
|
interface SharedFunctionCreated<T, Args extends unknown[]> extends SharedCreated {
|
|
@@ -17,7 +42,7 @@ interface SharedFunctionCreated<T, Args extends unknown[]> extends SharedCreated
|
|
|
17
42
|
}
|
|
18
43
|
export declare const createSharedFunction: <T, Args extends unknown[]>(fn: AFunction<T, Args>, scopeName?: Prefix) => SharedFunctionCreated<T, Args>;
|
|
19
44
|
export type SharedFunctionStateReturn<T, Args extends unknown[]> = {
|
|
20
|
-
readonly state: NonNullable<
|
|
45
|
+
readonly state: NonNullable<SharedFunctionValue<T>>;
|
|
21
46
|
readonly trigger: (...args: Args) => void;
|
|
22
47
|
readonly forceTrigger: (...args: Args) => void;
|
|
23
48
|
readonly clear: () => void;
|