简体   繁体   中英

React Router v5.2 - Blocking route change with createBrowserHistory and history.block

My app has two pages: Step1 and Step2 . Step1 has a checkbox that blocks navigation if it is checked and a Next button that navigates to Step2 when clicked. Step2 has a Previous button that navigates back to Step1 when clicked.

Link to demo

As per this tutorial , I'm using the block method of the createBrowserHistory object to block route changes if the checkbox in Step1 is checked:

const unblock = useRef();

  useEffect(() => {
    unblock.current = history.block((tx) => {
      if (block.current.checked) {
        const promptMessage = "Are you sure you want to leave?";

        if (window.confirm(promptMessage)) {
          unblock.current();
          tx.retry();
        }
      } else {
        console.log("tfgx");
        unblock.current();
        tx.retry();
      }
    });
  }, []);

I also had to set the history prop in the low-level <Router> (not <BrowserRouter> ) to the createBrowserHistory object, like so:

<Router history={createBrowserHistory()}>
...
</Router>

But this prevents the routes from being rendered properly . I think this may have something to do with <Switch> not being able to read the location object properly. If I use <BrowserRouter> , the location object looks like this: {pathname: "/step1", ... key: "7sd45"} . But when I use <Router={createBrowserHistory()}> , the location object looks like this {action: "PUSH", location: {pathname: "/step1", ... key: "7sd45"}} . (I'm also getting the warning You cannot change <Router history> .)

My desired result is to block navigation if the 'Block navigation' checkbox is checked and unblock it otherwise. If the location changes when navigation is unblocked, I would like the corresponding route to be rendered correctly.

The section on createBrowserHisory in the React Router v5 docs is sparse and there aren't many examples that make use of it, so I'd be grateful if someone could shed some light on this.


EDIT: Passing location.location to <Switch> seems to fix it ( Updated demo ). But if I call useLocation inside Step1 and print the result (line 17-18), I get {pathname: "/step1", ... key: "7sd45"} and not {action: "PUSH", location: {pathname: "/step1", ... key: "7sd45"}} . Why is this?

Also, if the user attempts to go to another location when navigation is blocked, my custom prompt appears as expected ("Are you sure you want to leave" with "OK" and "Cancel" buttons). However, if they dismiss this by clicking Cancel, then the browser's own dialog box appears -

In Chrome:

在此处输入图像描述

In Firefox:

在此处输入图像描述

Is it possible to suppress the browser prompt after my prompt has been dismissed?

The router context's history object also has a block function but it works a little differently. It takes a callback that consumes location and action arguments.

history.block((location, action) => {...});

Returning false from the callback blocks the navigation transition, returning true allows the transition to go through.

React.useEffect(() => {
  const unblock = history.block((location, action) => {
    if (checkBlockingCondition) {
      return window.confirm("Navigate Back?");
    }
    return true;
  });

  return () => {
    unblock();
  };
}, []);

Alternatively, react-router-dom suggests using the Prompt component to conditionally block route transitions. Your code is very close to their preventing transitions example.

Updates to your last codesandbox:

  1. Use blocking state versus react ref so the prompt rerenders and reevaluates the condition.
  2. Render a Prompt component.
  3. Prevent the default form submit action, ie to prevent the page from reloading.

code

import {
  BrowserRouter as Router,
  Prompt, // <-- import Prompt
  Redirect,
  Switch,
  Route,
  useLocation
} from "react-router-dom";

const Step1 = ({ id, history }) => {
  const [isBlocking, setIsBlocking] = useState(false);

  return (
    <form
      id={id}
      onSubmit={(e) => {
        e.preventDefault(); // <-- prevent default form action, i.e. page reload
        history.push("/step2");
      }}
    >
      <label>
        Block navigation
        <input
          type="checkbox"
          onChange={(e) => setIsBlocking(e.target.checked)}
        />
      </label>
      <br />
      <br />
      <button type="submit">Next</button>
      <Prompt
        when={isBlocking} // <-- blocking condition
        message="Are you sure you want to leave?"
     />
    </form>
  );
};

编辑 react-router-v5-2-blocking-route-change-with-createbrowserhistory-and-history

To do this in typescript, you could use a ternary based on your "dirty" condition to show the prompt before navigating to your next route.

As long as the component this goes in is inside of aa routing context, it will work.

isChecked is a psuedo-code variable to be replaced by whatever condition you want to evaluate on leaving the page. I recommend having it in the dependencies because when the state variable updates, so will the block condition.

  const history = useHistory();

  useEffect(() => {
    const unblock = history.block(
      !isChecked
        ? "You’ve got unsaved changes. Are you sure you want to navigate away from this page?"
        : true
    );

    return function cleanup() {
      unblock();
    };
  }, [isChecked]);

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM