react-shared-states 1.0.3 → 1.0.5
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 +174 -17
- package/dist/SharedData.d.ts +4 -3
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/use-shared-function.d.ts +4 -4
- package/dist/hooks/use-shared-state.d.ts +4 -4
- package/dist/hooks/use-shared-subscription.d.ts +39 -0
- package/dist/lib/utils.d.ts +3 -0
- package/dist/main.esm.js +397 -268
- package/dist/main.min.js +5 -5
- package/dist/types.d.ts +2 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
[](LICENSE)
|
|
13
13
|
|
|
14
14
|
|
|
15
|
-
Tiny, ergonomic, convention‑over‑configuration state
|
|
15
|
+
Tiny, ergonomic, convention‑over‑configuration state, async function, and real-time subscription sharing for React. Global by default, trivially scoped when you need isolation, and opt‑in static APIs when you must touch state outside components. As simple as `useState`, as flexible as Zustand, without boilerplate like Redux.
|
|
16
16
|
|
|
17
17
|
## 🔥 Why this instead of Redux / Zustand / Context soup?
|
|
18
18
|
* 0 config. Just pick a key: `useSharedState('cart', [])`.
|
|
@@ -173,14 +173,15 @@ export default function App(){
|
|
|
173
173
|
|
|
174
174
|
|
|
175
175
|
## 🧠 Core Concepts
|
|
176
|
-
| Concept
|
|
177
|
-
|
|
178
|
-
| Global by default
|
|
179
|
-
| Scoping
|
|
180
|
-
| Named scopes
|
|
181
|
-
| Manual override
|
|
182
|
-
| Shared functions
|
|
183
|
-
|
|
|
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`). |
|
|
184
185
|
|
|
185
186
|
|
|
186
187
|
## 🏗️ Sharing State (`useSharedState`)
|
|
@@ -248,13 +249,158 @@ const refresh = () => forceTrigger();
|
|
|
248
249
|
```
|
|
249
250
|
|
|
250
251
|
|
|
252
|
+
## 📡 Real-time Subscriptions (`useSharedSubscription`)
|
|
253
|
+
Perfect for Firebase listeners, WebSocket connections,
|
|
254
|
+
Server-Sent Events, or any streaming data source that needs cleanup.
|
|
255
|
+
|
|
256
|
+
Signature:
|
|
257
|
+
```ts
|
|
258
|
+
const { state, trigger, unsubscribe } = useSharedSubscription(key, subscriber, scopeName?);
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
`state` shape: `{ data?: T; isLoading: boolean; error?: unknown; subscribed: boolean }`
|
|
262
|
+
|
|
263
|
+
The `subscriber` function receives three callbacks:
|
|
264
|
+
- `set(data)`: Update the shared data
|
|
265
|
+
- `onError(error)`: Handle errors
|
|
266
|
+
- `onCompletion()`: Mark loading as complete
|
|
267
|
+
- Returns: Optional cleanup function (called on unsubscribe/unmount)
|
|
268
|
+
|
|
269
|
+
### Pattern: Firebase Firestore real-time listener
|
|
270
|
+
```tsx
|
|
271
|
+
import { useEffect } from 'react';
|
|
272
|
+
import { onSnapshot, doc } from 'firebase/firestore';
|
|
273
|
+
import { useSharedSubscription } from 'react-shared-states';
|
|
274
|
+
import { db } from './firebase-config'; // your Firebase config
|
|
275
|
+
|
|
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
|
+
);
|
|
300
|
+
|
|
301
|
+
// Start listening when component mounts
|
|
302
|
+
useEffect(() => {
|
|
303
|
+
trigger();
|
|
304
|
+
}, []);
|
|
305
|
+
|
|
306
|
+
if (state.isLoading) return <div>Connecting...</div>;
|
|
307
|
+
if (state.error) return <div>Error: {state.error.message}</div>;
|
|
308
|
+
if (!state.data) return <div>User not found</div>;
|
|
309
|
+
|
|
310
|
+
return (
|
|
311
|
+
<div>
|
|
312
|
+
<h1>{state.data.name}</h1>
|
|
313
|
+
<p>{state.data.email}</p>
|
|
314
|
+
<button onClick={unsubscribe}>Stop listening</button>
|
|
315
|
+
</div>
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
### Pattern: WebSocket connection
|
|
321
|
+
```tsx
|
|
322
|
+
import { useEffect } from 'react';
|
|
323
|
+
import { useSharedSubscription } from 'react-shared-states';
|
|
324
|
+
|
|
325
|
+
function ChatRoom({ roomId }: { roomId: string }) {
|
|
326
|
+
const { state, trigger } = useSharedSubscription(
|
|
327
|
+
`chat-${roomId}`,
|
|
328
|
+
(set, onError, onCompletion) => {
|
|
329
|
+
const ws = new WebSocket(`ws://chat-server/${roomId}`);
|
|
330
|
+
|
|
331
|
+
ws.onopen = () => onCompletion();
|
|
332
|
+
ws.onmessage = (event) => {
|
|
333
|
+
const message = JSON.parse(event.data);
|
|
334
|
+
set(prev => [...(prev || []), message]);
|
|
335
|
+
};
|
|
336
|
+
ws.onerror = onError;
|
|
337
|
+
|
|
338
|
+
return () => ws.close();
|
|
339
|
+
}
|
|
340
|
+
);
|
|
341
|
+
|
|
342
|
+
useEffect(() => {
|
|
343
|
+
trigger();
|
|
344
|
+
}, []);
|
|
345
|
+
|
|
346
|
+
return (
|
|
347
|
+
<div>
|
|
348
|
+
{state.isLoading && <p>Connecting to chat...</p>}
|
|
349
|
+
{state.error && <p>Connection failed</p>}
|
|
350
|
+
<div>
|
|
351
|
+
{state.data?.map(msg => (
|
|
352
|
+
<div key={msg.id}>{msg.text}</div>
|
|
353
|
+
))}
|
|
354
|
+
</div>
|
|
355
|
+
</div>
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
### Pattern: Server-Sent Events
|
|
361
|
+
```tsx
|
|
362
|
+
import { useEffect } from 'react';
|
|
363
|
+
import { useSharedSubscription } from 'react-shared-states';
|
|
364
|
+
|
|
365
|
+
function LiveUpdates() {
|
|
366
|
+
const { state, trigger } = useSharedSubscription(
|
|
367
|
+
'live-updates',
|
|
368
|
+
(set, onError, onCompletion) => {
|
|
369
|
+
const eventSource = new EventSource('/api/live-updates');
|
|
370
|
+
|
|
371
|
+
eventSource.onopen = () => onCompletion();
|
|
372
|
+
eventSource.onmessage = (event) => {
|
|
373
|
+
set(JSON.parse(event.data));
|
|
374
|
+
};
|
|
375
|
+
eventSource.onerror = onError;
|
|
376
|
+
|
|
377
|
+
return () => eventSource.close();
|
|
378
|
+
}
|
|
379
|
+
);
|
|
380
|
+
|
|
381
|
+
useEffect(() => {
|
|
382
|
+
trigger();
|
|
383
|
+
}, []);
|
|
384
|
+
|
|
385
|
+
return <div>Latest: {JSON.stringify(state.data)}</div>;
|
|
386
|
+
}
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
Subscription semantics:
|
|
390
|
+
* First `trigger()` establishes the subscription; subsequent calls do nothing if already subscribed.
|
|
391
|
+
* Multiple components with the same key+scope share one subscription + data stream.
|
|
392
|
+
* `unsubscribe()` closes the connection and clears the subscribed state.
|
|
393
|
+
* Automatic cleanup on component unmount when no other components are listening.
|
|
394
|
+
* Components mounting later instantly get the latest `data` without re-subscribing.
|
|
395
|
+
|
|
396
|
+
|
|
251
397
|
## 🛰️ Static APIs (outside React)
|
|
252
398
|
Useful for SSR hydration, event listeners, debugging, imperative workflows.
|
|
253
399
|
|
|
254
400
|
```ts
|
|
255
|
-
import { sharedStatesApi, sharedFunctionsApi } from 'react-shared-states';
|
|
401
|
+
import { sharedStatesApi, sharedFunctionsApi, sharedSubscriptionsApi } from 'react-shared-states';
|
|
256
402
|
|
|
257
|
-
// Preload
|
|
403
|
+
// Preload state
|
|
258
404
|
sharedStatesApi.set('bootstrap-data', { user: {...} });
|
|
259
405
|
|
|
260
406
|
// Read later
|
|
@@ -265,14 +411,18 @@ console.log(sharedStatesApi.getAll()); // Map with prefixed keys
|
|
|
265
411
|
|
|
266
412
|
// For shared functions
|
|
267
413
|
const fnState = sharedFunctionsApi.get('profile-123');
|
|
414
|
+
|
|
415
|
+
// For shared subscriptions
|
|
416
|
+
const subState = sharedSubscriptionsApi.get('live-chat');
|
|
268
417
|
```
|
|
269
418
|
|
|
270
419
|
## API summary:
|
|
271
420
|
|
|
272
|
-
| API
|
|
273
|
-
|
|
274
|
-
| `sharedStatesApi`
|
|
275
|
-
| `sharedFunctionsApi`
|
|
421
|
+
| API | Methods |
|
|
422
|
+
|--------------------------|---------------------------------------------------------------------------------------|
|
|
423
|
+
| `sharedStatesApi` | `get(key, scope?)`, `set(key,val,scope?)`, `has`, `clear`, `clearAll`, `getAll()` |
|
|
424
|
+
| `sharedFunctionsApi` | `get(key, scope?)` (returns fn state), `set`, `has`, `clear`, `clearAll`, `getAll()` |
|
|
425
|
+
| `sharedSubscriptionsApi` | `get(key, scope?)` (returns sub state), `set`, `has`, `clear`, `clearAll`, `getAll()` |
|
|
276
426
|
|
|
277
427
|
`scope` defaults to `"_global"`. Internally keys are stored as `${scope}_${key}`.
|
|
278
428
|
|
|
@@ -302,8 +452,9 @@ Two providers sharing the same `scopeName` act as a single logical scope even if
|
|
|
302
452
|
|
|
303
453
|
## 🧪 Testing Tips
|
|
304
454
|
* Use static APIs to assert state after component interactions.
|
|
305
|
-
* `sharedStatesApi.clearAll()` in `afterEach` to isolate tests.
|
|
455
|
+
* `sharedStatesApi.clearAll()`, `sharedFunctionsApi.clearAll()`, `sharedSubscriptionsApi.clearAll()` in `afterEach` to isolate tests.
|
|
306
456
|
* For async functions: trigger once, await UI stabilization, assert `results` present.
|
|
457
|
+
* For subscriptions: mock the subscription source (Firebase, WebSocket, etc.) and verify data flow.
|
|
307
458
|
|
|
308
459
|
|
|
309
460
|
## ❓ FAQ
|
|
@@ -319,6 +470,9 @@ Prefix keys by domain (e.g. `user:profile`, `cart:items`) or rely on provider sc
|
|
|
319
470
|
**Q: Why is my async function not re-running?**
|
|
320
471
|
It's cached. Use `forceTrigger()` or `clear()`.
|
|
321
472
|
|
|
473
|
+
**Q: How do I handle subscription cleanup?**
|
|
474
|
+
Subscriptions auto-cleanup when no components are listening. You can also manually call `unsubscribe()`.
|
|
475
|
+
|
|
322
476
|
**Q: Can I use it with Suspense?**
|
|
323
477
|
Currently no built-in Suspense wrappers; wrap `useSharedFunction` yourself if desired.
|
|
324
478
|
|
|
@@ -330,11 +484,14 @@ Returns `[value, setValue]`.
|
|
|
330
484
|
### `useSharedFunction(key, fn, scopeName?)`
|
|
331
485
|
Returns `{ state, trigger, forceTrigger, clear }`.
|
|
332
486
|
|
|
487
|
+
### `useSharedSubscription(key, subscriber, scopeName?)`
|
|
488
|
+
Returns `{ state, trigger, unsubscribe }`.
|
|
489
|
+
|
|
333
490
|
### `<SharedStatesProvider scopeName?>`
|
|
334
491
|
Wrap children; optional `scopeName` (string). If omitted a random unique one is generated.
|
|
335
492
|
|
|
336
493
|
### Static
|
|
337
|
-
`sharedStatesApi`, `sharedFunctionsApi` (see earlier table).
|
|
494
|
+
`sharedStatesApi`, `sharedFunctionsApi`, `sharedSubscriptionsApi` (see earlier table).
|
|
338
495
|
|
|
339
496
|
|
|
340
497
|
|
package/dist/SharedData.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AFunction, DataMapValue,
|
|
1
|
+
import { AFunction, DataMapValue, Prefix } from './types';
|
|
2
2
|
type SharedDataType<T> = DataMapValue & T;
|
|
3
3
|
export declare abstract class SharedData<T> {
|
|
4
4
|
data: Map<string, SharedDataType<T>>;
|
|
@@ -13,10 +13,11 @@ export declare abstract class SharedData<T> {
|
|
|
13
13
|
setValue(key: string, prefix: Prefix, data: T): void;
|
|
14
14
|
has(key: string, prefix: Prefix): string | undefined;
|
|
15
15
|
static prefix(key: string, prefix: Prefix): string;
|
|
16
|
+
useEffect(key: string, prefix: Prefix, unsub?: (() => void) | null): void;
|
|
16
17
|
}
|
|
17
18
|
export interface SharedApi<T> {
|
|
18
|
-
get: <S extends string = string>(key:
|
|
19
|
-
set: <S extends string = string>(key:
|
|
19
|
+
get: <S extends string = string>(key: S, scopeName: Prefix) => T;
|
|
20
|
+
set: <S extends string = string>(key: S, value: T, scopeName: Prefix) => void;
|
|
20
21
|
clearAll: () => void;
|
|
21
22
|
clear: (key: string, scopeName: Prefix) => void;
|
|
22
23
|
has: (key: string, scopeName: Prefix) => boolean;
|
package/dist/hooks/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AFunction,
|
|
1
|
+
import { AFunction, Prefix } from '../types';
|
|
2
2
|
import { SharedApi } from '../SharedData';
|
|
3
3
|
type SharedFunctionsState<T> = {
|
|
4
4
|
fnState: {
|
|
@@ -8,15 +8,15 @@ type SharedFunctionsState<T> = {
|
|
|
8
8
|
};
|
|
9
9
|
};
|
|
10
10
|
export declare class SharedFunctionsApi implements SharedApi<SharedFunctionsState<unknown>> {
|
|
11
|
-
get<T, S extends string = string>(key:
|
|
12
|
-
set<T, S extends string = string>(key:
|
|
11
|
+
get<T, S extends string = string>(key: S, scopeName?: Prefix): T;
|
|
12
|
+
set<T, S extends string = string>(key: S, fnState: SharedFunctionsState<T>, scopeName?: Prefix): void;
|
|
13
13
|
clearAll(): void;
|
|
14
14
|
clear(key: string, scopeName?: Prefix): void;
|
|
15
15
|
has(key: string, scopeName?: Prefix): boolean;
|
|
16
16
|
getAll(): Map<string, import('..').DataMapValue & SharedFunctionsState<unknown>>;
|
|
17
17
|
}
|
|
18
18
|
export declare const sharedFunctionsApi: SharedFunctionsApi;
|
|
19
|
-
export declare const useSharedFunction: <T, Args extends unknown[], S extends string = string>(key:
|
|
19
|
+
export declare const useSharedFunction: <T, Args extends unknown[], S extends string = string>(key: S, fn: AFunction<T, Args>, scopeName?: Prefix) => {
|
|
20
20
|
readonly state: {
|
|
21
21
|
results?: T | undefined;
|
|
22
22
|
isLoading: boolean;
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Prefix } from '../types';
|
|
2
2
|
import { SharedApi } from '../SharedData';
|
|
3
3
|
declare class SharedStatesApi implements SharedApi<{
|
|
4
4
|
value: unknown;
|
|
5
5
|
}> {
|
|
6
|
-
get<T, S extends string = string>(key:
|
|
7
|
-
set<T, S extends string = string>(key:
|
|
6
|
+
get<T, S extends string = string>(key: S, scopeName?: Prefix): T;
|
|
7
|
+
set<T, S extends string = string>(key: S, value: T, scopeName?: Prefix): void;
|
|
8
8
|
clearAll(): void;
|
|
9
9
|
clear(key: string, scopeName?: Prefix): void;
|
|
10
10
|
has(key: string, scopeName?: Prefix): boolean;
|
|
@@ -13,5 +13,5 @@ declare class SharedStatesApi implements SharedApi<{
|
|
|
13
13
|
}>;
|
|
14
14
|
}
|
|
15
15
|
export declare const sharedStatesApi: SharedStatesApi;
|
|
16
|
-
export declare const useSharedState: <T, S extends string = string>(key:
|
|
16
|
+
export declare const useSharedState: <T, S extends string = string>(key: S, value: T, scopeName?: Prefix) => readonly [T, (newValueOrCallbackToNewValue: T | ((prev: T) => T)) => void];
|
|
17
17
|
export {};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { PotentialPromise, Prefix } from '../types';
|
|
2
|
+
import { SharedApi } from '../SharedData';
|
|
3
|
+
type Unsubscribe = () => void;
|
|
4
|
+
export declare namespace SubscriberEvents {
|
|
5
|
+
type OnError = (error: unknown) => void;
|
|
6
|
+
type OnCompletion = () => void;
|
|
7
|
+
type Set<T> = (value: T) => void;
|
|
8
|
+
}
|
|
9
|
+
type Subscriber<T> = (set: SubscriberEvents.Set<T>, onError: SubscriberEvents.OnError, onCompletion: SubscriberEvents.OnCompletion) => PotentialPromise<Unsubscribe | void | undefined>;
|
|
10
|
+
type SharedSubscriptionsState<T> = {
|
|
11
|
+
fnState: {
|
|
12
|
+
data?: T;
|
|
13
|
+
isLoading: boolean;
|
|
14
|
+
error?: unknown;
|
|
15
|
+
subscribed: boolean;
|
|
16
|
+
};
|
|
17
|
+
unsubscribe?: Unsubscribe | void;
|
|
18
|
+
};
|
|
19
|
+
export declare class SharedSubscriptionsApi implements SharedApi<SharedSubscriptionsState<unknown>> {
|
|
20
|
+
get<T, S extends string = string>(key: S, scopeName?: Prefix): T;
|
|
21
|
+
set<T, S extends string = string>(key: S, fnState: SharedSubscriptionsState<T>, scopeName?: Prefix): void;
|
|
22
|
+
clearAll(): void;
|
|
23
|
+
clear(key: string, scopeName?: Prefix): void;
|
|
24
|
+
has(key: string, scopeName?: Prefix): boolean;
|
|
25
|
+
getAll(): Map<string, import('..').DataMapValue & SharedSubscriptionsState<unknown>>;
|
|
26
|
+
}
|
|
27
|
+
export declare const sharedSubscriptionsApi: SharedSubscriptionsApi;
|
|
28
|
+
export declare const useSharedSubscription: <T, S extends string = string>(key: S, subscriber: Subscriber<T>, scopeName?: Prefix) => {
|
|
29
|
+
readonly state: {
|
|
30
|
+
data?: T | undefined;
|
|
31
|
+
isLoading: boolean;
|
|
32
|
+
error?: unknown;
|
|
33
|
+
subscribed: boolean;
|
|
34
|
+
};
|
|
35
|
+
readonly trigger: () => void;
|
|
36
|
+
readonly forceTrigger: () => void;
|
|
37
|
+
readonly unsubscribe: () => void;
|
|
38
|
+
};
|
|
39
|
+
export {};
|