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-router

v0.2.1

Published

Navigation blocking adapters for React Action Guard across React Router, TanStack Router, and Next.js

Downloads

155

Readme

@okyrychenko-dev/react-action-guard-router

npm version npm downloads License: MIT

Router integration for React Action Guard - navigation blocking for React Router, Remix, TanStack Router, and Next.js

Features

  • 🛣️ Multi-Router Support - React Router v6+, Remix, TanStack Router, Next.js Pages Router, and best-effort App Router support
  • 🎯 Scope-Based Blocking - Synchronize navigation blocking with UI blocking scopes
  • 🚦 Condition-Based Blocking - Block based on boolean conditions or functions
  • 💬 Custom Dialog Support - useDialogState helper for async confirmation dialogs
  • 🌐 Browser Protection - Prevents tab close/refresh with beforeunload events
  • 🔄 Sync & Async Handlers - Support for both synchronous and Promise-based confirmations
  • 📦 Tree-Shakeable - Import only the router adapter you need
  • 🔒 TypeScript-Friendly - Strong editor support and typed router adapters
  • 🧹 Auto Cleanup - Automatic listener cleanup on component unmount

Installation

npm install @okyrychenko-dev/react-action-guard-router @okyrychenko-dev/react-action-guard
# or
yarn add @okyrychenko-dev/react-action-guard-router @okyrychenko-dev/react-action-guard
# or
pnpm add @okyrychenko-dev/react-action-guard-router @okyrychenko-dev/react-action-guard

This package requires the following peer dependencies:

Quick Start

Basic Usage with react-action-guard

The most powerful pattern - synchronize navigation blocking with UI blocking:

import { useBlocker } from '@okyrychenko-dev/react-action-guard';
import { useNavigationBlocker } from '@okyrychenko-dev/react-action-guard-router/react-router';

function PaymentForm() {
  const [isProcessing, setIsProcessing] = useState(false);

  // Block UI during payment processing
  useBlocker('payment-processing', {
    scope: 'checkout',
    reason: 'Processing payment...',
  }, isProcessing);

  // Block navigation when the same scope is active
  useNavigationBlocker({
    scope: 'checkout',
    message: 'Payment is processing. Please wait.',
  });

  // When isProcessing = true, both UI and navigation are blocked
}

Simple Condition-Based Blocking

For basic usage without react-action-guard scopes:

import { useNavigationBlocker } from '@okyrychenko-dev/react-action-guard-router/react-router';

function EditForm() {
  const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);

  useNavigationBlocker({
    when: hasUnsavedChanges,
    message: 'You have unsaved changes. Leave anyway?',
  });
}

API Reference

Hooks

useNavigationBlocker(options)

Blocks navigation in router applications based on conditions or scope state.

Parameters:

  • options: UseNavigationBlockerOptions
    • when?: boolean | (() => boolean) - Condition to activate blocking
    • scope?: string | string[] - Scope(s) from react-action-guard to monitor
    • message?: string - Confirmation dialog message
    • onConfirm?: (message: string) => boolean | PromiseLike<boolean> - Custom confirmation handler, called once per blocked navigation attempt
    • onBlock?: () => void - Callback when navigation is blocked
    • onAllow?: () => void - Callback when navigation is allowed
    • blockBrowserUnload?: boolean - Block tab close/refresh (default: true)

Returns: { isBlocking: boolean; isIntercepting?: boolean }

  • isBlocking - Blocking condition is armed for this adapter
  • isIntercepting?: boolean - Active interception state when the router can expose it

Available in:

  • @okyrychenko-dev/react-action-guard-router/react-router - React Router v6+ & Remix
  • @okyrychenko-dev/react-action-guard-router/tanstack-router - TanStack Router
  • @okyrychenko-dev/react-action-guard-router/nextjs - Next.js Pages & App Router

Examples:

// Scope-based blocking
useNavigationBlocker({
  scope: 'form',
  message: 'You have unsaved changes',
});

// Condition-based blocking
useNavigationBlocker({
  when: hasChanges,
  message: 'Discard changes?',
  onBlock: () => console.log('Navigation blocked'),
  onAllow: () => console.log('Navigation allowed'),
});

// Multiple scopes
useNavigationBlocker({
  scope: ['form', 'validation'],
  message: 'Form is being validated',
});

