@kuindji/reactive 1.0.24 → 1.2.0

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.
Files changed (60) hide show
  1. package/README.md +160 -14
  2. package/dist/action.d.ts +31 -10
  3. package/dist/action.js +156 -23
  4. package/dist/actionBus.d.ts +13 -4
  5. package/dist/actionBus.js +201 -5
  6. package/dist/actionMap.d.ts +26 -19
  7. package/dist/actionMap.js +10 -4
  8. package/dist/event.d.ts +37 -3
  9. package/dist/event.js +345 -78
  10. package/dist/eventBus.d.ts +7 -3
  11. package/dist/eventBus.js +194 -34
  12. package/dist/index.d.ts +7 -7
  13. package/dist/index.js +7 -7
  14. package/dist/lib/actionMapInternal.d.ts +8 -0
  15. package/dist/lib/actionMapInternal.js +8 -0
  16. package/dist/lib/isPromiseLike.d.ts +1 -0
  17. package/dist/lib/isPromiseLike.js +5 -0
  18. package/dist/lib/normalizeEventOptions.d.ts +13 -0
  19. package/dist/lib/normalizeEventOptions.js +21 -0
  20. package/dist/lib/types.d.ts +1 -1
  21. package/dist/react/ErrorBoundary.d.ts +1 -1
  22. package/dist/react/listenerOptionsEqual.d.ts +27 -0
  23. package/dist/react/listenerOptionsEqual.js +121 -0
  24. package/dist/react/useAction.d.ts +3 -3
  25. package/dist/react/useAction.js +10 -7
  26. package/dist/react/useActionBus.d.ts +4 -4
  27. package/dist/react/useActionBus.js +32 -2
  28. package/dist/react/useActionBusStatus.d.ts +13 -0
  29. package/dist/react/useActionBusStatus.js +26 -0
  30. package/dist/react/useActionMap.d.ts +4 -4
  31. package/dist/react/useActionMap.js +40 -7
  32. package/dist/react/useAsyncAction.d.ts +20 -0
  33. package/dist/react/useAsyncAction.js +53 -0
  34. package/dist/react/useEvent.d.ts +2 -2
  35. package/dist/react/useEvent.js +18 -2
  36. package/dist/react/useEventBus.d.ts +2 -2
  37. package/dist/react/useEventBus.js +14 -10
  38. package/dist/react/useListenToAction.d.ts +1 -1
  39. package/dist/react/useListenToAction.js +17 -38
  40. package/dist/react/useListenToActionBus.d.ts +3 -3
  41. package/dist/react/useListenToActionBus.js +15 -9
  42. package/dist/react/useListenToEvent.d.ts +2 -2
  43. package/dist/react/useListenToEvent.js +8 -6
  44. package/dist/react/useListenToEventBus.d.ts +3 -3
  45. package/dist/react/useListenToEventBus.js +9 -7
  46. package/dist/react/useListenToStoreChanges.d.ts +3 -3
  47. package/dist/react/useListenToStoreChanges.js +9 -7
  48. package/dist/react/useReconciledListener.d.ts +33 -0
  49. package/dist/react/useReconciledListener.js +44 -0
  50. package/dist/react/useStore.d.ts +2 -2
  51. package/dist/react/useStore.js +71 -19
  52. package/dist/react/useStoreSelector.d.ts +35 -0
  53. package/dist/react/useStoreSelector.js +144 -0
  54. package/dist/react/useStoreState.d.ts +2 -2
  55. package/dist/react/useStoreState.js +26 -21
  56. package/dist/react.d.ts +16 -13
  57. package/dist/react.js +16 -13
  58. package/dist/store.d.ts +12 -8
  59. package/dist/store.js +473 -39
  60. package/package.json +13 -3
@@ -1,4 +1,4 @@
1
- import type { BaseEvent, ListenerOptions } from "../event";
2
- import type { ErrorListenerSignature } from "../lib/types";
1
+ import type { BaseEvent, ListenerOptions } from "../event.js";
2
+ import type { ErrorListenerSignature } from "../lib/types.js";
3
3
  export type { BaseEvent, ErrorListenerSignature, ListenerOptions };
