React-Router 6 的 useBlocker 阻止应用程序外导航

时间:2021-03-04 13:35:21

标签: reactjs react-router react-router-dom react-router-v6

我有一个基于 React-Router v6 的应用程序。我创建了一个组件,如果安装了该组件,将阻止用户使用“返回”按钮。如果他们决定继续,它会重置应用并将用户重定向到主页。

import { useCallback } from 'react';
import { useBlocker, useLocation, useNavigate } from 'react-router';

const locationProperties = ['pathname', 'search', 'state'];

function isSameLocation(location1, location2) {
  return locationProperties.every((property) => location1[property] === location2[property]);
}

export default function NavigationGuard() {
  const location = useLocation();
  const navigate = useNavigate();

  const blocker = useCallback(
    ({ action, location: nextLocation, retry }) => {
      switch (action) {
        case 'PUSH':
        case 'REPLACE': {
          retry();
          return;
        }
        case 'POP': {
          if (isSameLocation(nextLocation, location)) {
            retry();
            return;
          }

          const answer = confirm('Are you sure you want to leave this page?');

          if (answer) {
            navigate('/');
          }

          return;
        }
      }
    },
    [location, navigate],
  );

  useBlocker(blocker);

  return null;
}

这很完美,有一个小问题:当用户点击外部链接时,会出现一个要求确认的提示。这是无意的。

我进行了一些调查,似乎每当页面上存在(并处于活动状态)useBlocker 时,history 包都会添加 onBeforeUnload 侦听器,从而导致出现所述不需要的提示。

我能想到的唯一解决方案是侦听即将被点击的链接上的事件,并在实际点击之前禁用 useBlocker,但这似乎太糟糕了。

import { useCallback, useState } from 'react';
import { useBlocker, useLocation, useNavigate } from 'react-router';
import { useEventListener } from '@wojtekmaj/react-hooks';

import { closest } from 'utils/dom';

const locationProperties = ['pathname', 'search', 'state'];

function isSameLocation(location1, location2) {
  return locationProperties.every((property) => location1[property] === location2[property]);
}

function isSameOrigin(location1, location2) {
  return new URL(location1).origin === new URL(location2).origin;
}

export default function NavigationGuard() {
  const location = useLocation();
  const navigate = useNavigate();
  const [isExternalLinkActive, setIsExternalLinkActive] = useState(false);

  function onActionStart(event) {
    const link = closest(event.target, 'a');

    if (!link || isSameOrigin(document.location, link.href)) {
      return;
    }

    setIsExternalLinkActive(true);
  }

  function onActionEnd() {
    if (!isExternalLinkActive) {
      return;
    }

    setImmediate(() => {
      setIsExternalLinkActive(false);
    });
  }

  useEventListener(document, 'pointerdown', onActionStart);
  useEventListener(document, 'focusin', onActionStart);

  useEventListener(document, 'pointerdown', onActionEnd);
  useEventListener(document, 'blur', onActionEnd);

  const blocker = useCallback(
    ({ action, location: nextLocation, retry }) => {
      switch (action) {
        case 'PUSH':
        case 'REPLACE': {
          retry();
          return;
        }
        case 'POP': {
          if (isSameLocation(nextLocation, location)) {
            retry();
            return;
          }

          const answer = confirm('Are you sure you want to leave this page?');

          if (answer) {
            navigate('/');
          }

          return;
        }
      }
    },
    [location, navigate],
  );

  useBlocker(blocker, !isExternalLinkActive);

  return null;
}

有没有更好的方法来做到这一点?

0 个答案:

没有答案
相关问题