DEV Community

Nilupul Perera
Nilupul Perera

Posted on

Mastering Navigation Control in React with useCallbackPrompt and useBlocker 🚦

Building seamless navigation experiences is essential in modern web applications. Whether you're safeguarding unsaved changes or confirming critical actions, having control over navigation flow ensures a better user experience.

This article introduces useCallbackPrompt, a custom React hook built on useBlocker, that offers enhanced control over navigation events with customizable prompts.

React Route Blocker

What is useCallbackPrompt?

useCallbackPrompt is a custom hook designed to intercept navigation events and display a confirmation prompt before allowing users to navigate away. It's built using the lower-level useBlocker hook, which integrates with React Router's internal navigation APIs.

What is useBlocker?

useBlocker is a utility hook that provides the core blocking mechanism by listening to React Router's navigation events. It enables the creation of custom logic when navigation is attempted.

Here’s the code for useBlocker:

import { useContext, useEffect } from 'react';
import { UNSAFE_NavigationContext } from 'react-router-dom';

const useBlocker = (blocker, enable = true) => {
  const navigator = useContext(UNSAFE_NavigationContext).navigator;

  useEffect(() => {
    if (!enable) return;

    const unblock = navigator.block((tx) => {
      const autoUnblockingTx = {
        ...tx,
        retry() {
          unblock();
          tx.retry();
        },
      };

      blocker(autoUnblockingTx);
    });

    return unblock;
  }, [navigator, blocker, enable]);
};

export default useBlocker;
Enter fullscreen mode Exit fullscreen mode

How useCallbackPrompt Works

useCallbackPrompt extends useBlocker to provide a more user-friendly interface for managing navigation prompts.

Here’s the implementation:

import { useCallback, useEffect, useState } from 'react';
import { useLocation, useNavigate } from 'react-router';
import useBlocker from './useBlocker';

const useCallbackPrompt = (enable) => {
  const navigate = useNavigate();
  const location = useLocation();
  const [showPrompt, setShowPrompt] = useState(false);
  const [lastLocation, setLastLocation] = useState(null);
  const [confirmedNavigation, setConfirmedNavigation] = useState(false);

  // Cancel the navigation
  const cancelNavigation = useCallback(() => {
    setShowPrompt(false);
  }, []);

  // Block navigation and show prompt
  const handleBlockedNavigation = useCallback(
    (nextLocation) => {
      if (
        !confirmedNavigation &&
        nextLocation?.location?.pathname !== location?.pathname
      ) {
        setShowPrompt(true);
        setLastLocation(nextLocation);
        return false;
      }
      return true;
    },
    [confirmedNavigation, location]
  );

  // Confirm the navigation
  const confirmNavigation = useCallback(() => {
    setShowPrompt(false);
    setConfirmedNavigation(true);
  }, []);

  useEffect(() => {
    if (confirmedNavigation && lastLocation) {
      navigate(lastLocation.location.pathname);
      setConfirmedNavigation(false);
    }
  }, [confirmedNavigation, lastLocation, navigate]);

  useBlocker(handleBlockedNavigation, enable);

  return [showPrompt, confirmNavigation, cancelNavigation];
};

export default useCallbackPrompt;
Enter fullscreen mode Exit fullscreen mode

Advantages of Using useCallbackPrompt

  1. Customizable Prompt: Fully customizable UI for the navigation dialog.
  2. Dynamic Control: Easily enable or disable navigation blocking based on conditions.
  3. Retry Mechanism: Automatically retries blocked navigation after confirmation.
  4. Seamless Integration: Works with React Router’s navigation API for modern SPAs.

How to Use useCallbackPrompt

Here’s how you can use useCallbackPrompt in your React app:

1. Install React Router

Ensure React Router is installed:

npm install react-router-dom
Enter fullscreen mode Exit fullscreen mode

2. Use the Hook

Here’s a form example where the prompt shows when users navigate away with unsaved changes:

import React, { useState } from 'react';
import useCallbackPrompt from './useCallbackPrompt';

const FormPage = () => {
  const [isDirty, setIsDirty] = useState(false);
  const [showPrompt, confirmNavigation, cancelNavigation] = useCallbackPrompt(isDirty);

  return (
    <>
      <form>
        <input
          type="text"
          onChange={() => setIsDirty(true)}
          placeholder="Type something..."
        />
        <button onClick={() => setIsDirty(false)}>Save</button>
      </form>

      {showPrompt && (
        <div className="modal">
          <p>You have unsaved changes. Do you really want to leave?</p>
          <button onClick={confirmNavigation}>Yes</button>
          <button onClick={cancelNavigation}>No</button>
        </div>
      )}
    </>
  );
};

export default FormPage;
Enter fullscreen mode Exit fullscreen mode

3. Customize the Modal

You can replace the modal with a design from your favorite UI library (e.g., Material-UI, TailwindCSS).


Use Cases

  1. Form Protection

    Prevent users from accidentally leaving a page with unsaved changes.

  2. Critical Actions

    Confirm before performing destructive actions like deleting data.

  3. Route Guards

    Dynamically block navigation based on authentication or other app states.


Conclusion

useCallbackPrompt and useBlocker are invaluable tools for managing navigation in React applications. By combining React Router’s navigation API with custom logic, you can build robust user experiences that prioritize safety and usability.

Github Repo: react-navigation-blocker

Let me know if you try these hooks or have questions—happy coding! 🚀

Top comments (0)