react-shared-states 1.0.11 → 1.0.13
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 +84 -415
- package/dist/hooks/index.d.ts +2 -2
- package/dist/hooks/use-shared-state.d.ts +3 -0
- package/dist/main.esm.js +387 -305
- package/dist/main.min.js +5 -5
- package/package.json +10 -3
- package/tests/index.test.tsx +136 -0
- package/pnpm-workspace.yaml +0 -2
package/README.md
CHANGED
|
@@ -23,7 +23,7 @@ Tiny, ergonomic, convention‑over‑configuration state, async function, and re
|
|
|
23
23
|
* Predictable: key + scope ⇒ value. That’s it.
|
|
24
24
|
|
|
25
25
|
|
|
26
|
-
##
|
|
26
|
+
## Install
|
|
27
27
|
|
|
28
28
|
```sh
|
|
29
29
|
npm install react-shared-states
|
|
@@ -33,7 +33,7 @@ or
|
|
|
33
33
|
pnpm add react-shared-states
|
|
34
34
|
```
|
|
35
35
|
|
|
36
|
-
##
|
|
36
|
+
## 60‑Second TL;DR
|
|
37
37
|
```tsx
|
|
38
38
|
import { useSharedState } from 'react-shared-states';
|
|
39
39
|
|
|
@@ -196,7 +196,7 @@ export default function App(){
|
|
|
196
196
|
```
|
|
197
197
|
|
|
198
198
|
|
|
199
|
-
##
|
|
199
|
+
## Core Concepts
|
|
200
200
|
| Concept | Summary |
|
|
201
201
|
|------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|
202
202
|
| Global by default | No provider necessary. Same key => shared state. |
|
|
@@ -209,433 +209,92 @@ export default function App(){
|
|
|
209
209
|
| Static/shared creation | Use `createSharedState`, `createSharedFunction`, `createSharedSubscription` to export reusable, type-safe shared resources that persist across `clearAll()` calls. |
|
|
210
210
|
|
|
211
211
|
|
|
212
|
-
##
|
|
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.
|
|
212
|
+
## Selecting State Slices (`useSharedStateSelector`)
|
|
271
213
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
export const profileFunction = createSharedFunction((id: string) => fetch(`/api/p/${id}`).then(r=>r.json()));
|
|
214
|
+
When a shared state holds an object, you might only need a small piece of it.
|
|
215
|
+
Using `useSharedState` will cause your component to re-render whenever *any* part of the object changes.
|
|
216
|
+
To optimize performance and avoid unnecessary re-renders, you can use the `useSharedStateSelector` hook.
|
|
276
217
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
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.
|
|
218
|
+
This hook allows you to subscribe to a specific, memoized slice of a shared state.
|
|
219
|
+
Your component will only re-render if the selected value changes.
|
|
220
|
+
It uses `react-fast-compare` for efficient deep equality checks.
|
|
293
221
|
|
|
294
222
|
Signature:
|
|
295
|
-
- `const
|
|
296
|
-
- `const
|
|
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)
|
|
223
|
+
- `const selectedValue = useSharedStateSelector(key, selector, scopeName?)`
|
|
224
|
+
- `const selectedValue = useSharedStateSelector(sharedStateCreated, selector)`
|
|
305
225
|
|
|
306
|
-
|
|
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';
|
|
226
|
+
The `selector` is a function that receives the full state and returns the desired slice.
|
|
312
227
|
|
|
313
|
-
|
|
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
|
-
);
|
|
228
|
+
### Example: Subscribing to a slice of a user object
|
|
333
229
|
|
|
334
|
-
|
|
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
|
-
```
|
|
230
|
+
Imagine a shared state for user settings:
|
|
354
231
|
|
|
355
|
-
### Pattern: WebSocket connection
|
|
356
232
|
```tsx
|
|
357
|
-
|
|
358
|
-
import {
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
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
|
-
}
|
|
233
|
+
// settingsState.ts
|
|
234
|
+
import { createSharedState } from 'react-shared-states';
|
|
235
|
+
|
|
236
|
+
export const settingsState = createSharedState({
|
|
237
|
+
theme: 'dark',
|
|
238
|
+
notifications: {
|
|
239
|
+
email: true,
|
|
240
|
+
push: false,
|
|
241
|
+
},
|
|
242
|
+
language: 'en',
|
|
243
|
+
});
|
|
393
244
|
```
|
|
394
245
|
|
|
395
|
-
|
|
396
|
-
```tsx
|
|
397
|
-
import { useEffect } from 'react';
|
|
398
|
-
import { useSharedSubscription } from 'react-shared-states';
|
|
246
|
+
A component that only cares about the theme can use `useSharedStateSelector` to avoid re-rendering when, for example, the notification settings change.
|
|
399
247
|
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
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
|
-
);
|
|
248
|
+
```tsx
|
|
249
|
+
import { useSharedState, useSharedStateSelector } from 'react-shared-states';
|
|
250
|
+
import { settingsState } from './settingsState';
|
|
415
251
|
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
252
|
+
function ThemeDisplay() {
|
|
253
|
+
const theme = useSharedStateSelector(settingsState, (settings) => settings.theme);
|
|
254
|
+
|
|
255
|
+
console.log('ThemeDisplay renders'); // This will only log when the theme changes
|
|
419
256
|
|
|
420
|
-
return <div>
|
|
257
|
+
return <div>Current theme: {theme}</div>;
|
|
421
258
|
}
|
|
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
259
|
|
|
260
|
+
function NotificationToggle() {
|
|
261
|
+
const [settings, setSettings] = useSharedState(settingsState);
|
|
431
262
|
|
|
432
|
-
|
|
433
|
-
|
|
263
|
+
const togglePush = () => {
|
|
264
|
+
setSettings(s => ({
|
|
265
|
+
...s,
|
|
266
|
+
notifications: { ...s.notifications, push: !s.notifications.push }
|
|
267
|
+
}));
|
|
268
|
+
};
|
|
434
269
|
|
|
435
|
-
|
|
436
|
-
|
|
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);
|
|
270
|
+
return <button onClick={togglePush}>Toggle Push Notifications</button>;
|
|
271
|
+
}
|
|
451
272
|
```
|
|
452
|
-
Useful for SSR hydration, event listeners, debugging, imperative workflows.
|
|
453
273
|
|
|
454
|
-
|
|
455
|
-
import { sharedStatesApi, sharedFunctionsApi, sharedSubscriptionsApi } from 'react-shared-states';
|
|
274
|
+
In this example, clicking the `NotificationToggle` button will **not** cause `ThemeDisplay` to re-render.
|
|
456
275
|
|
|
457
|
-
|
|
458
|
-
sharedStatesApi.set('bootstrap-data', { user: {...} });
|
|
459
|
-
|
|
460
|
-
// Preload state in a named scope
|
|
461
|
-
sharedStatesApi.set('bootstrap-data', { user: {...} }, 'myScope');
|
|
276
|
+
### A Note on Type Safety
|
|
462
277
|
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
const userScoped = sharedStatesApi.get('bootstrap-data', 'myScope');
|
|
278
|
+
For the best developer experience and full type safety,
|
|
279
|
+
it is **highly recommended** to use `useSharedStateSelector` with a statically created shared state object from `createSharedState`.
|
|
466
280
|
|
|
467
|
-
|
|
468
|
-
|
|
281
|
+
When you use a string key directly, the hook cannot infer the type of the state object.
|
|
282
|
+
You would have to provide the types explicitly as generic arguments, or they will default to `any`.
|
|
469
283
|
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
284
|
+
```tsx
|
|
285
|
+
// Less safe: using a string key
|
|
286
|
+
// You have to specify the types manually.
|
|
287
|
+
const theme = useSharedStateSelector<{ theme: string; /*...other props*/ }, 'settings', string>(
|
|
288
|
+
'settings',
|
|
289
|
+
(settings) => settings.theme
|
|
290
|
+
);
|
|
476
291
|
|
|
477
|
-
//
|
|
478
|
-
|
|
479
|
-
const
|
|
292
|
+
// Recommended: using a created state object
|
|
293
|
+
// Types are inferred automatically!
|
|
294
|
+
const theme = useSharedStateSelector(settingsState, (settings) => settings.theme);
|
|
480
295
|
```
|
|
481
296
|
|
|
482
|
-
##
|
|
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
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
## 🏗️ Sharing State (`useSharedState`)
|
|
593
|
-
Signature:
|
|
594
|
-
- `const [value, setValue] = useSharedState(key, initialValue, scopeName?)`
|
|
595
|
-
- `const [value, setValue] = useSharedState(sharedStateCreated)`
|
|
596
|
-
|
|
597
|
-
Behavior:
|
|
598
|
-
* First hook call (per key + scope) seeds with `initialValue`.
|
|
599
|
-
* Subsequent mounts with same key+scope ignore their `initialValue` (consistent source of truth).
|
|
600
|
-
* Setter accepts either value or updater `(prev)=>next`.
|
|
601
|
-
* React batching + equality check: listeners fire only when the value reference actually changes.
|
|
602
|
-
|
|
603
|
-
### Examples
|
|
604
|
-
1. Global theme (recommended for large apps)
|
|
605
|
-
```tsx
|
|
606
|
-
// themeState.ts
|
|
607
|
-
export const themeState = createSharedState('light');
|
|
608
|
-
// In components
|
|
609
|
-
const [theme, setTheme] = useSharedState(themeState);
|
|
610
|
-
```
|
|
611
|
-
2. Isolated wizard progress
|
|
612
|
-
```tsx
|
|
613
|
-
const wizardProgress = createSharedState(0);
|
|
614
|
-
<SharedStatesProvider>
|
|
615
|
-
<Wizard/>
|
|
616
|
-
</SharedStatesProvider>
|
|
617
|
-
// In Wizard
|
|
618
|
-
const [step, setStep] = useSharedState(wizardProgress);
|
|
619
|
-
```
|
|
620
|
-
3. Forcing cross‑portal sync
|
|
621
|
-
```tsx
|
|
622
|
-
const navState = createSharedState('closed', 'nav');
|
|
623
|
-
<SharedStatesProvider scopeName="nav" children={<PrimaryNav/>} />
|
|
624
|
-
<Portal>
|
|
625
|
-
<SharedStatesProvider scopeName="nav" children={<MobileNav/>} />
|
|
626
|
-
</Portal>
|
|
627
|
-
// In both navs
|
|
628
|
-
const [navOpen, setNavOpen] = useSharedState(navState);
|
|
629
|
-
```
|
|
630
|
-
4. Overriding nearest provider
|
|
631
|
-
```tsx
|
|
632
|
-
// Even if inside a provider, this explicitly binds to global
|
|
633
|
-
const globalFlag = createSharedState(false, '_global');
|
|
634
|
-
const [flag, setFlag] = useSharedState(globalFlag);
|
|
635
|
-
```
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
## ⚡ Shared Async Functions (`useSharedFunction`)
|
|
297
|
+
## Shared Async Functions (`useSharedFunction`)
|
|
639
298
|
Signature:
|
|
640
299
|
- `const { state, trigger, forceTrigger, clear } = useSharedFunction(key, asyncFn, scopeName?)`
|
|
641
300
|
- `const { state, trigger, forceTrigger, clear } = useSharedFunction(sharedFunctionCreated)`
|
|
@@ -646,6 +305,7 @@ Semantics:
|
|
|
646
305
|
* Multiple components with the same key+scope share one execution + result.
|
|
647
306
|
* `clear()` deletes the cache (next trigger re-runs).
|
|
648
307
|
* You decide when to invoke `trigger` (e.g. on mount, on button click, when dependencies change, etc.).
|
|
308
|
+
* Functions created with `createSharedFunction` are **static** and persist across `clearAll()` calls.
|
|
649
309
|
|
|
650
310
|
### Pattern: lazy load on first render
|
|
651
311
|
```tsx
|
|
@@ -665,7 +325,7 @@ const refresh = () => forceTrigger();
|
|
|
665
325
|
```
|
|
666
326
|
|
|
667
327
|
|
|
668
|
-
##
|
|
328
|
+
## Real-time Subscriptions (`useSharedSubscription`)
|
|
669
329
|
Perfect for Firebase listeners, WebSocket connections,
|
|
670
330
|
Server-Sent Events, or any streaming data source that needs cleanup.
|
|
671
331
|
|
|
@@ -807,10 +467,12 @@ Subscription semantics:
|
|
|
807
467
|
* Components mounting later instantly get the latest `data` without re-subscribing.
|
|
808
468
|
|
|
809
469
|
|
|
810
|
-
##
|
|
811
|
-
##
|
|
470
|
+
## Static APIs (outside React)
|
|
471
|
+
## Static/Global Shared Resource Creation
|
|
812
472
|
|
|
813
|
-
For large apps, you can create and export shared state, function,
|
|
473
|
+
For large apps, you can create and export shared state, function,
|
|
474
|
+
or subscription objects for type safety and to avoid key collisions.
|
|
475
|
+
This pattern is similar to Zustand or Jotai stores:
|
|
814
476
|
|
|
815
477
|
```ts
|
|
816
478
|
import { createSharedState, createSharedFunction, createSharedSubscription, useSharedState, useSharedFunction, useSharedSubscription } from 'react-shared-states';
|
|
@@ -866,7 +528,7 @@ const subStateScoped = sharedSubscriptionsApi.get('live-chat', 'myScope');
|
|
|
866
528
|
`scopeName` defaults to `"_global"`. Internally, keys are stored as `${scope}//${key}`. The `.getAll()` method returns a nested object: `{ [scope]: { [key]: value } }`.
|
|
867
529
|
|
|
868
530
|
|
|
869
|
-
##
|
|
531
|
+
## Scoping Rules Deep Dive
|
|
870
532
|
Resolution order used inside hooks:
|
|
871
533
|
1. Explicit 3rd parameter (`scopeName`)
|
|
872
534
|
2. Nearest `SharedStatesProvider` above the component
|
|
@@ -877,7 +539,7 @@ Unnamed providers auto‑generate a random scope name: each mount = isolated isl
|
|
|
877
539
|
Two providers sharing the same `scopeName` act as a single logical scope even if they are disjoint in the tree (great for portals / microfrontends).
|
|
878
540
|
|
|
879
541
|
|
|
880
|
-
##
|
|
542
|
+
## Comparison Snapshot
|
|
881
543
|
| Criterion | react-shared-states | Redux Toolkit | Zustand |
|
|
882
544
|
|----------------|------------------------------------------|----------------------|----------------------------------|
|
|
883
545
|
| Setup | Install & call hook | Slice + store config | Create store function |
|
|
@@ -889,7 +551,7 @@ Two providers sharing the same `scopeName` act as a single logical scope even if
|
|
|
889
551
|
| Learning curve | Minutes | Higher | Low |
|
|
890
552
|
|
|
891
553
|
|
|
892
|
-
##
|
|
554
|
+
## Testing Tips
|
|
893
555
|
* Use static APIs to assert state after component interactions.
|
|
894
556
|
* `sharedStatesApi.clearAll(false, true)`, `sharedFunctionsApi.clearAll(false, true)`, `sharedSubscriptionsApi.clearAll(false, true)` in `afterEach` to isolate tests and clear static states.
|
|
895
557
|
* For async functions: trigger once, await UI stabilization, assert `results` present.
|
|
@@ -898,8 +560,7 @@ Two providers sharing the same `scopeName` act as a single logical scope even if
|
|
|
898
560
|
|
|
899
561
|
## ❓ FAQ
|
|
900
562
|
**Q: How do I reset a single shared state?**
|
|
901
|
-
`sharedStatesApi.clear('key')`. If the state was created with `createSharedState`, it will reset to its initial value.
|
|
902
|
-
Otherwise, it will be removed.
|
|
563
|
+
`sharedStatesApi.clear('key')`. If the state was created with `createSharedState`, it will reset to its initial value. Otherwise, it will be removed.
|
|
903
564
|
|
|
904
565
|
**Q: Can I pre-hydrate data on the server?**
|
|
905
566
|
Yes. Call `sharedStatesApi.set(...)` during bootstrap, then first client hook usage will pick it up.
|
|
@@ -917,13 +578,19 @@ Subscriptions auto-cleanup when no components are listening. You can also manual
|
|
|
917
578
|
Currently no built-in Suspense wrappers; wrap `useSharedFunction` yourself if desired.
|
|
918
579
|
|
|
919
580
|
|
|
920
|
-
##
|
|
581
|
+
## Full API Reference
|
|
921
582
|
### `useSharedState(key, initialValue, scopeName?)`
|
|
922
583
|
Returns `[value, setValue]`.
|
|
923
584
|
|
|
924
585
|
### `useSharedState(sharedStateCreated)`
|
|
925
586
|
Returns `[value, setValue]`.
|
|
926
587
|
|
|
588
|
+
### `useSharedStateSelector(key, selector, scopeName?)`
|
|
589
|
+
Returns the selected value.
|
|
590
|
+
|
|
591
|
+
### `useSharedStateSelector(sharedStateCreated, selector)`
|
|
592
|
+
Returns the selected value.
|
|
593
|
+
|
|
927
594
|
### `useSharedFunction(key, fn, scopeName?)`
|
|
928
595
|
Returns `{ state, trigger, forceTrigger, clear }`.
|
|
929
596
|
|
|
@@ -944,7 +611,7 @@ Wrap children; optional `scopeName` (string). If omitted a random unique one is
|
|
|
944
611
|
|
|
945
612
|
|
|
946
613
|
|
|
947
|
-
##
|
|
614
|
+
## Contributions
|
|
948
615
|
|
|
949
616
|
We welcome contributions!
|
|
950
617
|
If you'd like to improve `react-shared-states`,
|
|
@@ -962,4 +629,6 @@ feel free to [open an issue](https://github.com/HichemTab-tech/react-shared-stat
|
|
|
962
629
|
## 🌟 Acknowledgements
|
|
963
630
|
|
|
964
631
|
Inspired by React's built-in primitives and the ergonomics of modern lightweight state libraries.
|
|
965
|
-
Thanks to early adopters for feedback.
|
|
632
|
+
Thanks to early adopters for feedback.
|
|
633
|
+
If you'd like to improve `react-shared-states`,
|
|
634
|
+
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).
|
package/dist/hooks/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export { useSharedState, sharedStatesApi, createSharedState, SharedStatesApi } from './use-shared-state';
|
|
2
|
-
export type { SharedStateCreated } from './use-shared-state';
|
|
1
|
+
export { useSharedState, sharedStatesApi, createSharedState, SharedStatesApi, useSharedStateSelector } from './use-shared-state';
|
|
2
|
+
export type { SharedStateCreated, SharedStateSelector } from './use-shared-state';
|
|
3
3
|
export { useSharedFunction, sharedFunctionsApi, createSharedFunction, SharedFunctionsApi } from './use-shared-function';
|
|
4
4
|
export type { SharedFunctionStateReturn } from './use-shared-function';
|
|
5
5
|
export { useSharedSubscription, sharedSubscriptionsApi, createSharedSubscription, SharedSubscriptionsApi } from './use-shared-subscription';
|
|
@@ -28,4 +28,7 @@ export interface SharedStateCreated<T> extends SharedCreated {
|
|
|
28
28
|
export declare const createSharedState: <T>(initialValue: T, scopeName?: Prefix) => SharedStateCreated<T>;
|
|
29
29
|
export declare function useSharedState<T, S extends string>(key: S, initialValue: T, scopeName?: Prefix): readonly [T, (v: T | ((prev: T) => T)) => void];
|
|
30
30
|
export declare function useSharedState<T>(sharedStateCreated: SharedStateCreated<T>): readonly [T, (v: T | ((prev: T) => T)) => void];
|
|
31
|
+
export type SharedStateSelector<S, T = S> = (original: S) => T;
|
|
32
|
+
export declare function useSharedStateSelector<T, S extends string, R>(key: S, selector: SharedStateSelector<T, R>, scopeName?: Prefix): Readonly<R>;
|
|
33
|
+
export declare function useSharedStateSelector<T, R>(sharedStateCreated: SharedStateCreated<T>, selector: SharedStateSelector<T, R>): Readonly<R>;
|
|
31
34
|
export {};
|