@okyrychenko-dev/react-action-guard 0.7.0 → 1.0.1

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
@@ -4,25 +4,18 @@
4
4
  [![npm downloads](https://img.shields.io/npm/dm/@okyrychenko-dev/react-action-guard.svg)](https://www.npmjs.com/package/@okyrychenko-dev/react-action-guard)
5
5
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
6
6
 
7
- > Elegant UI blocking management for React applications with priorities, scopes, and automatic cleanup
8
-
9
- ## Features
10
-
11
- - Priority-based blocking system
12
- - Scoped blocking (global, specific areas, or multiple scopes)
13
- - Automatic cleanup on unmount
14
- - **Timeout mechanism** - auto-remove blockers after specified time
15
- - **Provider pattern** - isolated stores for SSR, testing, and micro-frontends
16
- - Advanced hooks for different use cases:
17
- - Confirmable blockers with custom dialogs
18
- - Scheduled blocking for maintenance windows
19
- - Conditional blocking based on application state
20
- - Async action wrapping with timeout support
21
- - Advanced middleware system for analytics, logging, and performance monitoring
22
- - TypeScript support with full type safety (including optional type-safe scopes)
23
- - Built on Zustand for efficient state management
24
- - Tree-shakeable - import only what you need
25
- - Hooks-based API
7
+ > Coordinate UI blocking across async actions and competing interactions in React
8
+
9
+ `react-action-guard` helps you prevent duplicate submits, conflicting actions, and stale UI interactions without scattering local `isLoading` flags across components.
10
+
11
+ ## Why Use It
12
+
13
+ - Wrap async work with `useAsyncAction` and get blocker cleanup automatically
14
+ - Coordinate blocking by `scope` across unrelated components
15
+ - Inspect current blockers with `useIsBlocked` and `useBlockingInfo`
16
+ - Isolate state per provider for SSR, tests, and micro-frontends
17
+ - Extend lifecycle events with optional middleware
18
+ - Add advanced hooks for scheduled, confirmable, or conditional flows when needed
26
19
 
27
20
  ## Installation
28
21
 
@@ -39,18 +32,52 @@ This package requires the following peer dependencies:
39
32
  - [React](https://react.dev/) ^18.0.0 || ^19.0.0
40
33
  - [Zustand](https://zustand-demo.pmnd.rs/) ^5.0.0 - State management library
41
34
 
35
+ ## 1.0 Migration
36
+
37
+ `1.0.0` removes public re-exports of internal `react-zustand-toolkit` helpers from the root package.
38
+
39
+ 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.
40
+
42
41
  ## Quick Start
43
42
 
44
43
  ```jsx
45
- import { useBlocker, useIsBlocked } from "@okyrychenko-dev/react-action-guard";
44
+ import { useAsyncAction, useIsBlocked } from "@okyrychenko-dev/react-action-guard";
46
45
 
47
- function MyComponent() {
48
- const isBlocked = useIsBlocked("my-scope");
46
+ function SaveButton() {
47
+ const isSaving = useIsBlocked("form");
48
+ const runSave = useAsyncAction("save-profile", "form");
49
49
 
50
- return <button disabled={isBlocked}>Click me</button>;
50
+ const handleClick = async () => {
51
+ await runSave(async () => {
52
+ await api.saveProfile();
53
+ });
54
+ };
55
+
56
+ return <button onClick={handleClick} disabled={isSaving}>Save</button>;
51
57
  }
52
58
  ```
53
59
 
60
+ ## Core Concepts
61
+
62
+ - `scope` lets you coordinate blocking across components like `"form"`, `"navigation"`, or `"checkout"`
63
+ - `useAsyncAction` is the fastest path for async workflows
64
+ - `useBlocker` is the lower-level hook when you already have your own boolean state
65
+ - `UIBlockingProvider` gives you isolated state instead of the default global store
66
+
67
+ ## Core Use Cases
68
+
69
+ ### Prevent duplicate async actions
70
+
71
+ Use `useAsyncAction` when a button, mutation, or workflow should block while work is in flight.
72
+
73
+ ### Coordinate multiple components
74
+
75
+ Use shared scopes when one component starts work and another component should react by disabling UI or showing blocker details.
76
+
77
+ ### Isolate blocking domains
78
+
79
+ Use `UIBlockingProvider` when each request, test, or micro-frontend should get its own store instance.
80
+
54
81
  ## Documentation
55
82
 
56
83
  📚 **Interactive Storybook Documentation** - Run locally to explore live examples and detailed guides for all hooks
@@ -65,7 +92,16 @@ npm run storybook
65
92
 
66
93
  ### Hooks
67
94
 
68
- #### `useBlocker(blockerId, config, isActive)`
95
+ #### Core hooks
96
+
97
+ Start with these first:
98
+
99
+ - `useAsyncAction`
100
+ - `useBlocker`
101
+ - `useIsBlocked`
102
+ - `useBlockingInfo`
103
+
104
+ #### `useBlocker(blockerId, config, isActive?)`
69
105
 
70
106
  Automatically adds a blocker when the component mounts and removes it on unmount.
71
107
 
@@ -80,6 +116,8 @@ Automatically adds a blocker when the component mounts and removes it on unmount
80
116
  - `onTimeout?: (blockerId: string) => void` - Callback when auto-removed
81
117
  - `isActive?: boolean` - Whether the blocker is active (default: true)
82
118
 
119
+ When `isActive` is `true`, changing `config` updates the existing blocker via `updateBlocker` without requiring unmount/remount.
120
+
83
121
  **Example:**
84
122
 
85
123
  ```jsx
@@ -227,9 +265,18 @@ function ApiComponent() {
227
265
  }
228
266
  ```
229
267
 
268
+ #### Advanced hooks
269
+
270
+ These hooks are useful, but they are not the main onboarding path for most apps:
271
+
272
+ - `useConfirmableBlocker` for confirmation flows
273
+ - `useScheduledBlocker` for time-window blocking
274
+ - `useConditionalBlocker` for polling-based conditional synchronization
275
+
230
276
  #### `useConfirmableBlocker(blockerId, config)`
231
277
 
232
278
  Creates a confirmable action with UI blocking while the dialog is open or the action is running.
279
+ Use it for advanced confirmation flows after the core hooks are already a good fit.
233
280
 
234
281
  **Parameters:**
235
282
 
@@ -294,6 +341,7 @@ function UnsavedChangesGuard({ discardChanges }) {
294
341
  #### `useScheduledBlocker(blockerId, config)`
295
342
 
296
343
  Blocks UI during a scheduled time period or maintenance window.
344
+ Use it when blocking is driven by time windows rather than user-triggered async work.
297
345
 
298
346
  **Parameters:**
299
347
 
@@ -334,6 +382,7 @@ function MaintenanceWindow() {
334
382
  #### `useConditionalBlocker(blockerId, config)`
335
383
 
336
384
  Periodically checks a condition and blocks/unblocks based on the result.
385
+ Use this when polling is an acceptable tradeoff and the condition is not naturally event-driven.
337
386
 
338
387
  **Parameters:**
339
388
 
@@ -422,9 +471,17 @@ function MicroFrontend() {
422
471
 
423
472
  #### Context Hooks
424
473
 
425
- - `useUIBlockingContext()` - Get store from context (throws if outside provider)
474
+ - `useUIBlockingContext()` - Get store API from context (throws if outside provider)
475
+ - `useOptionalUIBlockingContext()` - Get store API from context or `null` outside provider
426
476
  - `useIsInsideUIBlockingProvider()` - Check if inside a provider
427
477
  - `useUIBlockingStoreFromContext(selector)` - Select state from context store
478
+ - `useResolvedStoreApi()` - Resolve to provider store API or global store API
479
+ - `useResolvedValue(selector)` - Resolve to provider/global store and select with shallow comparison
480
+
481
+ Legacy aliases remain available for backward compatibility:
482
+ - `useOptionalContext()` -> `useOptionalUIBlockingContext()`
483
+ - `useResolvedStore()` -> `useResolvedStoreApi()`
484
+ - `useResolvedStoreWithSelector()` -> `useResolvedValue()`
428
485
 
429
486
  ### Store
430
487
 
@@ -505,18 +562,11 @@ uiBlockingStoreApi.getState().addBlocker("server-call", {
505
562
  });
506
563
  ```
507
564
 
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
565
  ## Middleware System
518
566
 
519
- The library includes a powerful middleware system that allows you to hook into blocker lifecycle events for analytics, logging, and performance monitoring.
567
+ Middleware is optional. Use it when you need analytics, logging, or performance visibility around blocker lifecycle events.
568
+
569
+ Most applications can start without middleware and add it later if blocker observability becomes important.
520
570
 
521
571
  ### Middleware Actions
522
572
 
@@ -544,6 +594,8 @@ Middleware receives events for the following actions:
544
594
 
545
595
  ### Built-in Middleware
546
596
 
597
+ These are extensions around the blocker lifecycle, not the primary onboarding path for the library.
598
+
547
599
  #### Analytics Middleware
548
600
 
549
601
  Track blocker events with your analytics provider (Google Analytics, Mixpanel, Amplitude, or custom).
@@ -604,6 +656,9 @@ configureMiddleware([
604
656
  ```
605
657
 
606
658
  Note: `configureMiddleware` registers middleware on the global store. If you use `UIBlockingProvider`, register middleware via the provider's `middlewares` prop instead.
659
+ Each `configureMiddleware(...)` call replaces previously configured global middlewares (`middleware-*`) and keeps provider-level middlewares untouched.
660
+
661
+ Analytics middleware is SSR-safe: in non-browser environments it becomes a no-op for `ga`, `mixpanel`, and `amplitude` providers.
607
662
 
608
663
  ### Custom Middleware
609
664
 
@@ -635,7 +690,7 @@ configureMiddleware([myCustomMiddleware]);
635
690
 
636
691
  ### Combining Middleware
637
692
 
638
- You can combine multiple middleware for comprehensive monitoring:
693
+ You can combine multiple middleware when you need broader observability:
639
694
 
640
695
  ```jsx
641
696
  import {
@@ -730,6 +785,8 @@ useBlocker("save", { scope: "typo" }); // Type error
730
785
 
731
786
  ## Use Cases
732
787
 
788
+ The first four examples below are the most representative starting points.
789
+
733
790
  ### Loading States
734
791
 
735
792
  ```jsx
@@ -773,7 +830,7 @@ function UserForm() {
773
830
  }
774
831
  ```
775
832
 
776
- ### Unsaved Changes Protection
833
+ ### Advanced: Unsaved Changes Protection
777
834
 
778
835
  ```jsx
779
836
  import { useConfirmableBlocker } from "@okyrychenko-dev/react-action-guard";
@@ -870,7 +927,7 @@ function MultiStepWizard() {
870
927
  }
871
928
  ```
872
929
 
873
- ### Scheduled Maintenance Window
930
+ ### Advanced: Scheduled Maintenance Window
874
931
 
875
932
  ```jsx
876
933
  import { useScheduledBlocker } from "@okyrychenko-dev/react-action-guard";
@@ -897,7 +954,7 @@ function App() {
897
954
  }
898
955
  ```
899
956
 
900
- ### Dynamic Blocking Based on State
957
+ ### Advanced: Dynamic Blocking Based on State
901
958
 
902
959
  ```jsx
903
960
  import { useConditionalBlocker } from "@okyrychenko-dev/react-action-guard";
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;