@okyrychenko-dev/react-action-guard 0.7.0 → 1.0.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.
- package/README.md +21 -11
- package/dist/index.cjs +92 -47
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +437 -402
- package/dist/index.d.ts +437 -402
- package/dist/index.js +87 -45
- package/dist/index.js.map +1 -1
- package/package.json +10 -9
package/README.md
CHANGED
|
@@ -39,6 +39,12 @@ This package requires the following peer dependencies:
|
|
|
39
39
|
- [React](https://react.dev/) ^18.0.0 || ^19.0.0
|
|
40
40
|
- [Zustand](https://zustand-demo.pmnd.rs/) ^5.0.0 - State management library
|
|
41
41
|
|
|
42
|
+
## 1.0 Migration
|
|
43
|
+
|
|
44
|
+
`1.0.0` removes public re-exports of internal `react-zustand-toolkit` helpers from the root package.
|
|
45
|
+
|
|
46
|
+
If you previously imported `createShallowStore`, `createStoreToolkit`, `createStoreProvider`, or `createResolvedStoreHooks` from `@okyrychenko-dev/react-action-guard`, import them directly from `@okyrychenko-dev/react-zustand-toolkit` instead.
|
|
47
|
+
|
|
42
48
|
## Quick Start
|
|
43
49
|
|
|
44
50
|
```jsx
|
|
@@ -65,7 +71,7 @@ npm run storybook
|
|
|
65
71
|
|
|
66
72
|
### Hooks
|
|
67
73
|
|
|
68
|
-
#### `useBlocker(blockerId, config, isActive)`
|
|
74
|
+
#### `useBlocker(blockerId, config, isActive?)`
|
|
69
75
|
|
|
70
76
|
Automatically adds a blocker when the component mounts and removes it on unmount.
|
|
71
77
|
|
|
@@ -80,6 +86,8 @@ Automatically adds a blocker when the component mounts and removes it on unmount
|
|
|
80
86
|
- `onTimeout?: (blockerId: string) => void` - Callback when auto-removed
|
|
81
87
|
- `isActive?: boolean` - Whether the blocker is active (default: true)
|
|
82
88
|
|
|
89
|
+
When `isActive` is `true`, changing `config` updates the existing blocker via `updateBlocker` without requiring unmount/remount.
|
|
90
|
+
|
|
83
91
|
**Example:**
|
|
84
92
|
|
|
85
93
|
```jsx
|
|
@@ -422,9 +430,17 @@ function MicroFrontend() {
|
|
|
422
430
|
|
|
423
431
|
#### Context Hooks
|
|
424
432
|
|
|
425
|
-
- `useUIBlockingContext()` - Get store from context (throws if outside provider)
|
|
433
|
+
- `useUIBlockingContext()` - Get store API from context (throws if outside provider)
|
|
434
|
+
- `useOptionalUIBlockingContext()` - Get store API from context or `null` outside provider
|
|
426
435
|
- `useIsInsideUIBlockingProvider()` - Check if inside a provider
|
|
427
436
|
- `useUIBlockingStoreFromContext(selector)` - Select state from context store
|
|
437
|
+
- `useResolvedStoreApi()` - Resolve to provider store API or global store API
|
|
438
|
+
- `useResolvedValue(selector)` - Resolve to provider/global store and select with shallow comparison
|
|
439
|
+
|
|
440
|
+
Legacy aliases remain available for backward compatibility:
|
|
441
|
+
- `useOptionalContext()` -> `useOptionalUIBlockingContext()`
|
|
442
|
+
- `useResolvedStore()` -> `useResolvedStoreApi()`
|
|
443
|
+
- `useResolvedStoreWithSelector()` -> `useResolvedValue()`
|
|
428
444
|
|
|
429
445
|
### Store
|
|
430
446
|
|
|
@@ -505,15 +521,6 @@ uiBlockingStoreApi.getState().addBlocker("server-call", {
|
|
|
505
521
|
});
|
|
506
522
|
```
|
|
507
523
|
|
|
508
|
-
### Store Toolkit Helpers
|
|
509
|
-
|
|
510
|
-
Advanced store helpers are re-exported from the internal toolkit for custom setups:
|
|
511
|
-
|
|
512
|
-
- `createShallowStore`
|
|
513
|
-
- `createStoreToolkit`
|
|
514
|
-
- `createStoreProvider`
|
|
515
|
-
- `createResolvedStoreHooks`
|
|
516
|
-
|
|
517
524
|
## Middleware System
|
|
518
525
|
|
|
519
526
|
The library includes a powerful middleware system that allows you to hook into blocker lifecycle events for analytics, logging, and performance monitoring.
|
|
@@ -604,6 +611,9 @@ configureMiddleware([
|
|
|
604
611
|
```
|
|
605
612
|
|
|
606
613
|
Note: `configureMiddleware` registers middleware on the global store. If you use `UIBlockingProvider`, register middleware via the provider's `middlewares` prop instead.
|
|
614
|
+
Each `configureMiddleware(...)` call replaces previously configured global middlewares (`middleware-*`) and keeps provider-level middlewares untouched.
|
|
615
|
+
|
|
616
|
+
Analytics middleware is SSR-safe: in non-browser environments it becomes a no-op for `ga`, `mixpanel`, and `amplitude` providers.
|
|
607
617
|
|
|
608
618
|
### Custom Middleware
|
|
609
619
|
|
package/dist/index.cjs
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var react = require('react');
|
|
4
3
|
var reactZustandToolkit = require('@okyrychenko-dev/react-zustand-toolkit');
|
|
5
4
|
var zustand = require('zustand');
|
|
6
5
|
require('zustand/react/shallow');
|
|
7
6
|
var jsxRuntime = require('react/jsx-runtime');
|
|
8
7
|
var shallow = require('zustand/shallow');
|
|
9
8
|
var middleware = require('zustand/middleware');
|
|
9
|
+
var react = require('react');
|
|
10
10
|
|
|
11
|
-
// src/
|
|
11
|
+
// src/context/UIBlockingContext.tsx
|
|
12
12
|
|
|
13
13
|
// src/store/uiBlockingStore.constants.ts
|
|
14
14
|
var DEFAULT_SCOPE = "global";
|
|
@@ -56,11 +56,10 @@ var createUIBlockingActions = (set, get) => ({
|
|
|
56
56
|
},
|
|
57
57
|
runMiddlewares: async (context) => {
|
|
58
58
|
const { middlewares } = get();
|
|
59
|
-
for (const [
|
|
59
|
+
for (const [, middleware] of middlewares) {
|
|
60
60
|
try {
|
|
61
61
|
await middleware(context);
|
|
62
|
-
} catch
|
|
63
|
-
console.error(`[UIBlocking] Middleware "${name}" error:`, error);
|
|
62
|
+
} catch {
|
|
64
63
|
}
|
|
65
64
|
}
|
|
66
65
|
},
|
|
@@ -354,7 +353,8 @@ var createUIBlockingActions = (set, get) => ({
|
|
|
354
353
|
for (const [id, blocker] of activeBlockers) {
|
|
355
354
|
const blockerScopes = normalizeScopeToArray(blocker.scope);
|
|
356
355
|
if (blocker.scope === DEFAULT_SCOPE || blockerScopes.includes(scope)) {
|
|
357
|
-
|
|
356
|
+
const { timeoutId: _timeoutId, ...publicBlocker } = blocker;
|
|
357
|
+
blockers.push({ id, ...publicBlocker });
|
|
358
358
|
}
|
|
359
359
|
}
|
|
360
360
|
return blockers.sort((a, b) => b.priority - a.priority);
|
|
@@ -450,18 +450,24 @@ var createUIBlockingActions = (set, get) => ({
|
|
|
450
450
|
});
|
|
451
451
|
var {
|
|
452
452
|
Provider: BaseUIBlockingProvider,
|
|
453
|
-
|
|
454
|
-
useContextStore:
|
|
453
|
+
useContextStoreApi: useUIBlockingContext,
|
|
454
|
+
useContextStore: baseUseUIBlockingStoreFromContext,
|
|
455
455
|
useIsInsideProvider: useIsInsideUIBlockingProvider,
|
|
456
|
-
|
|
456
|
+
useContextStoreOptional: useOptionalUIBlockingContext
|
|
457
457
|
} = reactZustandToolkit.createStoreProvider(createUIBlockingActions, "UIBlocking");
|
|
458
|
+
function useUIBlockingStoreFromContext(selector, equalityFn) {
|
|
459
|
+
if (selector) {
|
|
460
|
+
return baseUseUIBlockingStoreFromContext(selector, equalityFn);
|
|
461
|
+
}
|
|
462
|
+
return baseUseUIBlockingStoreFromContext();
|
|
463
|
+
}
|
|
458
464
|
function UIBlockingProvider({
|
|
459
465
|
children,
|
|
460
466
|
enableDevtools = process.env.NODE_ENV === "development",
|
|
461
467
|
devtoolsName = "UIBlocking",
|
|
462
468
|
middlewares = []
|
|
463
469
|
}) {
|
|
464
|
-
const
|
|
470
|
+
const handleStoreInit = (store) => {
|
|
465
471
|
middlewares.forEach((mw, index) => {
|
|
466
472
|
store.getState().registerMiddleware(`provider-middleware-${index.toString()}`, mw);
|
|
467
473
|
});
|
|
@@ -471,7 +477,7 @@ function UIBlockingProvider({
|
|
|
471
477
|
{
|
|
472
478
|
enableDevtools,
|
|
473
479
|
devtoolsName,
|
|
474
|
-
|
|
480
|
+
onStoreInit: handleStoreInit,
|
|
475
481
|
children
|
|
476
482
|
}
|
|
477
483
|
);
|
|
@@ -484,24 +490,31 @@ var devtoolsConfig = {
|
|
|
484
490
|
};
|
|
485
491
|
|
|
486
492
|
// src/store/uiBlockingStore.store.ts
|
|
487
|
-
var
|
|
493
|
+
var storeBindings = reactZustandToolkit.createShallowStore(
|
|
494
|
+
middleware.devtools(createUIBlockingActions, devtoolsConfig)
|
|
495
|
+
);
|
|
496
|
+
var uiBlockingStoreApi = storeBindings.useStoreApi;
|
|
497
|
+
function useUIBlockingStore(selector, equalityFn) {
|
|
498
|
+
if (selector) {
|
|
499
|
+
return storeBindings.useStore(selector, equalityFn);
|
|
500
|
+
}
|
|
501
|
+
return storeBindings.useStore();
|
|
502
|
+
}
|
|
488
503
|
|
|
489
504
|
// src/context/useResolvedStore.ts
|
|
490
505
|
function useOptionalContextStore() {
|
|
491
|
-
return
|
|
506
|
+
return useOptionalUIBlockingContext();
|
|
492
507
|
}
|
|
493
|
-
function
|
|
508
|
+
function useResolvedStoreApi() {
|
|
494
509
|
const contextStore = useOptionalContextStore();
|
|
495
510
|
return contextStore ?? uiBlockingStoreApi;
|
|
496
511
|
}
|
|
497
|
-
function
|
|
498
|
-
const store =
|
|
512
|
+
function useResolvedValue(selector) {
|
|
513
|
+
const store = useResolvedStoreApi();
|
|
499
514
|
return zustand.useStore(store, shallow.useShallow(selector));
|
|
500
515
|
}
|
|
501
|
-
|
|
502
|
-
// src/hooks/useAsyncAction/useAsyncAction.ts
|
|
503
516
|
function useAsyncAction(actionId, scope, options) {
|
|
504
|
-
const { addBlocker, removeBlocker } =
|
|
517
|
+
const { addBlocker, removeBlocker } = useResolvedValue((state) => ({
|
|
505
518
|
addBlocker: state.addBlocker,
|
|
506
519
|
removeBlocker: state.removeBlocker
|
|
507
520
|
}));
|
|
@@ -528,19 +541,33 @@ function useAsyncAction(actionId, scope, options) {
|
|
|
528
541
|
return executeWithBlocking;
|
|
529
542
|
}
|
|
530
543
|
function useBlocker(blockerId, config, isActive = true) {
|
|
531
|
-
const { addBlocker, removeBlocker } =
|
|
544
|
+
const { addBlocker, removeBlocker, updateBlocker } = useResolvedValue((state) => ({
|
|
532
545
|
addBlocker: state.addBlocker,
|
|
533
|
-
removeBlocker: state.removeBlocker
|
|
546
|
+
removeBlocker: state.removeBlocker,
|
|
547
|
+
updateBlocker: state.updateBlocker
|
|
534
548
|
}));
|
|
549
|
+
const lastConfigRef = react.useRef(null);
|
|
535
550
|
react.useEffect(() => {
|
|
536
551
|
if (!isActive || !blockerId) {
|
|
537
552
|
return;
|
|
538
553
|
}
|
|
539
554
|
addBlocker(blockerId, config);
|
|
555
|
+
lastConfigRef.current = config;
|
|
540
556
|
return () => {
|
|
557
|
+
lastConfigRef.current = null;
|
|
541
558
|
removeBlocker(blockerId);
|
|
542
559
|
};
|
|
543
560
|
}, [blockerId, isActive, addBlocker, removeBlocker]);
|
|
561
|
+
react.useEffect(() => {
|
|
562
|
+
if (!isActive || !blockerId) {
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
if (lastConfigRef.current === config) {
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
updateBlocker(blockerId, config);
|
|
569
|
+
lastConfigRef.current = config;
|
|
570
|
+
}, [blockerId, config, isActive, updateBlocker]);
|
|
544
571
|
}
|
|
545
572
|
|
|
546
573
|
// src/hooks/useBlocker/useBlocker.utils.ts
|
|
@@ -552,7 +579,7 @@ function createBlockerConfig(config) {
|
|
|
552
579
|
};
|
|
553
580
|
}
|
|
554
581
|
function useBlockingInfo(scope = DEFAULT_SCOPE) {
|
|
555
|
-
const store =
|
|
582
|
+
const store = useResolvedStoreApi();
|
|
556
583
|
const activeBlockers = zustand.useStore(store, (state) => state.activeBlockers);
|
|
557
584
|
return react.useMemo(() => {
|
|
558
585
|
return store.getState().getBlockingInfo(scope);
|
|
@@ -568,7 +595,7 @@ function useConfigRef(config) {
|
|
|
568
595
|
|
|
569
596
|
// src/hooks/useConditionalBlocker/useConditionalBlocker.ts
|
|
570
597
|
function useConditionalBlocker(blockerId, config) {
|
|
571
|
-
const { addBlocker, removeBlocker } =
|
|
598
|
+
const { addBlocker, removeBlocker } = useResolvedValue((state) => ({
|
|
572
599
|
addBlocker: state.addBlocker,
|
|
573
600
|
removeBlocker: state.removeBlocker
|
|
574
601
|
}));
|
|
@@ -623,9 +650,6 @@ function useConfirmableBlocker(blockerId, config) {
|
|
|
623
650
|
setIsExecuting(true);
|
|
624
651
|
try {
|
|
625
652
|
await config.onConfirm();
|
|
626
|
-
} catch (error) {
|
|
627
|
-
console.error("[UIBlocking] Confirmable action error:", error);
|
|
628
|
-
throw error;
|
|
629
653
|
} finally {
|
|
630
654
|
setIsExecuting(false);
|
|
631
655
|
}
|
|
@@ -651,7 +675,7 @@ function useConfirmableBlocker(blockerId, config) {
|
|
|
651
675
|
|
|
652
676
|
// src/hooks/useIsBlocked/useIsBlocked.ts
|
|
653
677
|
function useIsBlocked(scope) {
|
|
654
|
-
return
|
|
678
|
+
return useResolvedValue((state) => state.isBlocked(scope));
|
|
655
679
|
}
|
|
656
680
|
|
|
657
681
|
// src/hooks/useScheduledBlocker/useScheduledBlocker.utils.ts
|
|
@@ -693,7 +717,7 @@ function isScheduleInPast(startTime, endTime, now) {
|
|
|
693
717
|
|
|
694
718
|
// src/hooks/useScheduledBlocker/useScheduledBlocker.ts
|
|
695
719
|
function useScheduledBlocker(blockerId, config) {
|
|
696
|
-
const { addBlocker, removeBlocker } =
|
|
720
|
+
const { addBlocker, removeBlocker } = useResolvedValue((state) => ({
|
|
697
721
|
addBlocker: state.addBlocker,
|
|
698
722
|
removeBlocker: state.removeBlocker
|
|
699
723
|
}));
|
|
@@ -714,7 +738,6 @@ function useScheduledBlocker(blockerId, config) {
|
|
|
714
738
|
const startTime = parseDate(currentConfig.schedule.start);
|
|
715
739
|
const now = Date.now();
|
|
716
740
|
if (!isValidTimestamp(startTime)) {
|
|
717
|
-
console.error(`[UIBlocking] Invalid start time for blocker "${blockerId}"`);
|
|
718
741
|
return;
|
|
719
742
|
}
|
|
720
743
|
const scheduleEnd = () => {
|
|
@@ -739,9 +762,6 @@ function useScheduledBlocker(blockerId, config) {
|
|
|
739
762
|
if (startTime > now) {
|
|
740
763
|
const delay = startTime - now;
|
|
741
764
|
if (!isSafeTimeout(delay)) {
|
|
742
|
-
console.warn(
|
|
743
|
-
`[UIBlocking] Schedule delay exceeds maximum timeout (${MAX_TIMEOUT_MS.toString()}ms) for blocker "${blockerId}"`
|
|
744
|
-
);
|
|
745
765
|
return;
|
|
746
766
|
}
|
|
747
767
|
timeoutsRef.current.start = setTimeout(startBlocking, delay);
|
|
@@ -750,7 +770,7 @@ function useScheduledBlocker(blockerId, config) {
|
|
|
750
770
|
if (isInBlockingPeriod(startTime, endTime, now)) {
|
|
751
771
|
startBlocking();
|
|
752
772
|
} else if (isScheduleInPast(startTime, endTime, now)) {
|
|
753
|
-
|
|
773
|
+
return;
|
|
754
774
|
}
|
|
755
775
|
}
|
|
756
776
|
return () => {
|
|
@@ -764,6 +784,12 @@ function useScheduledBlocker(blockerId, config) {
|
|
|
764
784
|
function capitalizeFirst(str) {
|
|
765
785
|
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
766
786
|
}
|
|
787
|
+
function getAnalyticsWindow() {
|
|
788
|
+
if (typeof window === "undefined") {
|
|
789
|
+
return void 0;
|
|
790
|
+
}
|
|
791
|
+
return window;
|
|
792
|
+
}
|
|
767
793
|
function buildEventData(context) {
|
|
768
794
|
const eventData = {
|
|
769
795
|
blocker_id: context.blockerId
|
|
@@ -781,20 +807,23 @@ function buildEventData(context) {
|
|
|
781
807
|
return eventData;
|
|
782
808
|
}
|
|
783
809
|
function trackGoogleAnalytics(action, eventData) {
|
|
784
|
-
|
|
785
|
-
|
|
810
|
+
const analyticsWindow = getAnalyticsWindow();
|
|
811
|
+
if (analyticsWindow?.gtag) {
|
|
812
|
+
analyticsWindow.gtag("event", `ui_blocking_${action}`, eventData);
|
|
786
813
|
}
|
|
787
814
|
}
|
|
788
815
|
function trackMixpanel(action, eventData) {
|
|
789
|
-
|
|
816
|
+
const analyticsWindow = getAnalyticsWindow();
|
|
817
|
+
if (analyticsWindow?.mixpanel) {
|
|
790
818
|
const eventName = `UI Blocking ${action.split("_").map(capitalizeFirst).join(" ")}`;
|
|
791
|
-
|
|
819
|
+
analyticsWindow.mixpanel.track(eventName, eventData);
|
|
792
820
|
}
|
|
793
821
|
}
|
|
794
822
|
function trackAmplitude(action, eventData) {
|
|
795
|
-
|
|
823
|
+
const analyticsWindow = getAnalyticsWindow();
|
|
824
|
+
if (analyticsWindow?.amplitude) {
|
|
796
825
|
const eventName = `UI Blocking ${action.split("_").map(capitalizeFirst).join(" ")}`;
|
|
797
|
-
|
|
826
|
+
analyticsWindow.amplitude.track(eventName, eventData);
|
|
798
827
|
}
|
|
799
828
|
}
|
|
800
829
|
function trackCustom(trackFn, action, eventData) {
|
|
@@ -831,10 +860,16 @@ function createAnalyticsMiddleware(config) {
|
|
|
831
860
|
}
|
|
832
861
|
|
|
833
862
|
// src/middleware/configureMiddleware.ts
|
|
863
|
+
var CONFIGURE_MIDDLEWARE_PREFIX = "middleware-";
|
|
834
864
|
function configureMiddleware(middlewares) {
|
|
835
865
|
const store = uiBlockingStoreApi.getState();
|
|
866
|
+
store.middlewares.forEach((_, name) => {
|
|
867
|
+
if (name.startsWith(CONFIGURE_MIDDLEWARE_PREFIX)) {
|
|
868
|
+
store.unregisterMiddleware(name);
|
|
869
|
+
}
|
|
870
|
+
});
|
|
836
871
|
middlewares.forEach((middleware, index) => {
|
|
837
|
-
const name =
|
|
872
|
+
const name = `${CONFIGURE_MIDDLEWARE_PREFIX}${index.toString()}`;
|
|
838
873
|
store.registerMiddleware(name, middleware);
|
|
839
874
|
});
|
|
840
875
|
}
|
|
@@ -918,9 +953,6 @@ function handleRemoveAction(context, blockStartTimes, slowBlockThreshold, onSlow
|
|
|
918
953
|
const duration = context.timestamp - startTime;
|
|
919
954
|
blockStartTimes.delete(context.blockerId);
|
|
920
955
|
if (duration >= slowBlockThreshold) {
|
|
921
|
-
console.warn(
|
|
922
|
-
`[UIBlocking] Slow block detected: "${context.blockerId}" took ${duration.toString()}ms`
|
|
923
|
-
);
|
|
924
956
|
onSlowBlock?.(context.blockerId, duration);
|
|
925
957
|
}
|
|
926
958
|
}
|
|
@@ -952,10 +984,17 @@ function createTypedHooks() {
|
|
|
952
984
|
};
|
|
953
985
|
}
|
|
954
986
|
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
}
|
|
987
|
+
// src/index.ts
|
|
988
|
+
function useOptionalContext() {
|
|
989
|
+
return useOptionalUIBlockingContext();
|
|
990
|
+
}
|
|
991
|
+
function useResolvedStore() {
|
|
992
|
+
return useResolvedStoreApi();
|
|
993
|
+
}
|
|
994
|
+
function useResolvedStoreWithSelector(selector) {
|
|
995
|
+
return useResolvedValue(selector);
|
|
996
|
+
}
|
|
997
|
+
|
|
959
998
|
exports.UIBlockingProvider = UIBlockingProvider;
|
|
960
999
|
exports.configureMiddleware = configureMiddleware;
|
|
961
1000
|
exports.createAnalyticsMiddleware = createAnalyticsMiddleware;
|
|
@@ -970,6 +1009,12 @@ exports.useConditionalBlocker = useConditionalBlocker;
|
|
|
970
1009
|
exports.useConfirmableBlocker = useConfirmableBlocker;
|
|
971
1010
|
exports.useIsBlocked = useIsBlocked;
|
|
972
1011
|
exports.useIsInsideUIBlockingProvider = useIsInsideUIBlockingProvider;
|
|
1012
|
+
exports.useOptionalContext = useOptionalContext;
|
|
1013
|
+
exports.useOptionalUIBlockingContext = useOptionalUIBlockingContext;
|
|
1014
|
+
exports.useResolvedStore = useResolvedStore;
|
|
1015
|
+
exports.useResolvedStoreApi = useResolvedStoreApi;
|
|
1016
|
+
exports.useResolvedStoreWithSelector = useResolvedStoreWithSelector;
|
|
1017
|
+
exports.useResolvedValue = useResolvedValue;
|
|
973
1018
|
exports.useScheduledBlocker = useScheduledBlocker;
|
|
974
1019
|
exports.useUIBlockingContext = useUIBlockingContext;
|
|
975
1020
|
exports.useUIBlockingStore = useUIBlockingStore;
|