// Function condition
useNavigationBlocker({
  when: () => formIsDirty() || hasUnsavedData(),
  message: 'You have unsaved work',
});

useDialogState<TMessage = string>()

Helper hook for managing custom confirmation dialogs.

Parameters: None

Returns:

  • dialogState: DialogState<TMessage> | null - Current dialog state
    • message: TMessage - The message passed to confirm
    • isOpen: boolean - Whether dialog is open
    • resolve: (value: boolean) => void - Internal resolver
  • confirm: (message: TMessage) => Promise<boolean> - Show dialog, returns Promise
  • onConfirm: () => void - Resolve dialog with true
  • onCancel: () => void - Resolve dialog with false

Example:

import { useNavigationBlocker, useDialogState } from '@okyrychenko-dev/react-action-guard-router/react-router';

function MyComponent() {
  const [hasChanges, setHasChanges] = useState(false);
  const { dialogState, confirm, onConfirm, onCancel } = useDialogState();

  useNavigationBlocker({
    when: hasChanges,
    onConfirm: confirm,  // Returns Promise<boolean>
  });

  return (
    <>
      <form>...</form>
      {dialogState && (
        <CustomDialog
          message={dialogState.message}
          onConfirm={onConfirm}
          onCancel={onCancel}
        />
      )}
    </>
  );
}

Custom message types:

interface CustomMessage {
  title: string;
  body: string;
  severity: 'warning' | 'error';
}

const { dialogState, confirm } = useDialogState<CustomMessage>();

useNavigationBlocker({
  when: isDirty,
  onConfirm: () => confirm({
    title: 'Unsaved Changes',
    body: 'Your work will be lost',
    severity: 'warning',
  }),
});

usePrompt(message, when) (React Router only)

Simple API similar to React Router v5's usePrompt.

Parameters:

  • message: string - Confirmation message
  • when: boolean - Condition to activate blocking

Example:

import { usePrompt } from '@okyrychenko-dev/react-action-guard-router/react-router';

function MyForm() {
  const [hasChanges, setHasChanges] = useState(false);
  usePrompt('You have unsaved changes', hasChanges);
}

useBeforeUnload(when, message?)

Standalone hook for blocking browser unload events (tab close, refresh).

Parameters:

  • when: boolean - Condition to activate blocking
  • message?: string - Optional custom message (default: "Are you sure you want to leave?")

Example:

import { useBeforeUnload } from '@okyrychenko-dev/react-action-guard-router';

function MyComponent() {
  const [hasUnsavedWork, setHasUnsavedWork] = useState(false);
  useBeforeUnload(hasUnsavedWork, 'You have unsaved work');
}

This utility works independently of any router and can be used in any React application.


Router-Specific Notes

React Router (& Remix)

Full support with React Router v6's useBlocker hook.

import { useNavigationBlocker } from '@okyrychenko-dev/react-action-guard-router/react-router';

function MyComponent() {
  useNavigationBlocker({
    when: isDirty,
    message: 'Discard changes?',
  });
}

Note: Remix uses React Router v6 internally, so the React Router adapter works seamlessly in Remix applications. isIntercepting is available here because React Router exposes blocker state directly.

TanStack Router

Full support with TanStack Router's history blocking.

import { useNavigationBlocker } from '@okyrychenko-dev/react-action-guard-router/tanstack-router';

function MyComponent() {
  useNavigationBlocker({
    when: isDirty,
    message: 'Discard changes?',
  });
}

Async onConfirm is supported and evaluated once per blocked navigation attempt.

Next.js Pages Router

Full support with Next.js router events.

// pages/edit.tsx
import { useNavigationBlocker } from '@okyrychenko-dev/react-action-guard-router/nextjs';

function EditPage() {
  useNavigationBlocker({
    when: isDirty,
    message: 'Discard changes?',
  });
}

Async onConfirm behavior:
Pages Router does not allow pausing transitions. When you return a Promise from onConfirm, the hook cancels the current navigation and re-attempts it via router.push(url) if confirmed. This may not preserve original transition options (e.g., shallow, scroll, locale).

Next.js App Router

Best-effort support only. Browser unload protection works, but App Router does not expose an official navigation-blocking API.

// app/edit/page.tsx
'use client';

import { useNavigationBlocker } from '@okyrychenko-dev/react-action-guard-router/nextjs';

