Bootstrap

react-router 6 中实现 Prompt ,阻止页面切换与关闭

react-router6 中将 prompt 及其相关的处理方法去除掉了,但是在一些时候又必须使用。所以,有必要了解一下如何解决这个问题。
仅限于浏览器环境
git地址: https://gitee.com/wkjgit/prompt.git
npm地址: https://www.npmjs.com/package/react-router6-prompt
补充: 已添加 HashRouter 支持,并发布到 npm 上。

由于 react-router6 是以这样的形式使用:

import {BrowserRouter} from 'react-router-dom';
import App from './App';
ReactDOM.render(
  <React.StrictMode>
    <BrowserRouter>
      <App />
    </BrowserRouter>
  </React.StrictMode>,
  document.getElementById('root')
);

但是 react-router 或者 react-router-dom 都没有导出 history 模块,所以,我们需要重新定义一下 BrowserRouter 组件,用来将 history 导出,并在后面使用此处的 history 来实现具体的页面转换拦截。

自定义 BrowserRouter 导出 history

import React from 'react';
import { createBrowserHistory } from "history";
import type { BrowserHistory } from "history";

import { Router } from "react-router";

export interface BrowserRouterProps {
    basename?: string;
    children?: React.ReactNode;
    window?: Window;
}

const browserHistory = createBrowserHistory({ window });

const BrowserRouter: React.FC = ({
    basename,
    children,
    window,
}: BrowserRouterProps) => {
    let historyRef = React.useRef<BrowserHistory>();
    if (historyRef.current == null) {
        historyRef.current = browserHistory;
    }

    let history = historyRef.current;
    let [state, setState] = React.useState({
        action: history.action,
        location: history.location,
    });

    React.useLayoutEffect(() => history.listen(setState), [history]);

    return (
        <Router
            basename={basename}
            children={children}
            location={state.location}
            navigationType={state.action}
            navigator={history}
        />
    );
}

export {
    browserHistory as history,
    BrowserRouter
}

这个组件在原有的基础上只是简单的将 history 导出了,仅此而已。

创建 Prompt 组件,用来实现具体的页面切换/卸载限制

import React, { useEffect } from 'react';
import {history} from './BrowserRouter';
import {Blocker, Location} from 'history';
import {useLocation, useNavigate} from 'react-router-dom';

type Props = {
    when: boolean;
    message?: string | (() => boolean) | (() => Promise<boolean>);
}

const Prompt: React.FC<Props> = (props) => {
    const location = useLocation();
    const navigate = useNavigate();

    // 存储关闭阻止页面切换的方法(调用此方法将关闭阻止页面切换)
    let unblock: any = null;

    // 阻止页面卸载
    const beforeUnload = (event: any) => {
        event.preventDefault();
        event.returnValue = '';
    }

    // 页面切换时的回调
    const handlePageChange: Blocker = async ({location: nextLocation}) => {
        // 是否关闭切换限制并跳转
        let toNext: boolean = false;

        if (props.message) {
            if (typeof props.message === "string") {
                toNext = confirm(props.message);
            } else {
                toNext = await props.message();
            }
        } else {
            toNext = confirm("是否放弃更改");
        }

        toNext && closeBlockAndNavigate(nextLocation);
    }

    // 关闭阻止页面切换
    const closeBlockPageSwitching = () => {
        if (unblock) {
            unblock();
            unblock = null;
            window.removeEventListener("beforeunload", beforeUnload);
        }
    }

    // 关闭阻止页面切换,并跳转
    const closeBlockAndNavigate = (nextLocation: Location) => {
        closeBlockPageSwitching();
        navigate(nextLocation);
    }

    // 监听when 和 pathname 变化,当发生变化时判断是否需要开启block navigate.
    useEffect(() => {
        if (props.when) {
            // 阻塞页面跳转(history行为)
            unblock = history.block(handlePageChange);
            window.addEventListener('beforeunload', beforeUnload);
        }
        return () => {
            props.when && closeBlockPageSwitching();
        }
    }, [props.when, location.pathname]);

    return (
        <></>
    );
}

export default Prompt;

使用

import { useState } from 'react'
import logo from './logo.svg'
import './App.css'
import Prompt from './components/Prompt'
import {NavLink, useRoutes, RouteObject} from 'react-router-dom';

const Home = () => {
  return (
    <div>home</div>
  );
}

const About = () => {
  return (
    <div>about</div>
  );
}

const routes: RouteObject[] = [
  {
    path: '/',
    element: <Home />
  },
  {
    path: '/about',
    element: <About />
  }
];

function App() {
  const [count, setCount] = useState<boolean>(false);
  const router = useRoutes(routes);

  return (
    <div className="App">
      <NavLink to="/">HOME</NavLink>
      <NavLink to="/about">ABOUT</NavLink>
      <button onClick={() => { setCount(!count) }}>测试</button>
      <Prompt when={count} message={() => {
        alert();
        return true;
      }} />
      {router}
    </div>
  )
}
export default App;

这里只是提供了一个实现思路,当然直接使用也是没问题的。
在目前 react-router6 还未支持 Prompt 相关事宜时,也是一个不错的解决方法。

;