@okyrychenko-dev/react-action-guard 1.0.0 → 1.0.2
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 +76 -31
- package/dist/index.cjs +1 -11
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +6 -14
- package/dist/index.d.ts +6 -14
- package/dist/index.js +1 -11
- package/dist/index.js.map +1 -1
- package/package.json +5 -4
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
|
|
|
@@ -48,15 +41,43 @@ If you previously imported `createShallowStore`, `createStoreToolkit`, `createSt
|
|
|
48
41
|
## Quick Start
|
|
49
42
|
|
|
50
43
|
```jsx
|
|
51
|
-
import {
|
|
44
|
+
import { useAsyncAction, useIsBlocked } from "@okyrychenko-dev/react-action-guard";
|
|
52
45
|
|
|
53
|
-
function
|
|
54
|
-
const
|
|
46
|
+
function SaveButton() {
|
|
47
|
+
const isSaving = useIsBlocked("form");
|
|
48
|
+
const runSave = useAsyncAction("save-profile", "form");
|
|
49
|
+
|
|
50
|
+
const handleClick = async () => {
|
|
51
|
+
await runSave(async () => {
|
|
52
|
+
await api.saveProfile();
|
|
53
|
+
});
|
|
54
|
+
};
|
|
55
55
|
|
|
56
|
-
return <button disabled={
|
|
56
|
+
return <button onClick={handleClick} disabled={isSaving}>Save</button>;
|
|
57
57
|
}
|
|
58
58
|
```
|
|
59
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
|
+
|
|
60
81
|
## Documentation
|
|
61
82
|
|
|
62
83
|
📚 **Interactive Storybook Documentation** - Run locally to explore live examples and detailed guides for all hooks
|
|
@@ -71,6 +92,15 @@ npm run storybook
|
|
|
71
92
|
|
|
72
93
|
### Hooks
|
|
73
94
|
|
|
95
|
+
#### Core hooks
|
|
96
|
+
|
|
97
|
+
Start with these first:
|
|
98
|
+
|
|
99
|
+
- `useAsyncAction`
|
|
100
|
+
- `useBlocker`
|
|
101
|
+
- `useIsBlocked`
|
|
102
|
+
- `useBlockingInfo`
|
|
103
|
+
|
|
74
104
|
#### `useBlocker(blockerId, config, isActive?)`
|
|
75
105
|
|
|
76
106
|
Automatically adds a blocker when the component mounts and removes it on unmount.
|
|
@@ -235,9 +265,18 @@ function ApiComponent() {
|
|
|
235
265
|
}
|
|
236
266
|
```
|
|
237
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
|
+
|
|
238
276
|
#### `useConfirmableBlocker(blockerId, config)`
|
|
239
277
|
|
|
240
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.
|
|
241
280
|
|
|
242
281
|
**Parameters:**
|
|
243
282
|
|
|
@@ -302,6 +341,7 @@ function UnsavedChangesGuard({ discardChanges }) {
|
|
|
302
341
|
#### `useScheduledBlocker(blockerId, config)`
|
|
303
342
|
|
|
304
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.
|
|
305
345
|
|
|
306
346
|
**Parameters:**
|
|
307
347
|
|
|
@@ -342,6 +382,7 @@ function MaintenanceWindow() {
|
|
|
342
382
|
#### `useConditionalBlocker(blockerId, config)`
|
|
343
383
|
|
|
344
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.
|
|
345
386
|
|
|
346
387
|
**Parameters:**
|
|
347
388
|
|
|
@@ -393,8 +434,6 @@ Provides an isolated store instance for SSR, testing, or micro-frontends. Withou
|
|
|
393
434
|
**Props:**
|
|
394
435
|
|
|
395
436
|
- `children: ReactNode` - Child components
|
|
396
|
-
- `enableDevtools?: boolean` - Enable Redux DevTools (default: true in development)
|
|
397
|
-
- `devtoolsName?: string` - Name for DevTools (default: "UIBlocking")
|
|
398
437
|
- `middlewares?: Middleware[]` - Initial middlewares to register
|
|
399
438
|
|
|
400
439
|
**Example:**
|
|
@@ -421,7 +460,7 @@ function renderWithProvider(ui) {
|
|
|
421
460
|
// Micro-frontends - each app has its own blocking state
|
|
422
461
|
function MicroFrontend() {
|
|
423
462
|
return (
|
|
424
|
-
<UIBlockingProvider
|
|
463
|
+
<UIBlockingProvider>
|
|
425
464
|
<MicroApp />
|
|
426
465
|
</UIBlockingProvider>
|
|
427
466
|
);
|
|
@@ -523,7 +562,9 @@ uiBlockingStoreApi.getState().addBlocker("server-call", {
|
|
|
523
562
|
|
|
524
563
|
## Middleware System
|
|
525
564
|
|
|
526
|
-
|
|
565
|
+
Middleware is optional. Use it when you need analytics, logging, or performance visibility around blocker lifecycle events.
|
|
566
|
+
|
|
567
|
+
Most applications can start without middleware and add it later if blocker observability becomes important.
|
|
527
568
|
|
|
528
569
|
### Middleware Actions
|
|
529
570
|
|
|
@@ -551,6 +592,8 @@ Middleware receives events for the following actions:
|
|
|
551
592
|
|
|
552
593
|
### Built-in Middleware
|
|
553
594
|
|
|
595
|
+
These are extensions around the blocker lifecycle, not the primary onboarding path for the library.
|
|
596
|
+
|
|
554
597
|
#### Analytics Middleware
|
|
555
598
|
|
|
556
599
|
Track blocker events with your analytics provider (Google Analytics, Mixpanel, Amplitude, or custom).
|
|
@@ -645,7 +688,7 @@ configureMiddleware([myCustomMiddleware]);
|
|
|
645
688
|
|
|
646
689
|
### Combining Middleware
|
|
647
690
|
|
|
648
|
-
You can combine multiple middleware
|
|
691
|
+
You can combine multiple middleware when you need broader observability:
|
|
649
692
|
|
|
650
693
|
```jsx
|
|
651
694
|
import {
|
|
@@ -740,6 +783,8 @@ useBlocker("save", { scope: "typo" }); // Type error
|
|
|
740
783
|
|
|
741
784
|
## Use Cases
|
|
742
785
|
|
|
786
|
+
The first four examples below are the most representative starting points.
|
|
787
|
+
|
|
743
788
|
### Loading States
|
|
744
789
|
|
|
745
790
|
```jsx
|
|
@@ -783,7 +828,7 @@ function UserForm() {
|
|
|
783
828
|
}
|
|
784
829
|
```
|
|
785
830
|
|
|
786
|
-
### Unsaved Changes Protection
|
|
831
|
+
### Advanced: Unsaved Changes Protection
|
|
787
832
|
|
|
788
833
|
```jsx
|
|
789
834
|
import { useConfirmableBlocker } from "@okyrychenko-dev/react-action-guard";
|
|
@@ -880,7 +925,7 @@ function MultiStepWizard() {
|
|
|
880
925
|
}
|
|
881
926
|
```
|
|
882
927
|
|
|
883
|
-
### Scheduled Maintenance Window
|
|
928
|
+
### Advanced: Scheduled Maintenance Window
|
|
884
929
|
|
|
885
930
|
```jsx
|
|
886
931
|
import { useScheduledBlocker } from "@okyrychenko-dev/react-action-guard";
|
|
@@ -907,7 +952,7 @@ function App() {
|
|
|
907
952
|
}
|
|
908
953
|
```
|
|
909
954
|
|
|
910
|
-
### Dynamic Blocking Based on State
|
|
955
|
+
### Advanced: Dynamic Blocking Based on State
|
|
911
956
|
|
|
912
957
|
```jsx
|
|
913
958
|
import { useConditionalBlocker } from "@okyrychenko-dev/react-action-guard";
|
package/dist/index.cjs
CHANGED
|
@@ -463,8 +463,6 @@ function useUIBlockingStoreFromContext(selector, equalityFn) {
|
|
|
463
463
|
}
|
|
464
464
|
function UIBlockingProvider({
|
|
465
465
|
children,
|
|
466
|
-
enableDevtools = process.env.NODE_ENV === "development",
|
|
467
|
-
devtoolsName = "UIBlocking",
|
|
468
466
|
middlewares = []
|
|
469
467
|
}) {
|
|
470
468
|
const handleStoreInit = (store) => {
|
|
@@ -472,15 +470,7 @@ function UIBlockingProvider({
|
|
|
472
470
|
store.getState().registerMiddleware(`provider-middleware-${index.toString()}`, mw);
|
|
473
471
|
});
|
|
474
472
|
};
|
|
475
|
-
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
476
|
-
BaseUIBlockingProvider,
|
|
477
|
-
{
|
|
478
|
-
enableDevtools,
|
|
479
|
-
devtoolsName,
|
|
480
|
-
onStoreInit: handleStoreInit,
|
|
481
|
-
children
|
|
482
|
-
}
|
|
483
|
-
);
|
|
473
|
+
return /* @__PURE__ */ jsxRuntime.jsx(BaseUIBlockingProvider, { onStoreInit: handleStoreInit, children });
|
|
484
474
|
}
|
|
485
475
|
|
|
486
476
|
// src/store/uiBlockingStore.config.ts
|