4
4
  export declare function useListenToEvent<TEvent extends BaseEvent, TListenerSignature extends TEvent["__type"]["signature"], TErrorListenerSignature extends TEvent["__type"]["errorListenerSignature"]>(event: TEvent, listener: TListenerSignature, options?: ListenerOptions, errorListener?: TErrorListenerSignature): void;
@@ -1,16 +1,18 @@
1
1
  import { useCallback, useEffect, useRef } from "react";
2
+ import { useReconciledListener } from "./useReconciledListener.js";
2
3
  export function useListenToEvent(event, listener, options, errorListener) {
3
4
  const listenerRef = useRef(listener);
4
5
  listenerRef.current = listener;
5
6
  const genericHandler = useCallback((...args) => {
6
7
  return listenerRef.current(...args);
7
8
  }, []);
8
- useEffect(() => {
9
- event.addListener(genericHandler, options);
10
- return () => {
11
- event.removeListener(genericHandler);
12
- };
13
- }, [event, genericHandler]);
9
+ useReconciledListener({
10
+ keyDeps: [event],
11
+ options,
12
+ subscribe: (opts) => event.addListener(genericHandler, opts !== null && opts !== void 0 ? opts : undefined),
13
+ unsubscribe: (ctx) => event.removeListener(genericHandler, ctx),
14
+ update: (ctx, opts) => event.updateListenerOptions(genericHandler, ctx, opts !== null && opts !== void 0 ? opts : undefined),
15
+ });
14
16
  useEffect(() => {
15
17
  if (errorListener) {
16
18
  event.addErrorListener(errorListener);
@@ -1,5 +1,5 @@
1
- import type { ListenerOptions } from "../event";
2
- import type { BaseEventBus } from "../eventBus";
3
- import type { ErrorListenerSignature, KeyOf } from "../lib/types";
1
+ import type { ListenerOptions } from "../event.js";
2
+ import type { BaseEventBus } from "../eventBus.js";
3
+ import type { ErrorListenerSignature, KeyOf } from "../lib/types.js";
4
4
  export type { BaseEventBus, ErrorListenerSignature, ListenerOptions };
5
5
  export declare function useListenToEventBus<TEventBus extends BaseEventBus, TKey extends KeyOf<TEventBus["__type"]["eventSignatures"]>, TListener extends TEventBus["__type"]["eventSignatures"][TKey]>(eventBus: TEventBus, eventName: TKey, listener: TListener, options?: ListenerOptions, errorListener?: ErrorListenerSignature<any[]>): void;
@@ -1,4 +1,5 @@
1
1
  import { useCallback, useEffect, useRef } from "react";
2
+ import { useReconciledListener } from "./useReconciledListener.js";
2
3
  export function useListenToEventBus(eventBus, eventName, listener, options, errorListener) {
3
4
  const listenerRef = useRef(listener);
4
5
  listenerRef.current = listener;
@@ -6,13 +7,14 @@ export function useListenToEventBus(eventBus, eventName, listener, options, erro
6
7
  var _a;
7
8
  return (_a = listenerRef.current) === null || _a === void 0 ? void 0 : _a.call(listenerRef, ...args);
8
9
  }, []);
9
- // Main listener - cleanup pattern handles eventBus/eventName changes
10
- useEffect(() => {
11
- eventBus.addListener(eventName, genericHandler, options);
12
- return () => {
13
- eventBus.removeListener(eventName, genericHandler);
14
- };
15
- }, [eventBus, eventName, genericHandler]);
10
+ // Main listener - reconciled across eventBus/eventName/option changes
11
+ useReconciledListener({
12
+ keyDeps: [eventBus, eventName],
13
+ options,
14
+ subscribe: (opts) => eventBus.addListener(eventName, genericHandler, opts !== null && opts !== void 0 ? opts : undefined),
15
+ unsubscribe: (ctx) => eventBus.removeListener(eventName, genericHandler, ctx),
16
+ update: (ctx, opts) => eventBus.updateListenerOptions(eventName, genericHandler, ctx, opts !== null && opts !== void 0 ? opts : undefined),
17
+ });
16
18
  // Error listener - cleanup pattern
17
19
  useEffect(() => {
18
20
  if (errorListener) {
@@ -1,5 +1,5 @@
1
- import type { ListenerOptions } from "../event";
2
- import { KeyOf } from "../lib/types";
3
- import type { BaseStore } from "../store";
1
+ import type { ListenerOptions } from "../event.js";
2
+ import { KeyOf } from "../lib/types.js";
3
+ import type { BaseStore } from "../store.js";
4
4
  export type { BaseStore, ListenerOptions };
5
5
  export declare function useListenToStoreChanges<TStore extends BaseStore, TKey extends KeyOf<TStore["__type"]["propTypes"]>, TListener extends TStore["__type"]["changeEvents"][TKey]>(store: TStore, key: TKey, listener: TListener, options?: ListenerOptions): void;
@@ -1,14 +1,16 @@
1
- import { useCallback, useEffect, useRef } from "react";
1
+ import { useCallback, useRef } from "react";
2
+ import { useReconciledListener } from "./useReconciledListener.js";
2
3
  export function useListenToStoreChanges(store, key, listener, options) {
3
4
  const listenerRef = useRef(listener);
4
5
  listenerRef.current = listener;
5
6
  const genericHandler = useCallback((value, previousValue) => {
6
7
  return listenerRef.current(value, previousValue);
7
8
  }, []);
8
- useEffect(() => {
9
- store.onChange(key, genericHandler, options);
10
- return () => {
11
- store.removeOnChange(key, genericHandler);
12
- };
13
- }, [store, key, genericHandler]);
9
+ useReconciledListener({
10
+ keyDeps: [store, key],
11
+ options,
12
+ subscribe: (opts) => store.onChange(key, genericHandler, opts !== null && opts !== void 0 ? opts : undefined),
13
+ unsubscribe: (ctx) => store.removeOnChange(key, genericHandler, ctx),
14
+ update: (ctx, opts) => store.updateOnChangeOptions(key, genericHandler, ctx, opts !== null && opts !== void 0 ? opts : undefined),
15
+ });
14
16
  }
@@ -0,0 +1,33 @@
1
+ import type { ListenerOptions } from "../event.js";
2
+ type ListenerOps = {
3
+ /**
4
+ * Identity dependencies. When any element changes (reference equality) the
5
+ * listener is fully resubscribed: the old registration is removed using the
6
+ * previous closure (previous target + previous context) and a fresh one is
7
+ * added. `context` is appended automatically because it is part of listener
8
+ * identity. The array length must stay constant across renders.
9
+ */
10
+ keyDeps: ReadonlyArray<unknown>;
11
+ options?: ListenerOptions | null;
12
+ /** Add the listener to the current target with the given options. */
13
+ subscribe: (options?: ListenerOptions | null) => void;
14
+ /** Remove the listener from the current target using the given context. */
15
+ unsubscribe: (context: object | null) => void;
16
+ /** Update soft options on the live listener in place (counters preserved). */
17
+ update: (context: object | null, options?: ListenerOptions | null) => void;
18
+ };
19
+ /**
20
+ * Reconciles a single reactive listener across renders without relying on the
21
+ * identity of the options object.
22
+ *
23
+ * Two effects cooperate:
24
+ * - an identity effect keyed by `[...keyDeps, context]` performs the classic
25
+ * add-on-mount / remove-on-cleanup cycle, so target/context changes (and
26
+ * React StrictMode remounts) resubscribe correctly using the OLD context on
27
+ * cleanup;
28
+ * - a reconciliation effect runs every render and, when only soft options
29
+ * changed, updates the live listener in place instead of resubscribing, so
30
+ * per-listener counters are preserved.
31
+ */
32
+ export declare function useReconciledListener({ keyDeps, options, subscribe, unsubscribe, update, }: ListenerOps): void;
33
+ export {};
@@ -0,0 +1,44 @@
1
+ import { useEffect, useRef } from "react";
2
+ import { areListenerOptionsEqual } from "./listenerOptionsEqual.js";
3
+ /**
4
+ * Reconciles a single reactive listener across renders without relying on the
5
+ * identity of the options object.
6
+ *
7
+ * Two effects cooperate:
8
+ * - an identity effect keyed by `[...keyDeps, context]` performs the classic
9
+ * add-on-mount / remove-on-cleanup cycle, so target/context changes (and
10
+ * React StrictMode remounts) resubscribe correctly using the OLD context on
11
+ * cleanup;
12
+ * - a reconciliation effect runs every render and, when only soft options
13
+ * changed, updates the live listener in place instead of resubscribing, so
14
+ * per-listener counters are preserved.
15
+ */
16
+ export function useReconciledListener({ keyDeps, options, subscribe, unsubscribe, update, }) {
17
+ var _a;
18
+ const context = (_a = options === null || options === void 0 ? void 0 : options.context) !== null && _a !== void 0 ? _a : null;
19
+ const committedRef = useRef(undefined);
20
+ const registeredRef = useRef(false);
21
+ // Identity effect: (re)subscribe on target/context change.
22
+ useEffect(() => {
23
+ subscribe(options);
24
+ committedRef.current = options;
25
+ registeredRef.current = true;
26
+ return () => {
27
+ unsubscribe(context);
28
+ registeredRef.current = false;
29
+ };
30
+ }, [...keyDeps, context]);
31
+ // Reconciliation effect: in-place soft-option updates every render.
32
+ useEffect(() => {
33
+ if (!registeredRef.current) {
34
+ return;
35
+ }
36
+ if (committedRef.current === options) {
37
+ return;
38
+ }
39
+ if (!areListenerOptionsEqual(committedRef.current, options)) {
40
+ update(context, options);
41
+ }
42
+ committedRef.current = options;
43
+ });
44
+ }
@@ -1,5 +1,5 @@
1
- import { createStore } from "../store";
2
- import type { BasePropMap, BeforeChangeEventName, ChangeEventName, ErrorEventName, ResetEventName, StoreDefinitionHelper } from "../store";
1
+ import { createStore } from "../store.js";
2
+ import type { BasePropMap, BeforeChangeEventName, ChangeEventName, ErrorEventName, ResetEventName, StoreDefinitionHelper } from "../store.js";
3
3
  export type { BasePropMap, BeforeChangeEventName, ChangeEventName, ErrorEventName, ResetEventName, StoreDefinitionHelper, };
4
4
  export declare function useStore<PropMap extends BasePropMap, Store extends StoreDefinitionHelper<PropMap> = StoreDefinitionHelper<PropMap>, Config extends {
5
5
  onChange?: Partial<Store["changeEvents"]>;
@@ -1,26 +1,78 @@
1
- import { useMemo } from "react";
2
- import { createStore } from "../store";
1
+ import { useEffect, useMemo, useRef } from "react";
2
+ import { createStore } from "../store.js";
3
3
  export function useStore(initialData = {}, config) {
4
- const store = useMemo(() => {
5
- const store = createStore(initialData);
6
- if (config === null || config === void 0 ? void 0 : config.onChange) {
7
- for (const key in config.onChange) {
8
- store.onChange(key, config.onChange[key]);
9
- }
4
+ // initialData is seed-only (captured once); later changes are ignored.
5
+ const store = useMemo(() => createStore(initialData), []);
6
+ // Track only the handlers we added (per category + key) and compare by
7
+ // reference, so consumer listeners added outside the hook are never
8
+ // touched and inline-equal config maps do not duplicate or churn
9
+ // subscriptions.
10
+ const appliedRef = useRef({
11
+ onChange: {},
12
+ pipes: {},
13
+ control: {},
14
+ });
15
+ const add = (category, key, fn) => {
16
+ if (category === "onChange") {
17
+ store.onChange(key, fn);
10
18
  }
11
- if (config === null || config === void 0 ? void 0 : config.pipes) {
12
- for (const key in config.pipes) {
13
- // @ts-expect-error - TS widens for-in key to string; types are correct
14
- store.pipe(key, config.pipes[key]);
15
- }
19
+ else if (category === "pipes") {
20
+ store.pipe(key, fn);
16
21
  }
17
- if (config === null || config === void 0 ? void 0 : config.control) {
18
- for (const key in config.control) {
19
- // @ts-expect-error - TS widens for-in key to string; types are correct
20
- store.control(key, config.control[key]);
21
- }
22
+ else {
23
+ store.control(key, fn);
24
+ }
25
+ };
26
+ const remove = (category, key, fn) => {
27
+ if (category === "onChange") {
28
+ store.removeOnChange(key, fn);
22
29
  }
23
- return store;
30
+ else if (category === "pipes") {
31
+ store.removePipe(key, fn);
32
+ }
33
+ else {
34
+ store.removeControl(key, fn);
35
+ }
36
+ };
37
+ // Reconcile config handlers every render (no cleanup here, so equal config
38
+ // never causes remove/add churn).
39
+ useEffect(() => {
40
+ const categories = {
41
+ onChange: config === null || config === void 0 ? void 0 : config.onChange,
42
+ pipes: config === null || config === void 0 ? void 0 : config.pipes,
43
+ control: config === null || config === void 0 ? void 0 : config.control,
44
+ };
45
+ Object.keys(categories).forEach((category) => {
46
+ var _a;
47
+ const next = (_a = categories[category]) !== null && _a !== void 0 ? _a : {};
48
+ const prev = appliedRef.current[category];
49
+ // Remove stale/changed handlers before adding (matters for pipes).
50
+ for (const key in prev) {
51
+ if (next[key] !== prev[key]) {
52
+ remove(category, key, prev[key]);
53
+ }
54
+ }
55
+ for (const key in next) {
56
+ if (next[key] !== prev[key]) {
57
+ add(category, key, next[key]);
58
+ }
59
+ }
60
+ appliedRef.current[category] = Object.assign({}, next);
61
+ });
62
+ });
63
+ // Unmount cleanup: detach everything we applied (also makes StrictMode
64
+ // remount re-subscribe cleanly via the reconcile effect above).
65
+ useEffect(() => {
66
+ return () => {
67
+ const applied = appliedRef.current;
68
+ Object.keys(applied).forEach((category) => {
69
+ const map = applied[category];
70
+ for (const key in map) {
71
+ remove(category, key, map[key]);
72
+ }
73
+ applied[category] = {};
74
+ });
75
+ };
24
76
  }, []);
25
77
  return store;
26
78
  }
@@ -0,0 +1,35 @@
1
+ import type { KeyOf } from "../lib/types.js";
2
+ import type { BaseStore } from "../store.js";
3
+ export type EqualityFn<T> = (a: T, b: T) => boolean;
4
+ /**
5
+ * Subscribes to a derived slice of a store with custom equality. Selector
6
+ * re-execution is gated on the raw input (dep values, or a shallow compare of
7
+ * the full state), so a selector that builds a fresh object each call returns a
8
+ * stable cached reference while its input is unchanged — safe even without an
9
+ * equality fn (an un-gated fresh reference on every getSnapshot call would loop
10
+ * forever). The optional equality fn additionally bails React re-renders when a
11
+ * recompute produces an equal-but-fresh result.
12
+ *
13
+ * Two forms:
14
+ * useStoreSelector(store, (s) => `${s.first} ${s.last}`, shallowEqual?)
15
+ * useStoreSelector(store, ["first", "last"], (first, last) => …, eqFn?)
16
+ *
17
+ * The deps-keyed form recomputes only when the change batch touches its keys.
18
+ *
19
+ * Prefer the deps-keyed form for narrow reads. The selector form (no deps)
20
+ * subscribes to every store change and rebuilds the whole state via getData()
21
+ * on each one, re-running the selector even for unrelated writes (the equality
22
+ * fn still bails React re-renders, but the recompute itself is not filtered).
23
+ * The deps-keyed form both filters the subscription to its keys and avoids
24
+ * materializing the full state, so reach for it when selecting a few slices of
25
+ * a large or frequently-written store.
26
+ *
27
+ * Concurrent-safe: the selection is memoized in a render-phase `useMemo` (an
28
+ * abandoned concurrent render discards it rather than leaking it into the
29
+ * committed tree) and the committed value is recorded in an effect, not during
30
+ * render. This mirrors React's own `useSyncExternalStoreWithSelector`.
31
+ */
32
+ export declare function useStoreSelector<TStore extends BaseStore, R>(store: TStore, selector: (state: TStore["__type"]["propTypes"]) => R, equalityFn?: EqualityFn<R>): R;
33
+ export declare function useStoreSelector<TStore extends BaseStore, const D extends readonly KeyOf<TStore["__type"]["propTypes"]>[], R>(store: TStore, deps: D, selector: (...values: {
34
+ [I in keyof D]: TStore["__type"]["propTypes"][D[I]];
35
+ }) => R, equalityFn?: EqualityFn<R>): R;
@@ -0,0 +1,144 @@
1
+ import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useSyncExternalStore, } from "react";
2
+ import { ChangeEventName } from "../store.js";
3
+ // Shallow per-entry equality for two state objects (enumerable own keys
4
+ // compared with Object.is). Used to gate selector re-execution in the
5
+ // no-deps form, whose input is rebuilt fresh by getData() on every call.
6
+ function shallowEqualObject(a, b) {
7
+ if (a === b) {
8
+ return true;
9
+ }
10
+ const aKeys = Object.keys(a);
11
+ const bKeys = Object.keys(b);
12
+ if (aKeys.length !== bKeys.length) {
13
+ return false;
14
+ }
15
+ for (const key of aKeys) {
16
+ if (!Object.prototype.hasOwnProperty.call(b, key)
17
+ || !Object.is(a[key], b[key])) {
18
+ return false;
19
+ }
20
+ }
21
+ return true;
22
+ }
23
+ export function useStoreSelector(store, arg2, arg3, arg4) {
24
+ var _a;
25
+ const deps = Array.isArray(arg2) ? arg2 : null;
26
+ const selector = (deps ? arg3 : arg2);
27
+ const equalityFn = (_a = (deps ? arg4 : arg3)) !== null && _a !== void 0 ? _a : Object.is;
28
+ // Committed selection cache. Written ONLY in an effect (commit phase) so an
29
+ // abandoned concurrent render cannot leak its selection into the committed
30
+ // tree (which a render-phase write to this cache would).
31
+ const instRef = useRef(null);
32
+ if (instRef.current === null) {
33
+ instRef.current = { hasValue: false, value: null };
34
+ }
35
+ const inst = instRef.current;
36
+ // Latest deps for the subscribe filter. Updated in a layout effect (commit
37
+ // phase), not during render, and read only inside the change listener (which
38
+ // fires after commit), so the subscription always filters on committed deps.
39
+ const depsRef = useRef(deps);
40
+ useLayoutEffect(() => {
41
+ depsRef.current = deps;
42
+ });
43
+ // The memoized selection getter. Built during render, but the useMemo result
44
+ // is part of the fiber's memoized state: an abandoned concurrent render
45
+ // discards it, so no closure leaks. Rebuilt only when the store, selector,
46
+ // equality, or deps identity changes. The committed `inst.value` is read
47
+ // (never written) here, so a re-render with an equal result bails out to the
48
+ // committed reference.
49
+ const getSelection = useMemo(() => {
50
+ let hasMemo = false;
51
+ let memoizedInput = [];
52
+ let memoized;
53
+ // Read the raw selector input (dep values, or the full state). On a
54
+ // destroyed store, read via getData() (which returns {} without
55
+ // asserting) instead of store.get() (which throws): getSnapshot can
56
+ // run for a still-mounted component after the store is destroyed
57
+ // (e.g. a provider torn down first), and must not throw out of
58
+ // render. Returns the input as an arg array so it can be both
59
+ // shallow-compared and spread into the selector.
60
+ const readInput = () => {
61
+ if (deps) {
62
+ if (store.isDestroyed()) {
63
+ const snapshot = store.getData();
64
+ return deps.map((d) => snapshot[d]);
65
+ }
66
+ return deps.map((d) => store.get(d));
67
+ }
68
+ return [store.getData()];
69
+ };
70
+ // Compare two raw inputs. The deps form holds dep values, compared
71
+ // by identity (the store replaces values on change, so identity
72
+ // tracks change). The selector form holds a single full-state
73
+ // object rebuilt fresh by getData() on every call, so it must be
74
+ // shallow-compared by entries rather than reference.
75
+ const inputsEqual = (a, b) => {
76
+ if (deps) {
77
+ if (a.length !== b.length) {
78
+ return false;
79
+ }
80
+ for (let i = 0; i < a.length; i++) {
81
+ if (!Object.is(a[i], b[i])) {
82
+ return false;
83
+ }
84
+ }
85
+ return true;
86
+ }
87
+ return shallowEqualObject(a[0], b[0]);
88
+ };
89
+ return () => {
90
+ const input = readInput();
91
+ if (!hasMemo) {
92
+ hasMemo = true;
93
+ memoizedInput = input;
94
+ const next = selector(...input);
95
+ if (inst.hasValue && equalityFn(inst.value, next)) {
96
+ memoized = inst.value;
97
+ return inst.value;
98
+ }
99
+ memoized = next;
100
+ return next;
101
+ }
102
+ // Gate selector re-execution on the raw input. getSnapshot must
103
+ // return a cached reference that only changes when the store
104
+ // changes; re-running a fresh-object selector on every call (and
105
+ // relying solely on equalityFn) would return a new reference
106
+ // each call under the default Object.is and loop forever. When
107
+ // the input is unchanged we return the cached selection without
108
+ // re-running the selector — mirroring React's own
109
+ // useSyncExternalStoreWithSelector, which gates on snapshot
110
+ // identity (here the store's reads are not stable references, so
111
+ // we shallow-compare the input instead).
112
+ if (inputsEqual(memoizedInput, input)) {
113
+ return memoized;
114
+ }
115
+ memoizedInput = input;
116
+ const next = selector(...input);
117
+ if (equalityFn(memoized, next)) {
118
+ return memoized;
119
+ }
120
+ memoized = next;
121
+ return next;
122
+ };
123
+ }, [store, selector, equalityFn, deps, inst]);
124
+ const subscribe = useCallback((onStoreChange) => {
125
+ const listener = (names) => {
126
+ const currentDeps = depsRef.current;
127
+ if (currentDeps
128
+ && !names.some((n) => currentDeps.indexOf(n) !== -1)) {
129
+ return;
130
+ }
131
+ onStoreChange();
132
+ };
133
+ store.control(ChangeEventName, listener);
134
+ return () => {
135
+ store.removeControl(ChangeEventName, listener);
136
+ };
137
+ }, [store]);
138
+ const value = useSyncExternalStore(subscribe, getSelection, getSelection);
139
+ useEffect(() => {
140
+ inst.hasValue = true;
141
+ inst.value = value;
142
+ }, [value, inst]);
143
+ return value;
144
+ }
@@ -1,3 +1,3 @@
1
- import { KeyOf } from "../lib/types";
2
- import { BaseStore } from "../store";
1
+ import { KeyOf } from "../lib/types.js";
2
+ import { BaseStore } from "../store.js";
3
3
  export declare function useStoreState<TStore extends BaseStore, TKey extends KeyOf<TStore["__type"]["propTypes"]>>(store: TStore, key: TKey): readonly [TStore["__type"]["propTypes"][TKey], (value: TStore["__type"]["propTypes"][TKey] | ((previousValue?: TStore["__type"]["propTypes"][TKey]) => TStore["__type"]["propTypes"][TKey])) => void];
@@ -1,30 +1,35 @@
1
- import { useCallback, useEffect, useRef, useState } from "react";
1
+ import { useCallback, useSyncExternalStore } from "react";
2
2
  export function useStoreState(store, key) {
3
- const [value, setValue] = useState(store.get(key));
4
- const storeRef = useRef(store);
5
- const keyRef = useRef(key);
6
- const onChange = useCallback((value) => {
7
- setValue(value);
8
- }, []);
3
+ const subscribe = useCallback((onStoreChange) => {
4
+ const listener = () => {
5
+ onStoreChange();
6
+ };
7
+ store.onChange(key, listener);
8
+ return () => {
9
+ store.removeOnChange(key, listener);
10
+ };
11
+ }, [store, key]);
12
+ const getSnapshot = useCallback(
13
+ // getSnapshot can run for a still-mounted component after the store is
14
+ // destroyed (e.g. a provider torn down first), and must not throw out of
15
+ // render. On a destroyed store read via getData() (returns {} without
16
+ // asserting) instead of get() (which throws). Mirrors useStoreSelector.
17
+ () => (store.isDestroyed()
18
+ ? store.getData()[key]
19
+ : store.get(key)), [store, key]);
20
+ const value = useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
9
21
  const setter = useCallback((value) => {
10
22
  if (typeof value === "function") {
11
- storeRef.current.set(keyRef.current, value(storeRef.current.get(keyRef.current)));
23
+ // The cast is required by tsc (the typeof-narrowed `value` is
24
+ // `Setter | (ValueType & Function)`, not all callable), even
25
+ // though no-unnecessary-type-assertion disagrees on this TS
26
+ // version.
27
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
28
+ store.set(key, value(store.get(key)));
12
29
  }
13
30
  else {
14
- storeRef.current.set(keyRef.current, value);
31
+ store.set(key, value);
15
32
  }
16
- }, []);
17
- useEffect(() => {
18
- return () => {
19
- storeRef.current.removeOnChange(keyRef.current, onChange);
20
- };
21
- }, []);
22
- useEffect(() => {
23
- storeRef.current.removeOnChange(keyRef.current, onChange);
24
- storeRef.current = store;
25
- keyRef.current = key;
26
- storeRef.current.onChange(keyRef.current, onChange);
27
- setValue(store.get(key));
28
33
  }, [store, key]);
29
34
  return [value, setter];
30
35
  }
package/dist/react.d.ts CHANGED
@@ -1,13 +1,16 @@
1
- export * from "./react/ErrorBoundary";
2
- export * from "./react/useAction";
3
- export * from "./react/useActionBus";
4
- export * from "./react/useActionMap";
5
- export * from "./react/useEvent";
6
- export * from "./react/useEventBus";
7
- export * from "./react/useListenToAction";
8
- export * from "./react/useListenToActionBus";
9
- export * from "./react/useListenToEvent";
10
- export * from "./react/useListenToEventBus";
11
- export * from "./react/useListenToStoreChanges";
12
- export * from "./react/useStore";
13
- export * from "./react/useStoreState";
1
+ export * from "./react/ErrorBoundary.js";
2
+ export * from "./react/useAction.js";
3
+ export * from "./react/useActionBus.js";
4
+ export * from "./react/useActionBusStatus.js";
5
+ export * from "./react/useAsyncAction.js";
6
+ export * from "./react/useActionMap.js";
7
+ export * from "./react/useEvent.js";
8
+ export * from "./react/useEventBus.js";
9
+ export * from "./react/useListenToAction.js";
10
+ export * from "./react/useListenToActionBus.js";
11
+ export * from "./react/useListenToEvent.js";
12
+ export * from "./react/useListenToEventBus.js";
13
+ export * from "./react/useListenToStoreChanges.js";
14
+ export * from "./react/useStore.js";
15
+ export * from "./react/useStoreSelector.js";
16
+ export * from "./react/useStoreState.js";
package/dist/react.js CHANGED
@@ -1,13 +1,16 @@
1
- export * from "./react/ErrorBoundary";
2
- export * from "./react/useAction";
3
- export * from "./react/useActionBus";
4
- export * from "./react/useActionMap";
5
- export * from "./react/useEvent";
6
- export * from "./react/useEventBus";
7
- export * from "./react/useListenToAction";
8
- export * from "./react/useListenToActionBus";
9
- export * from "./react/useListenToEvent";
10
- export * from "./react/useListenToEventBus";
11
- export * from "./react/useListenToStoreChanges";
12
- export * from "./react/useStore";
13
- export * from "./react/useStoreState";
1
+ export * from "./react/ErrorBoundary.js";
2
+ export * from "./react/useAction.js";
3
+ export * from "./react/useActionBus.js";
4
+ export * from "./react/useActionBusStatus.js";
5
+ export * from "./react/useAsyncAction.js";
6
+ export * from "./react/useActionMap.js";
7
+ export * from "./react/useEvent.js";
8
+ export * from "./react/useEventBus.js";
9
+ export * from "./react/useListenToAction.js";
10
+ export * from "./react/useListenToActionBus.js";
11
+ export * from "./react/useListenToEvent.js";
12
+ export * from "./react/useListenToEventBus.js";
13
+ export * from "./react/useListenToStoreChanges.js";
14
+ export * from "./react/useStore.js";
15
+ export * from "./react/useStoreSelector.js";
16
+ export * from "./react/useStoreState.js";