@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 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/hooks/useAsyncAction/useAsyncAction.ts
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 [name, middleware] of middlewares) {
59
+ for (const [, middleware] of middlewares) {
60
60
  try {
61
61
  await middleware(context);
62
- } catch (error) {
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
- blockers.push({ id, ...blocker });
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
- useContext: useUIBlockingContext,
454
- useContextStore: useUIBlockingStoreFromContext,
453
+ useContextStoreApi: useUIBlockingContext,
454
+ useContextStore: baseUseUIBlockingStoreFromContext,
455
455
  useIsInsideProvider: useIsInsideUIBlockingProvider,
456
- useOptionalContext
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 handleStoreCreate = (store) => {
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
- onStoreCreate: handleStoreCreate,
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 { useStore: useUIBlockingStore, useStoreApi: uiBlockingStoreApi } = reactZustandToolkit.createShallowStore(middleware.devtools(createUIBlockingActions, devtoolsConfig));
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 useOptionalContext();
506
+ return useOptionalUIBlockingContext();
492
507
  }
493
- function useResolvedStore() {
508
+ function useResolvedStoreApi() {
494
509
  const contextStore = useOptionalContextStore();
495
510
  return contextStore ?? uiBlockingStoreApi;
496
511
  }
497
- function useResolvedStoreWithSelector(selector) {
498
- const store = useResolvedStore();
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 } = useResolvedStoreWithSelector((state) => ({
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 } = useResolvedStoreWithSelector((state) => ({
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 = useResolvedStore();
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 } = useResolvedStoreWithSelector((state) => ({
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 useResolvedStoreWithSelector((state) => state.isBlocked(scope));
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 } = useResolvedStoreWithSelector((state) => ({
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
- console.warn(`[UIBlocking] Schedule is in the past for blocker "${blockerId}"`);
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
- if (window.gtag) {
785
- window.gtag("event", `ui_blocking_${action}`, eventData);
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
- if (window.mixpanel) {
816
+ const analyticsWindow = getAnalyticsWindow();
817
+ if (analyticsWindow?.mixpanel) {
790
818
  const eventName = `UI Blocking ${action.split("_").map(capitalizeFirst).join(" ")}`;
791
- window.mixpanel.track(eventName, eventData);
819
+ analyticsWindow.mixpanel.track(eventName, eventData);
792
820
  }
793
821
  }
794
822
  function trackAmplitude(action, eventData) {
795
- if (window.amplitude) {
823
+ const analyticsWindow = getAnalyticsWindow();
824
+ if (analyticsWindow?.amplitude) {
796
825
  const eventName = `UI Blocking ${action.split("_").map(capitalizeFirst).join(" ")}`;
797
- window.amplitude.track(eventName, eventData);
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 = `middleware-${index.toString()}`;
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
- Object.defineProperty(exports, "createShallowStore", {
956
- enumerable: true,
957
- get: function () { return reactZustandToolkit.createShallowStore; }
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;