npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@okyrychenko-dev/react-action-guard

v1.0.1

Published

Elegant UI blocking management for React applications with priorities, scopes, and automatic cleanup

Readme

@okyrychenko-dev/react-action-guard

npm version npm downloads License: MIT

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 useAsyncAction and get blocker cleanup automatically
  • Coordinate blocking by scope across unrelated components
  • Inspect current blockers with useIsBlocked and useBlockingInfo
  • 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 zustand

This package requires the following peer dependencies:

  • React ^18.0.0 || ^19.0.0
  • Zustand ^5.0.0 - State management library

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

  • scope lets you coordinate blocking across components like "form", "navigation", or "checkout"
  • useAsyncAction is the fastest path for async workflows
  • useBlocker is the lower-level hook when you already have your own boolean state
  • UIBlockingProvider gives 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 storybook

API Reference

Hooks

Core hooks

Start with these first:

  • useAsyncAction
  • useBlocker
  • useIsBlocked
  • useBlockingInfo

useBlocker(blockerId, config, isActive?)

Automatically adds a blocker when the component mounts and removes it on unmount.

Parameters:

  • blockerId: string - Unique identifier for the blocker
  • config: BlockerConfig - Configuration object
    • scope?: 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 milliseconds
    • onTimeout?: (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 blocker
  • reason: string - Reason for blocking (defaults to "Unknown")
  • priority: number - Priority level (higher = higher priority, minimum value is 0)
  • scope: string | string[] - Scope(s) being blocked
  • timestamp: number - When the blocker was added (milliseconds since epoch)
  • timeout?: number - Optional timeout duration in milliseconds
  • onTimeout?: (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 action
  • scope?: string | string[] - Scope(s) to block during execution
  • options?: UseAsyncActionOptions - Optional configuration
    • timeout?: number - Auto-remove blocker after N milliseconds
    • onTimeout?: (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:

  • useConfirmableBlocker for confirmation flows
  • useScheduledBlocker for time-window blocking
  • useConditionalBlocker for 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 blocker
  • config: ConfirmableBlockerConfig - Configuration object
    • confirmMessage: string - Message to show in confirmation dialog
    • confirmTitle?: 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 confirms
    • onCancel?: () => void - Callback when user cancels
    • Plus all BlockerConfig properties (scope, reason, priority, timeout)

Returns:

  • execute: () => void - Opens the confirmation dialog
  • isDialogOpen: boolean - Whether the dialog is open
  • isExecuting: boolean - Whether the confirm action is running
  • confirmConfig: { title, message, confirmText, cancelText } - UI-ready dialog config
  • onConfirm: () => Promise<void> - Confirm handler to wire to your dialog
  • onCancel: () => 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 blocker
  • config: ScheduledBlockerConfig
    • schedule: BlockingSchedule
      • start: 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 starts
    • onScheduleEnd?: () => void - Callback when blocking ends
    • Plus all BlockerConfig properties (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 blocker
  • config: ConditionalBlockerConfig<TState>
    • scope: string | string[] - Required scope(s) to block
    • condition: (state?: TState) => boolean - Function that determines if blocking should be active
    • checkInterval?: number - How often to check the condition in ms (default: 1000)
    • state?: TState - Optional state to pass to the condition function
    • Plus all other BlockerConfig properties (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 components
  • enableDevtools?: 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 or null outside provider
  • useIsInsideUIBlockingProvider() - Check if inside a provider
  • useUIBlockingStoreFromContext(selector) - Select state from context store
  • useResolvedStoreApi() - Resolve to provider store API or global store API
  • useResolvedValue(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 new timeout value provided)
  • removeBlocker(id) - Manually remove a blocker
  • isBlocked(scope) - Check if scope is blocked
  • getBlockingInfo(scope) - Get detailed blocking information
  • clearAllBlockers() - Remove all blockers (emits "clear" middleware event)
  • clearBlockersForScope(scope) - Remove blockers for specific scope (emits "clear_scope" middleware event)
  • registerMiddleware(name, middleware) - Register middleware manually
  • unregisterMiddleware(name) - Unregister middleware manually

Note about updateBlocker and timeouts:

  • If you pass a new timeout value, the timer will be restarted
  • If you don't pass timeout, the existing timer continues unchanged
  • Set timeout: 0 to clear an existing timeout
  • addBlocker always 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 (includes count field)
  • "clear_scope" - Blockers for specific scope were cleared (includes scope and count fields)

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 error

Use 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 dev

Contributing

Contributions are welcome! Please ensure:

  1. All tests pass (npm run test)
  2. Code is properly typed (npm run typecheck)
  3. Linting passes (npm run lint)
  4. Code is formatted (npm run format)

Changelog

See CHANGELOG.md for a detailed list of changes in each version.

License

MIT © Oleksii Kyrychenko