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

v1.0.1

Published

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

Readme

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

See the versioned capability matrix for evaluated peers, reproducible checks, evidence kinds, and limitations.

npm version npm downloads License: MIT

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

Start with the core-only coordination example to see one operation affect two independent controls. Add this package when you need its integration. Canonical documentation and local instructions provide the next learning step.

Features

  • 🛣️ Multi-Router Support - React Router v6.19+, Remix, TanStack Router, limited Next.js Pages Router interception, and App Router unload-only protection
  • 🎯 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 { useActionBlocker } 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
  useActionBlocker(
    "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?",
  });
}

Canonical reference

Read the public reference and integration guide. The expandable material below remains available for existing links.

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.19+ & 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 - Settle and close this specific dialog
  • confirm: (message: TMessage) => Promise<boolean> - Show dialog, returns Promise
  • onConfirm: () => void - Resolve dialog with true
  • onCancel: () => void - Resolve dialog with false

Each request settles once: confirmation resolves true; cancellation, replacement by another confirm call, and hook unmount resolve false. Calling dialogState.resolve also closes that dialog. Repeated calls and resolvers captured from an older dialog have no effect on a newer dialog.

Effect teardown also cancels and clears the pending dialog when React preserves hook state, such as when an Activity becomes hidden. Revealing the subtree starts with no open dialog and allows new confirmation requests.

confirm, onConfirm, and onCancel retain stable references across renders. The hook-level onConfirm and onCancel always act on the current dialog; use its dialogState.resolve when a captured callback must belong to a specific dialog.

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",
    }),
});

Render-time conditions

A function passed as when must be a pure condition. Each React render evaluates it again, including a stable function created with useCallback or one reading a ref or external state. Changing a ref or external value alone does not trigger a render or refresh protection; arrange a React rerender or subscription. useCallback is optional and does not cache the condition's result. A true condition or a blocked scope enables protection (OR semantics).

This correction affects consumers whose stable predicates previously retained a stale result: protection now follows the current result after rerender. Router capabilities are unchanged; Next App Router protects document unload only and does not intercept same-document navigation.

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

Uses TanStack Router's native useBlocker for in-app transitions. Requires TanStack Router 1.170.41 or newer; earlier versions are no longer declared supported.

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

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

Sync and async onConfirm are evaluated once per blocked navigation attempt. Active blocking without a message denies silently and emits onBlock. Denial, rejection, and thrown confirmation errors retain the current location. Acceptance permits that transition once and emits onAllow; subsequent transitions remain protected. Superseded confirmations and results received after unmount or authorization option changes cannot authorize navigation. Replacing when or changing the effective scope contents invalidates a pending confirmation even when the computed blocking condition remains true. Equivalent scope arrays preserve the attempt: array identity, order, and duplicates do not affect scope comparison. Changes to observer callbacks onBlock and onAllow do not invalidate a pending confirmation; the attempt retains the callbacks it started with.

scope and when retain their existing OR behavior. blockBrowserUnload controls native TanStack unload protection. Its one-shot bypass suppresses a second browser prompt after accepted document navigation; later unloads remain protected. The adapter does not register a separate shared unload handler. Unload uses a browser-controlled prompt, not asynchronous custom confirmation.

Not-found limitation: In the verified TanStack Router 1.170.41, navigation from an unmatched URL (__notFound__) to a matched route bypasses native useBlocker before this adapter's callback runs, even with when: true or an active scope. onBlock, onConfirm, and onAllow are not called for that transition. A blocker mounted above the not-found UI therefore cannot protect this exit; do not rely on it to guard unsaved work there. Subsequent matched-to-matched navigation remains guarded. This is upstream behavior introduced by the upstream TanStack Router change, covered by real-router tests for both condition sources. The adapter does not compensate for the bypass.