function EditPage() {
  useNavigationBlocker({
    when: isDirty,
    message: 'You have unsaved changes',
  });
}

App Router Limitations:

  • ✅ Browser back/forward/close/refresh are blocked
  • <Link> component navigation is NOT blocked
  • router.push() is NOT blocked

For full navigation blocking support, use Pages Router.

Adapter Capabilities

| Adapter | isBlocking meaning | isIntercepting | Async onConfirm | Caveats | | --- | --- | --- | --- | --- | | React Router | Blocking condition is armed | Yes | Yes | Best semantic fidelity | | TanStack Router | Blocking condition is armed | No | Yes | Depends on history.block integration | | Next.js Pages Router | Blocking condition is armed | No | Yes | Re-attempts confirmed navigation with router.push(url) | | Next.js App Router | Blocking condition is armed | No | Best effort only | No official blocker API from Next.js |


Use Cases

Form Validation

function SignupForm() {
  const [formData, setFormData] = useState({});
  const isDirty = Object.keys(formData).length > 0;

  useNavigationBlocker({
    when: isDirty,
    message: 'Your signup progress will be lost. Continue?',
  });
}

Multi-Step Wizard

function MultiStepWizard() {
  const [currentStep, setCurrentStep] = useState(1);
  const [isSubmitting, setIsSubmitting] = useState(false);

  // Block UI during submission
  useBlocker('wizard-submit', {
    scope: 'wizard',
    reason: 'Submitting form...',
  }, isSubmitting);

  // Block navigation during submission OR if wizard incomplete
  useNavigationBlocker({
    scope: 'wizard',
    when: currentStep < 5,
    message: `You're on step ${currentStep}/5. Exit wizard?`,
  });
}

E-Commerce Checkout

import { useBlockingMutation } from '@okyrychenko-dev/react-action-guard-tanstack';
import { useNavigationBlocker } from '@okyrychenko-dev/react-action-guard-router/react-router';

function CheckoutPage() {
  const paymentMutation = useBlockingMutation({
    mutationFn: processPayment,
    blockingConfig: { scope: 'checkout' },
  });

  useNavigationBlocker({
    scope: 'checkout',
    message: 'Payment is processing. Leaving will cancel the transaction.',
  });
}

File Upload with Progress

function FileUploader() {
  const [uploadProgress, setUploadProgress] = useState(0);
  const isUploading = uploadProgress > 0 && uploadProgress < 100;

  useNavigationBlocker({
    when: isUploading,
    message: `Upload ${uploadProgress}% complete. Cancel upload?`,
    onBlock: () => pauseUpload(),
  });
}

TypeScript

TypeScript-friendly API surface:

import type {
  // Core types
  UseNavigationBlockerOptions,
  NavigationBlockerReturn,
  DialogState,
  ConfirmationResult,
  ConfirmationCallbacks,
} from '@okyrychenko-dev/react-action-guard-router';

// Router-specific types
import type { UseNavigationBlockerOptions } from '@okyrychenko-dev/react-action-guard-router/react-router';

Typed scopes with react-action-guard:

import { createTypedHooks } from '@okyrychenko-dev/react-action-guard';

type AppScopes = 'form' | 'checkout' | 'navigation';
const { useBlocker } = createTypedHooks<AppScopes>();

function MyComponent() {
  useBlocker('id', { scope: 'form' });  // ✅ Typed in the core package

  useNavigationBlocker({
    scope: 'form',  // ✅ Reuses the same scope values cleanly
    message: 'Leave form?',
  });
}

Bundle Size

Tree-shakeable by router adapter. Import only what you need:

// Only React Router code is bundled
import { useNavigationBlocker } from '@okyrychenko-dev/react-action-guard-router/react-router';

Approximate sizes (minified):

  • Core utilities: ~2 KB
  • React Router adapter: ~2.5 KB
  • TanStack Router adapter: ~2.5 KB
  • Next.js adapter: ~3 KB

Development

# Install dependencies
npm install

# Run tests
npm run test

# Build the package
npm run build

# Type checking
npm run typecheck

# Lint
npm run lint

# Fix lint errors
npm run lint:fix

Related Packages


Contributing

Contributions are welcome! Please ensure:

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

License

MIT © Oleksii Kyrychenko