@okyrychenko-dev/react-action-guard
v1.0.1
Published
Elegant UI blocking management for React applications with priorities, scopes, and automatic cleanup
Maintainers
Readme
@okyrychenko-dev/react-action-guard
Coordinate UI blocking across async actions and competing interactions in React
react-action-guard helps you prevent duplicate submits, conflicting actions, and stale UI interactions without scattering local isLoading flags across components.
Why Use It
- Wrap async work with
useAsyncActionand get blocker cleanup automatically - Coordinate blocking by
scopeacross unrelated components - Inspect current blockers with
useIsBlockedanduseBlockingInfo - Isolate state per provider for SSR, tests, and micro-frontends
- Extend lifecycle events with optional middleware
- Add advanced hooks for scheduled, confirmable, or conditional flows when needed
Installation
npm install @okyrychenko-dev/react-action-guard zustand
# or
yarn add @okyrychenko-dev/react-action-guard zustand
# or
pnpm add @okyrychenko-dev/react-action-guard zustandThis package requires the following peer dependencies:
1.0 Migration
1.0.0 removes public re-exports of internal react-zustand-toolkit helpers from the root package.
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.
Quick Start
import { useAsyncAction, useIsBlocked } from "@okyrychenko-dev/react-action-guard";
function SaveButton() {
const isSaving = useIsBlocked("form");
const runSave = useAsyncAction("save-profile", "form");
const handleClick = async () => {
await runSave(async () => {
await api.saveProfile();
});
};
return <button onClick={handleClick} disabled={isSaving}>Save</button>;
}Core Concepts
scopelets you coordinate blocking across components like"form","navigation", or"checkout"useAsyncActionis the fastest path for async workflowsuseBlockeris the lower-level hook when you already have your own boolean stateUIBlockingProvidergives you isolated state instead of the default global store
Core Use Cases
Prevent duplicate async actions
Use useAsyncAction when a button, mutation, or workflow should block while work is in flight.
Coordinate multiple components
Use shared scopes when one component starts work and another component should react by disabling UI or showing blocker details.
Isolate blocking domains
Use UIBlockingProvider when each request, test, or micro-frontend should get its own store instance.
Documentation
📚 Interactive Storybook Documentation - Run locally to explore live examples and detailed guides for all hooks
To run Storybook locally:
npm run storybookAPI Reference
Hooks
Core hooks
Start with these first:
useAsyncActionuseBlockeruseIsBlockeduseBlockingInfo
useBlocker(blockerId, config, isActive?)
Automatically adds a blocker when the component mounts and removes it on unmount.
Parameters:
blockerId: string- Unique identifier for the blockerconfig: BlockerConfig- Configuration objectscope?: string | string[]- Scope(s) to block (default: "global")reason?: string- Reason for blocking (for debugging)priority?: number- Priority level (higher = more important)timeout?: number- Auto-remove after N millisecondsonTimeout?: (blockerId: string) => void- Callback when auto-removed
isActive?: boolean- Whether the blocker is active (default: true)
When isActive is true, changing config updates the existing blocker via updateBlocker without requiring unmount/remount.
Example:
function MyComponent() {
useBlocker("my-blocker", {
scope: "form",
reason: "Form is saving",
priority: 10,
});
return <div>Content</div>;
}
// With timeout - auto-removes after 30 seconds
function SaveButton() {
const [isSaving, setIsSaving] = useState(false);
useBlocker("save-operation", {
scope: "form",
reason: "Saving...",
timeout: 30000,
onTimeout: (id) => {
console.warn(`Operation ${id} timed out`);
showNotification("Operation timed out");
setIsSaving(false);
},
}, isSaving);
const handleSave = async () => {
setIsSaving(true);
try {
await saveData();
} finally {
setIsSaving(false);
}
};
return <button onClick={handleSave} disabled={isSaving}>Save</button>;
}useIsBlocked(scope)
Checks if a specific scope (or scopes) is currently blocked.
Parameters:
scope?: string | string[]- Scope(s) to check (default: "global")
Returns: boolean - Whether the scope is blocked
Example:
function SubmitButton() {
const isFormBlocked = useIsBlocked("form");
return <button disabled={isFormBlocked}>Submit</button>;
}useBlockingInfo(scope)
Gets detailed information about all active blockers for a specific scope.
Parameters:
scope?: string- Scope to get blocking information for (default: "global")
Returns: ReadonlyArray<BlockerInfo> - Array of blocker information objects, sorted by priority (highest first)
BlockerInfo:
id: string- Unique identifier of the blockerreason: string- Reason for blocking (defaults to "Unknown")priority: number- Priority level (higher = higher priority, minimum value is 0)scope: string | string[]- Scope(s) being blockedtimestamp: number- When the blocker was added (milliseconds since epoch)timeout?: number- Optional timeout duration in millisecondsonTimeout?: (blockerId: string) => void- Optional callback when timeout expires
Example:
function CheckoutButton() {
const blockers = useBlockingInfo("checkout");
if (blockers.length > 0) {
const topBlocker = blockers[0]; // Highest priority blocker
return (
<Tooltip content={`Blocked: ${topBlocker.reason}`}>
<Button disabled>Checkout ({blockers.length} blockers)</Button>
</Tooltip>
);
}
return <Button>Checkout</Button>;
}useAsyncAction(actionId, scope, options)
Wraps an async function with automatic blocking/unblocking.
Parameters:
actionId: string- Identifier for the actionscope?: string | string[]- Scope(s) to block during executionoptions?: UseAsyncActionOptions- Optional configurationtimeout?: number- Auto-remove blocker after N millisecondsonTimeout?: (blockerId: string) => void- Callback when timed out
Returns: (asyncFn: () => Promise<T>) => Promise<T> - Function wrapper
Example:
function MyComponent() {
const executeWithBlocking = useAsyncAction("save-data", "form");
const handleSave = async () => {
await executeWithBlocking(async () => {
await saveData();
});
};
return <button onClick={handleSave}>Save</button>;
}
// With timeout - prevents infinite blocking if operation hangs
function ApiComponent() {
const execute = useAsyncAction("api-call", "global", {
timeout: 60000, // 1 minute timeout
onTimeout: (id) => {
showError("Request timed out. Please try again.");
},
});
const fetchData = () => execute(async () => {
const response = await fetch("/api/data");
return response.json();
});
return <button onClick={fetchData}>Fetch Data</button>;
}Advanced hooks
These hooks are useful, but they are not the main onboarding path for most apps:
useConfirmableBlockerfor confirmation flowsuseScheduledBlockerfor time-window blockinguseConditionalBlockerfor polling-based conditional synchronization
useConfirmableBlocker(blockerId, config)
Creates a confirmable action with UI blocking while the dialog is open or the action is running. Use it for advanced confirmation flows after the core hooks are already a good fit.
Parameters:
blockerId: string- Unique identifier for the blockerconfig: ConfirmableBlockerConfig- Configuration objectconfirmMessage: string- Message to show in confirmation dialogconfirmTitle?: string- Dialog title (default: "Confirm Action")confirmButtonText?: string- Confirm button label (default: "Confirm")cancelButtonText?: string- Cancel button label (default: "Cancel")onConfirm: () => void | Promise<void>- Callback when user confirmsonCancel?: () => void- Callback when user cancels- Plus all
BlockerConfigproperties (scope, reason, priority, timeout)
Returns:
execute: () => void- Opens the confirmation dialogisDialogOpen: boolean- Whether the dialog is openisExecuting: boolean- Whether the confirm action is runningconfirmConfig: { title, message, confirmText, cancelText }- UI-ready dialog configonConfirm: () => Promise<void>- Confirm handler to wire to your dialogonCancel: () => void- Cancel handler to wire to your dialog
Example:
function UnsavedChangesGuard({ discardChanges }) {
const {
execute,
isDialogOpen,
isExecuting,
confirmConfig,
onConfirm,
onCancel,
} = useConfirmableBlocker("unsaved-changes", {
scope: "navigation",
reason: "Unsaved changes",
confirmMessage: "You have unsaved changes. Are you sure you want to leave?",
onConfirm: async () => {
await discardChanges();
},
});
return (
<>
<button onClick={execute}>Leave</button>
{isDialogOpen && (
<ConfirmDialog
title={confirmConfig.title}
message={confirmConfig.message}
confirmText={confirmConfig.confirmText}
cancelText={confirmConfig.cancelText}
onConfirm={onConfirm}
onCancel={onCancel}
/>
)}
{isExecuting && <LoadingOverlay message="Processing..." />}
</>
);
}useScheduledBlocker(blockerId, config)
Blocks UI during a scheduled time period or maintenance window. Use it when blocking is driven by time windows rather than user-triggered async work.
Parameters:
blockerId: string- Unique identifier for the blockerconfig: ScheduledBlockerConfigschedule: BlockingSchedulestart: string | Date | number- Start time (ISO string, Date, or timestamp)end?: string | Date | number- End time (optional)duration?: number- Duration in milliseconds (takes precedence over end)
onScheduleStart?: () => void- Callback when blocking startsonScheduleEnd?: () => void- Callback when blocking ends- Plus all
BlockerConfigproperties (scope, reason, priority)
Example:
function MaintenanceWindow() {
useScheduledBlocker("maintenance", {
scope: "global",
reason: "Scheduled maintenance",
priority: 1000,
schedule: {
start: "2024-01-15T02:00:00Z",
duration: 3600000, // 1 hour in milliseconds
},
onScheduleStart: () => {
console.log("Maintenance started");
},
onScheduleEnd: () => {
console.log("Maintenance completed");
},
});
return <div>App content</div>;
}useConditionalBlocker(blockerId, config)
Periodically checks a condition and blocks/unblocks based on the result. Use this when polling is an acceptable tradeoff and the condition is not naturally event-driven.
Parameters:
blockerId: string- Unique identifier for the blockerconfig: ConditionalBlockerConfig<TState>scope: string | string[]- Required scope(s) to blockcondition: (state?: TState) => boolean- Function that determines if blocking should be activecheckInterval?: number- How often to check the condition in ms (default: 1000)state?: TState- Optional state to pass to the condition function- Plus all other
BlockerConfigproperties (reason, priority)
Example:
function NetworkStatusBlocker() {
const [isOnline, setIsOnline] = useState(navigator.onLine);
useEffect(() => {
const handleOnline = () => setIsOnline(true);
const handleOffline = () => setIsOnline(false);
window.addEventListener("online", handleOnline);
window.addEventListener("offline", handleOffline);
return () => {
window.removeEventListener("online", handleOnline);
window.removeEventListener("offline", handleOffline);
};
}, []);
useConditionalBlocker("network-check", {
scope: ["form", "navigation"],
reason: "No network connection",
priority: 100,
condition: () => !isOnline,
checkInterval: 2000,
});
return <div>App content</div>;
}Provider (Optional)
UIBlockingProvider
Provides an isolated store instance for SSR, testing, or micro-frontends. Without the provider, hooks use a global store.
Props:
children: ReactNode- Child componentsenableDevtools?: boolean- Enable Redux DevTools (default: true in development)devtoolsName?: string- Name for DevTools (default: "UIBlocking")middlewares?: Middleware[]- Initial middlewares to register
Example:
import { UIBlockingProvider } from "@okyrychenko-dev/react-action-guard";
// SSR - each request gets isolated state
function App() {
return (
<UIBlockingProvider>
<MyApp />
</UIBlockingProvider>
);
}
// Testing - isolated state per test, no cleanup needed
function renderWithProvider(ui) {
return render(
<UIBlockingProvider>{ui}</UIBlockingProvider>
);
}
// Micro-frontends - each app has its own blocking state
function MicroFrontend() {
return (
<UIBlockingProvider devtoolsName="MicroApp-Blocking">
<MicroApp />
</UIBlockingProvider>
);
}Context Hooks
useUIBlockingContext()- Get store API from context (throws if outside provider)useOptionalUIBlockingContext()- Get store API from context ornulloutside provideruseIsInsideUIBlockingProvider()- Check if inside a provideruseUIBlockingStoreFromContext(selector)- Select state from context storeuseResolvedStoreApi()- Resolve to provider store API or global store APIuseResolvedValue(selector)- Resolve to provider/global store and select with shallow comparison
Legacy aliases remain available for backward compatibility:
useOptionalContext()->useOptionalUIBlockingContext()useResolvedStore()->useResolvedStoreApi()useResolvedStoreWithSelector()->useResolvedValue()
Store
useUIBlockingStore
Direct access to the Zustand store for advanced use cases (requires a selector).
Methods:
addBlocker(id, config)- Manually add a blocker (re-adding with the same ID replaces config)updateBlocker(id, config)- Update blocker metadata (timeout restarts if newtimeoutvalue provided)removeBlocker(id)- Manually remove a blockerisBlocked(scope)- Check if scope is blockedgetBlockingInfo(scope)- Get detailed blocking informationclearAllBlockers()- Remove all blockers (emits"clear"middleware event)clearBlockersForScope(scope)- Remove blockers for specific scope (emits"clear_scope"middleware event)registerMiddleware(name, middleware)- Register middleware manuallyunregisterMiddleware(name)- Unregister middleware manually
Note about updateBlocker and timeouts:
- If you pass a new
timeoutvalue, the timer will be restarted - If you don't pass
timeout, the existing timer continues unchanged - Set
timeout: 0to clear an existing timeout addBlockeralways resets/creates a new timeout timer when called with the same ID
Example:
import { useUIBlockingStore } from "@okyrychenko-dev/react-action-guard";
function AdvancedComponent() {
const { addBlocker, updateBlocker, removeBlocker } = useUIBlockingStore((state) => ({
addBlocker: state.addBlocker,
updateBlocker: state.updateBlocker,
removeBlocker: state.removeBlocker,
}));
const startBlocking = () => {
addBlocker("custom-blocker", {
scope: ["form", "navigation"],
reason: "Critical operation in progress",
priority: 100,
});
};
const updatePriority = () => {
// Update metadata - timeout continues if not changed
updateBlocker("custom-blocker", { priority: 200 });
};
const extendTimeout = () => {
// Update timeout - timer restarts with new value
updateBlocker("custom-blocker", { timeout: 60000 });
};
const stopBlocking = () => {
removeBlocker("custom-blocker");
};
return (
<div>
<button onClick={startBlocking}>Start</button>
<button onClick={updatePriority}>Increase Priority</button>
<button onClick={stopBlocking}>Stop</button>
</div>
);
}For non-hook contexts (tests, utilities, event handlers), use uiBlockingStoreApi:
import { uiBlockingStoreApi } from "@okyrychenko-dev/react-action-guard";
uiBlockingStoreApi.getState().addBlocker("server-call", {
scope: "global",
reason: "Server call running",
});Middleware System
Middleware is optional. Use it when you need analytics, logging, or performance visibility around blocker lifecycle events.
Most applications can start without middleware and add it later if blocker observability becomes important.
Middleware Actions
Middleware receives events for the following actions:
"add"- Blocker was added"update"- Blocker metadata was updated"remove"- Blocker was removed"timeout"- Blocker was auto-removed due to timeout"clear"- All blockers were cleared (includescountfield)"clear_scope"- Blockers for specific scope were cleared (includesscopeandcountfields)
MiddlewareContext type:
{
action: "add" | "update" | "remove" | "timeout" | "clear" | "clear_scope";
blockerId: string;
config?: BlockerConfig;
timestamp: number;
prevState?: BlockerConfig; // Available for "update" and "remove"
scope?: string; // Available for "clear_scope"
count?: number; // Available for "clear" and "clear_scope"
}Built-in Middleware
These are extensions around the blocker lifecycle, not the primary onboarding path for the library.
Analytics Middleware
Track blocker events with your analytics provider (Google Analytics, Mixpanel, Amplitude, or custom).
import {
configureMiddleware,
createAnalyticsMiddleware,
} from "@okyrychenko-dev/react-action-guard";
// Google Analytics
configureMiddleware([createAnalyticsMiddleware({ provider: "ga" })]);
// Mixpanel
configureMiddleware([createAnalyticsMiddleware({ provider: "mixpanel" })]);
// Amplitude
configureMiddleware([createAnalyticsMiddleware({ provider: "amplitude" })]);
// Custom analytics
configureMiddleware([
createAnalyticsMiddleware({
track: (event, data) => {
myAnalytics.track(event, data);
},
}),
]);Logger Middleware
Log blocker lifecycle events to the console for debugging.
import { configureMiddleware, loggerMiddleware } from "@okyrychenko-dev/react-action-guard";
configureMiddleware([loggerMiddleware]);Performance Middleware
Monitor blocker performance and detect slow operations.
import {
configureMiddleware,
createPerformanceMiddleware,
} from "@okyrychenko-dev/react-action-guard";
configureMiddleware([
createPerformanceMiddleware({
onSlowBlock: (blockerId, duration) => {
console.warn(`Blocker ${blockerId} was active for ${duration}ms`);
},
slowBlockThreshold: 5000, // 5 seconds
}),
]);Note: configureMiddleware registers middleware on the global store. If you use UIBlockingProvider, register middleware via the provider's middlewares prop instead.
Each configureMiddleware(...) call replaces previously configured global middlewares (middleware-*) and keeps provider-level middlewares untouched.
Analytics middleware is SSR-safe: in non-browser environments it becomes a no-op for ga, mixpanel, and amplitude providers.
Custom Middleware
Create your own middleware to handle blocker events:
import { configureMiddleware } from "@okyrychenko-dev/react-action-guard";
const myCustomMiddleware = (context) => {
const { action, blockerId, config, timestamp, scope, count } = context;
if (action === "add") {
console.log(`Blocker added: ${blockerId}`, config);
} else if (action === "update") {
console.log(`Blocker updated: ${blockerId}`, config);
} else if (action === "remove") {
console.log(`Blocker removed: ${blockerId}`);
} else if (action === "timeout") {
console.log(`Blocker timed out: ${blockerId}`);
} else if (action === "clear") {
console.log(`All blockers cleared (${count} total)`);
} else if (action === "clear_scope") {
console.log(`Cleared ${count} blocker(s) for scope: ${scope}`);
}
};
configureMiddleware([myCustomMiddleware]);Combining Middleware
You can combine multiple middleware when you need broader observability:
import {
configureMiddleware,
createAnalyticsMiddleware,
loggerMiddleware,
createPerformanceMiddleware,
} from "@okyrychenko-dev/react-action-guard";
configureMiddleware([
loggerMiddleware,
createAnalyticsMiddleware({ provider: "ga" }),
createPerformanceMiddleware({
slowBlockThreshold: 3000,
onSlowBlock: (blockerId, duration) => {
// Send to error tracking service
errorTracker.captureMessage(`Slow blocker: ${blockerId}`, {
duration,
});
},
}),
]);Tree Shaking
The library is fully tree-shakeable. Import only the features you need to keep your bundle size small:
// Only imports the hook you need
import { useBlocker } from "@okyrychenko-dev/react-action-guard";
// Middleware is not included unless you import it
import {
configureMiddleware,
createAnalyticsMiddleware,
} from "@okyrychenko-dev/react-action-guard";The package is configured with "sideEffects": false, allowing modern bundlers (Webpack, Rollup, Vite) to eliminate unused code automatically.
TypeScript
The package is written in TypeScript and includes full type definitions.
import type {
// Core types
BlockerConfig,
BlockerInfo,
UIBlockingStore,
UIBlockingStoreState,
// Hook types
ConfirmableBlockerConfig,
ConfirmDialogConfig,
UseConfirmableBlockerReturn,
ScheduledBlockerConfig,
ConditionalBlockerConfig,
BlockingSchedule,
UseAsyncActionOptions,
// Middleware types
Middleware,
MiddlewareContext,
AnalyticsConfig,
AnalyticsProvider,
PerformanceConfig,
// Type-safe scopes
BlockerConfigTyped,
DefaultScopes,
ScopeValue,
} from "@okyrychenko-dev/react-action-guard";Type-Safe Scopes
Create typed versions of the hooks to prevent scope typos at compile time:
import { createTypedHooks } from "@okyrychenko-dev/react-action-guard";
type AppScopes = "global" | "form" | "navigation" | "checkout";
const { useBlocker, useIsBlocked, useAsyncAction, useBlockingInfo } =
createTypedHooks<AppScopes>();
useBlocker("save", { scope: "form" }); // OK
useBlocker("save", { scope: "typo" }); // Type errorUse Cases
The first four examples below are the most representative starting points.
Loading States
function DataLoader() {
const [isLoading, setIsLoading] = useState(false);
useBlocker(
"data-loader",
{
scope: "content",
reason: "Loading data",
},
isLoading
);
// ... rest of component
}Form Submission with Analytics
import { useAsyncAction, useIsBlocked } from "@okyrychenko-dev/react-action-guard";
function UserForm() {
const executeWithBlocking = useAsyncAction("submit-form", "form");
const isBlocked = useIsBlocked("form");
const handleSubmit = async (data) => {
await executeWithBlocking(async () => {
await submitForm(data);
});
};
return (
<form onSubmit={handleSubmit}>
<input disabled={isBlocked} />
<button disabled={isBlocked}>Submit</button>
</form>
);
}Advanced: Unsaved Changes Protection
import { useConfirmableBlocker } from "@okyrychenko-dev/react-action-guard";
function FormWithUnsavedWarning() {
const [formData, setFormData] = useState({});
const [hasChanges, setHasChanges] = useState(false);
const {
execute,
isDialogOpen,
confirmConfig,
onConfirm,
onCancel,
} = useConfirmableBlocker("unsaved-form", {
scope: "navigation",
reason: "Unsaved form data",
priority: 100,
confirmMessage: "You have unsaved changes. Discard them?",
onConfirm: () => {
setFormData({});
setHasChanges(false);
},
});
return (
<form>
<input
onChange={(e) => {
setFormData({ ...formData, name: e.target.value });
setHasChanges(true);
}}
/>
<button type="button" onClick={execute}>Cancel</button>
{isDialogOpen && (
<ConfirmDialog
title={confirmConfig.title}
message={confirmConfig.message}
confirmText={confirmConfig.confirmText}
cancelText={confirmConfig.cancelText}
onConfirm={onConfirm}
onCancel={onCancel}
/>
)}
</form>
);
}Global Loading Overlay
function App() {
const isGloballyBlocked = useIsBlocked("global");
return (
<div>
{isGloballyBlocked && <LoadingOverlay />}
<YourApp />
</div>
);
}Multi-Step Process with Priority
function MultiStepWizard() {
const [step, setStep] = useState(1);
// Higher priority for payment step
useBlocker(
"payment-step",
{
scope: ["navigation", "form"],
reason: "Processing payment",
priority: 100,
},
step === 3
);
// Lower priority for other steps
useBlocker(
"wizard-step",
{
scope: "navigation",
reason: "Wizard in progress",
priority: 50,
},
step < 3
);
return <div>Step {step}</div>;
}Advanced: Scheduled Maintenance Window
import { useScheduledBlocker } from "@okyrychenko-dev/react-action-guard";
function App() {
useScheduledBlocker("weekly-maintenance", {
scope: "global",
reason: "Weekly system maintenance",
priority: 500,
schedule: {
start: new Date("2024-01-21T03:00:00Z"),
duration: 1800000, // 30 minutes
},
onScheduleStart: () => {
showNotification("System maintenance in progress");
},
onScheduleEnd: () => {
showNotification("Maintenance completed");
window.location.reload();
},
});
return <YourApp />;
}Advanced: Dynamic Blocking Based on State
import { useConditionalBlocker } from "@okyrychenko-dev/react-action-guard";
function StorageQuotaGuard() {
const [storageUsed, setStorageUsed] = useState(0);
const STORAGE_LIMIT = 1000000; // 1MB
useConditionalBlocker("storage-limit", {
scope: ["upload", "save"],
reason: "Storage quota exceeded",
priority: 200,
condition: () => storageUsed > STORAGE_LIMIT,
state: storageUsed,
checkInterval: 5000, // Check every 5 seconds
});
return (
<div>
<p>
Storage used: {storageUsed} / {STORAGE_LIMIT} bytes
</p>
<UploadButton />
</div>
);
}Clearing Blockers on Navigation
Clear blockers when navigating away or on specific events:
import { useUIBlockingStore } from "@okyrychenko-dev/react-action-guard";
import { useEffect } from "react";
function CheckoutPage() {
const { clearBlockersForScope, clearAllBlockers } = useUIBlockingStore(
(state) => ({
clearBlockersForScope: state.clearBlockersForScope,
clearAllBlockers: state.clearAllBlockers,
})
);
// Clear checkout-specific blockers when leaving the page
useEffect(() => {
return () => {
// Clean up checkout blockers on unmount
clearBlockersForScope("checkout");
};
}, [clearBlockersForScope]);
const handleCancelOrder = () => {
// Clear all blockers when user explicitly cancels
clearAllBlockers();
navigate("/");
};
return (
<div>
<CheckoutForm />
<button onClick={handleCancelOrder}>Cancel Order</button>
</div>
);
}Managing Session Timeouts with Dynamic Updates
Extend or update blocker timeouts dynamically:
import { useUIBlockingStore } from "@okyrychenko-dev/react-action-guard";
function SessionManager() {
const { addBlocker, updateBlocker, removeBlocker } = useUIBlockingStore(
(state) => ({
addBlocker: state.addBlocker,
updateBlocker: state.updateBlocker,
removeBlocker: state.removeBlocker,
})
);
const startSession = () => {
addBlocker("session-timeout", {
scope: "global",
reason: "Session expiring soon",
priority: 50,
timeout: 300000, // 5 minutes
onTimeout: () => {
logout();
showNotification("Session expired");
},
});
};
const extendSession = () => {
// Extend timeout - timer restarts with new value
updateBlocker("session-timeout", {
timeout: 600000, // 10 minutes
reason: "Session extended",
});
};
const endSession = () => {
removeBlocker("session-timeout");
logout();
};
return (
<div>
<button onClick={startSession}>Start Session</button>
<button onClick={extendSession}>Extend Session</button>
<button onClick={endSession}>Logout</button>
</div>
);
}Development
# Install dependencies
npm install
# Run tests
npm run test
# Run tests with coverage
npm run test:coverage
# Build the package
npm run build
# Type checking
npm run typecheck
# Lint code
npm run lint
# Fix lint errors
npm run lint:fix
# Format code
npm run format
# Watch mode for development
npm run devContributing
Contributions are welcome! Please ensure:
- All tests pass (
npm run test) - Code is properly typed (
npm run typecheck) - Linting passes (
npm run lint) - Code is formatted (
npm run format)
Changelog
See CHANGELOG.md for a detailed list of changes in each version.
License
MIT © Oleksii Kyrychenko