Compatibility tests exercise the public hook at 1.170.41 with real memory history for in-app navigation and browser history in the DOM test environment for unload protection. The unload tests intercept document location assignment and dispatch beforeunload events; they do not establish browser prompt UI behavior. Separate Playwright tests verified same-document back/forward on matched routes in Chrome 154.0.8037.57 on 2026-10-03: custom-dialog cancellation retains the original route and URL, confirmation reaches the intended destination once, and subsequent history attempts remain protected. This evidence does not establish other browsers or cross-document history transitions. Reproduction steps, evaluated versions, and result artifacts are documented in browser-history verification. The verified floor was raised from 1.170.28 because the older locked router-core/history combination skipped blockers for external navigation. The native API is documented in TanStack navigation blocking.

Next.js Pages Router

Limited interception through 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).

Only the current confirmation attempt can resume navigation. Cleanup, disabling protection, or changing the protected scope or message invalidates pending answers. Callback rerenders and equivalent scopes preserve the current attempt. onAllow fires once for an accepted attempt; the replay permission is consumed once and cleared when the retry finishes or another URL is attempted.

Selected packed Next 15.5.23 production Pages/App browser evidence and its limitations are recorded in the runtime guide and capability matrix. This does not establish every supported peer or navigation option.

Next.js App Router

Unload-only protection: the hook requests a browser beforeunload prompt, subject to browser policy. It does not intercept same-document Link, push, or back/forward navigation, and does not evaluate onConfirm. App Router does not expose an official navigation-blocking API.

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

import { useState } from "react";
import { useAppRouterBlocker } from "@okyrychenko-dev/react-action-guard-router/nextjs";

export default function EditPage() {
  const [isDirty, setIsDirty] = useState(false);

  useAppRouterBlocker({
    when: isDirty,
    message: "You have unsaved changes",
  });

  return <input onChange={() => setIsDirty(true)} />;
}

App Router Limitations:

  • beforeunload can request the browser’s native prompt for document unload (close, refresh, or cross-document navigation), subject to browser policy.
  • Same-document App Router back/forward is not intercepted.
  • ❌ <Link> component navigation is NOT blocked
  • ❌ router.push() is NOT blocked

Pages Router provides route-event interception with the replay limitations described above.

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 | Native useBlocker; memory-history transitions verified | | 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 | No | beforeunload only; no same-document interception |


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
  useActionBlocker(
    "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. Leave this page?",
  });
}

UI blocking and navigation confirmation do not cancel the payment request or roll back a transaction. Cancellation, server authorization, and idempotency belong to the application and its backend.

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
  BaseNavigationBlockerOptions,
  NavigationBlockerReturn,
  DialogState,
} 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 { useActionBlocker } = createTypedHooks<AppScopes>();

function MyComponent() {
  useActionBlocker("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";

Use the publication report for dated, per-entry-point measurements and the exact evaluated cohort. It records raw and gzip byte counts for distributed files and minimal consumer bundles, including separate React Router, TanStack Router and Next.js entry points.

Reproduce the report with pnpm run package:check from the repository root. The measurement script uses the recorded esbuild version, browser ESM output and minification for minimal consumer bundles. React, React DOM, TanStack Query and router framework imports are externalized as listed in each report entry. Distributed file sizes are reported separately from consumer bundles. These profiles measure adapter code under those exclusions, not the total dependency cost in your application; measure your own production build when evaluating that cost.


Development

This package lives in the react-action-guard monorepo (pnpm workspaces). From the monorepo root:

# Install dependencies for all packages
pnpm install

# Run this package's scripts with --filter
pnpm --filter @okyrychenko-dev/react-action-guard-router run test:run
pnpm --filter @okyrychenko-dev/react-action-guard-router run build
pnpm --filter @okyrychenko-dev/react-action-guard-router run typecheck
pnpm --filter @okyrychenko-dev/react-action-guard-router run lint
pnpm --filter @okyrychenko-dev/react-action-guard-router run lint:fix

# Or cd into the package and run scripts directly
cd packages/router
pnpm run test:run

Related Packages


Contributing

Contributions are welcome! Please ensure, before opening a PR:

  1. Tests pass (pnpm run test:run)
  2. Code is properly typed (pnpm run typecheck)
  3. Linting passes (pnpm run lint)
  4. Code is formatted (pnpm run format)
  5. If the change affects this package's public behavior, add a changeset: pnpm changeset (see the Releasing section of the monorepo README)

License

MIT © Oleksii Kyrychenko