Search code examples
reactjsreact-router-dom

React.JS react-router-dom v6.8 block navigation w/ custom modal


I'm currently working on implementing a custom modal to replace the use of window.confirm() in order to notify the user about any changes that might be lost if they navigate away from the current page. However, I've run into an issue where I'm receiving an error message stating that A router only supports one blocker at a time, and as a result, the modal isn't opening and the route isn't being changed back. I have seen many examples, but for v6.8 nothing is working so I was trying to implement it by myself but got stuck. I made an example as well. Thank you.

Here is the code

import React, { useState } from "react";
import {
  useBeforeUnload,
  unstable_useBlocker as useBlocker
} from "react-router-dom";
import { Modal, Button } from "react-bootstrap";

function usePrompt(message, { beforeUnload } = {}) {
  const [show, setShow] = useState(false);
  const handleClose = () => setShow(false);

  const handleConfirm = () => {
    blocker.allow();
    setShow(false);
  };

  let blocker = useBlocker(
    React.useCallback(() => {
      setShow(true);
      return true;
    }, [])
  );

  let prevState = React.useRef(blocker.state);
  React.useEffect(() => {
    if (blocker.state === "blocked") {
      blocker.reset();
    }
    prevState.current = blocker.state;
  }, [blocker]);

  useBeforeUnload(
    React.useCallback(
      (event) => {
        if (beforeUnload && blocker.state === "blocked") {
          event.preventDefault();
          event.returnValue = message;
        }
      },
      [message, beforeUnload, blocker.state]
    ),
    { capture: true }
  );

  return (
    <Modal show={show} onHide={handleClose}>
      <Modal.Header closeButton>
        <Modal.Title>Are you sure?</Modal.Title>
      </Modal.Header>
      <Modal.Body>{message}</Modal.Body>
      <Modal.Footer>
        <Button variant="secondary" onClick={handleClose}>
          Cancel
        </Button>
        <Button variant="primary" onClick={handleConfirm}>
          Confirm
        </Button>
      </Modal.Footer>
    </Modal>
  );
}

export default function Prompt({ when, message, ...props }) {
  usePrompt(when ? message : false, props);
  return null;
}

And usage

<Prompt
  when={true}
  message="If you leave changes will be lost. Continue?"
  beforeUnload={true}
/>

Solution

  • I've been trying to achieve a similar result for a while now and I think I finally figured it out. I found this example after I figured it out, which will definitely be of help:

    https://stackblitz.com/github/remix-run/react-router/tree/main/examples/navigation-blocking?file=src%2Fapp.tsx

    I've run into an issue where I'm receiving an error message stating that A router only supports one blocker at a time

    This is a known bug from 6.8.1, so I am still using 6.8.0 and it works fine. https://github.com/remix-run/react-router/issues/10073

    Also be aware that unstable_useBlocker is seems to be only for Single Page Applications as well indicated by the xmldoc

    Allow the application to block navigations within the SPA and present the user a confirmation dialog to confirm the navigation. Mostly used to avoid using half-filled form data. This does not handle hard-reloads or cross-origin navigations.

    I've gone and modified your sandbox a little bit to make it work, including downgrading to 6.8.0 to remove that pesky warning. When you try to navigate and a blocker is present and enabled, the blocker sets the state to "Blocked". The state is not set when you set it to true on initialisation. I believe you have to use useNavigation as well for the unstable_useBlocker to work. It was very confusing for me at first. See here:

    https://codesandbox.io/s/react-router-6-8-blocker-qmvie0

    The example has a bug when you cancel the modal, you cannot click the same link, but I do not think it has to do with the router or blocker (It seems that the nav link becomes "active" or something when clicked, so they cannot be clicked again)

    export default function Prompt({ message }) {
    let blocker = useBlocker(true); //useBlocker(isFormDirty)
    
    //in ts, if blocker.state() === "blocked", else blocker.reset() and proceed() can be undefined
      const handleClose = () => blocker.reset(); 
      const handleConfirm = () => blocker.proceed();
    
      return (
        <Modal show={blocker.state === "blocked"} onHide={handleClose}>
          <Modal.Header closeButton>
            <Modal.Title>Are you sure?</Modal.Title>
          </Modal.Header>
          <Modal.Body>{message}</Modal.Body>
          <Modal.Footer>
            <Button variant="secondary" onClick={handleClose}>
              Cancel
            </Button>
            <Button variant="primary" onClick={handleConfirm}>
              Confirm
            </Button>
          </Modal.Footer>
        </Modal>
      );
    }