@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 +96 -39
- 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
|
@@ -4,25 +4,18 @@
|
|
|
4
4
|
[](https://www.npmjs.com/package/@okyrychenko-dev/react-action-guard)
|
|
5
5
|
[](https://opensource.org/licenses/MIT)
|
|
6
6
|
|
|
7
|
-
>
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
|
|
18
|
-
|
|
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 {
|
|
44
|
+
import { useAsyncAction, useIsBlocked } from "@okyrychenko-dev/react-action-guard";
|
|
46
45
|
|
|
47
|
-
function
|
|
48
|
-
const
|
|
46
|
+
function SaveButton() {
|
|
47
|
+
const isSaving = useIsBlocked("form");
|
|
48
|
+
const runSave = useAsyncAction("save-profile", "form");
|
|
49
49
|
|
|
50
|
-
|
|
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
|
-
####
|
|
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
|
-
|
|
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
|
|
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/
|
|
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;
|