在 React 中控制浏览器后退按钮



我想让我的 Web 应用程序像移动应用程序一样工作。这意味着当用户按下后退时,他们希望弹出窗口关闭,而不是整个页面都会更改。

我的最终目标是使当模态打开时,"后退"按钮现在将关闭模态,如果他们再次单击它,它将返回。

我已经尝试了几种方法,尽管它们很接近,但它们从未始终如一地响应。 https://codesandbox.io/s/github/subwaymatch/react-disable-back-button-example-v2

有人有我正在寻找的经过验证的工作版本吗?

实际上,我相信后退功能对用户体验很有用,但对于模式打开/关闭,你是对的。 浏览器后退按钮应关闭桌面和移动设备中的模式。我为您提供两个帮助程序函数,一个用于中和浏览器后退按钮,然后运行您自己的功能另一个用于恢复浏览器后退按钮。打开模态时使用neutralizeBack函数,在打开的模态关闭时使用revivalBack函数。使用第二个回到我对浏览器后退按钮功能的用户体验的态度。

  • neutralizeBack应运行回调函数。 此回调函数是您要执行的操作:

    const neutralizeBack = (callback) => {
    window.history.pushState(null, "", window.location.href);
    window.onpopstate = () => {
    window.history.pushState(null, "", window.location.href);
    callback();
    };
    };
    
  • 当您
  • 想要恢复浏览器后退按钮功能时,应运行revivalBack

    const revivalBack = () => {
    window.onpopstate = undefined;
    window.history.back();
    };
    

用法示例:

handleOpenModal = () =>
this.setState(
{ modalOpen: true },
() => neutralizeBack(this.handleCloseModal)
);
handleCloseModal = () =>
this.setState(
{ modalOpen: false },
revivalBack
);

您可以尝试在URL中使用哈希。 哈希是以主题标签开头的 URL 段。在哈希之间导航通常不会触发任何页面加载,但仍然会向浏览器历史记录推送一个条目,使后退按钮能够关闭模式/弹出窗口。

// www.example.com#modal
window.location.hash // -> "#modal"

显示和隐藏的模态状态基于window.location.hash

你可以创建一个这样的钩子(仅用于抽象)

function useHashRouteToggle(modalHash) {
const [isOpen, toggleOpen] = useState(false);
const toggleActive = (open) => {
if (open) {
window.location.assign(modalHash); // navigate to same url but with the specified hash
} else {
window.location.replace('#'); // remove the hash
}
}
useEffect(() => { 
// function for handling hash change in browser, toggling modal open 
const handleOnHashChange = () => {  
const isHashMatch = window.location.hash === modalHash;   
toggleOpen(isHashMatch);  
};  
// event listener for hashchange event
window.addEventListener('hashchange', handleOnHashChange);  

return () => window.removeEventListener('hashchange', handleOnHashChange);  
}, [modalHash]);
return [isActive, toggleActive];
} 

然后在弹出窗口/模态上使用它。

const [isActive, toggleActive] = useHashRouteToggle('#modal');
const openModal = () => toggleActive(true);
<Modal isShow={isActive} />

这样,您可以在不修改或覆盖浏览器行为的情况下实现您的需求。上面的代码只是为了抽象你可以做什么。您可以根据需要对其进行优化。希望它能给你一些想法。

if (isOpen) {
// push to history when modal opens
window.history.pushState(null, '', window.location.href)

// close modal on 'back'
window.onpopstate = () => {
window.onpopstate = () => {}
window.history.back()
setIsOpen(false)
}
}
return <Modal open={isOpen} />

为了使后退按钮在模态关闭时起作用,您需要在打开模态时按下一条路线,并且在关闭时您可以使用 history.goBack()。也许这个例子可能会有所帮助。

import React from "react";
import {
BrowserRouter as Router,
Switch,
Route,
Link,
useHistory,
useLocation,
useParams
} from "react-router-dom";
export default function ModalGalleryExample() {
return (
<Router>
<ModalSwitch />
</Router>
);
}
function ModalSwitch() {
let location = useLocation();
let background = location.state && location.state.background;
return (
<div>
<Switch location={background || location}>
<Route exact path="/" children={<Gallery />} />
<Route path="/img/:id" children={<ImageView />} />
</Switch>
{background && <Route path="/img/:id" children={<Modal />} />}
</div>
);
}
const IMAGES = [
{ id: 0, title: "Dark Orchid", color: "DarkOrchid" },
{ id: 1, title: "Lime Green", color: "LimeGreen" },
{ id: 2, title: "Tomato", color: "Tomato" },
{ id: 3, title: "Seven Ate Nine", color: "#789" },
{ id: 4, title: "Crimson", color: "Crimson" }
];
function Thumbnail({ color }) {
return (
<div
style={{
width: 50,
height: 50,
background: color
}}
/>
);
}
function Image({ color }) {
return (
<div
style={{
width: "100%",
height: 400,
background: color
}}
/>
);
}
function Gallery() {
let location = useLocation();
return (
<div>
{IMAGES.map(i => (
<Link
key={i.id}
to={{
pathname: `/img/${i.id}`,
// This is the trick! This link sets
// the `background` in location state.
state: { background: location }
}}
>
<Thumbnail color={i.color} />
<p>{i.title}</p>
</Link>
))}
</div>
);
}
function ImageView() {
let { id } = useParams();
let image = IMAGES[parseInt(id, 10)];
if (!image) return <div>Image not found</div>;
return (
<div>
<h1>{image.title}</h1>
<Image color={image.color} />
</div>
);
}
function Modal() {
let history = useHistory();
let { id } = useParams();
let image = IMAGES[parseInt(id, 10)];
if (!image) return null;
let back = e => {
e.stopPropagation();
history.goBack();
};
return (
<div
onClick={back}
style={{
position: "absolute",
top: 0,
left: 0,
bottom: 0,
right: 0,
background: "rgba(0, 0, 0, 0.15)"
}}
>
<div
className="modal"
style={{
position: "absolute",
background: "#fff",
top: 25,
left: "10%",
right: "10%",
padding: 15,
border: "2px solid #444"
}}
>
<h1>{image.title}</h1>
<Image color={image.color} />
<button type="button" onClick={back}>
Close
</button>
</div>
</div>
);
}

有关参考,请查看反应路由器模态库示例

这是我版本的 Dimitrij Agal 答案 ,带有实际工作代码,而不仅仅是伪代码。它使用"react-router-dom": "^6.0.0-beta.0"

import { useEffect, useState } from "react";
import { useNavigate, useLocation } from "react-router-dom";


export function useHashRouteToggle(hash) {
const navigate = useNavigate();
const location = useLocation();
const [isActive, setIsActive] = useState(false);
const toggleActive = (bool) => {
if (bool !== isActive) {   // needed if there are multiple modals with close-on-esc-keyup in the same page
if (bool) {
navigate(location.pathname + "#" + hash)
} else {
navigate(-1);
}
setIsActive(bool);
}
}
useEffect(() => { 
const handleOnHashChange = () => {  
setIsActive(false);
};  
window.addEventListener('hashchange', handleOnHashChange);  

return () => window.removeEventListener('hashchange', handleOnHashChange);  
});
return [isActive, toggleActive];
} 

你像这样使用它:

const [showModalDelete, setShowModalDelete] = useHashRouteToggle("delete")
// ...
<CoreModal
isActive={showModalDelete}
setIsActive={setShowModalDelete}
title={t("deleteProduct")}
content={modalContent}
/>

但是,至少存在 2 个问题:

  • 如果用户在关闭模态后使用"前进"按钮,他/她将不得不按两次"返回"按钮。
  • 我试图将模态的初始状态作为参数传递,以防程序员想要启动打开的模态(isActive === true),但我无法让它工作,尽管我认为我没有探索太多这种可能性,因为我所有的模态都是关闭的。

任何反馈将不胜感激

我的版本基于AmerllicA的答案。

基本上,当您打开模态时,您会按状态(就好像模态是不同的页面一样),然后当您关闭模态时,您会弹出它,除非它已经被导航弹出。

onModalOpen() {
window.history.pushState(null, '', window.location.href)
window.onpopstate = () => {
this.onModalClose(true)
}
}
onModalClose(fromNavigation) {
if(!fromNavigation)
window.history.back()
window.onpopstate = () => {
// Do nothing
}
}

我试图用模态打开/关闭做完全相同的事情,让用户通过前进按钮打开模态并通过后退按钮关闭它。

我看到了所有的答案,但我认为最好用hook

这是我最终得到的一个钩子。

当模态状态设置为打开时,我替换当前历史状态popstate因为事件为您提供当前页面的状态,并在页面加载后调用(见此处),我也在模态打开时推送一个新状态,

所以现在我们在历史上有 2 种状态,第一个是closeModal第二个是openModal,现在当用户更改历史记录时,我们可以知道我们需要做什么(打开或关闭模态)。

export function useModalHistory(
id: string,
isOpen: boolean,
onChange: (open: boolean) => void,
) {
useEffect(() => {
if (id && isOpen) {
// set new states to history when isOpen is true
// but we need to check isOpen happened from `popstate` event or not
// so we can prevent loop
if (window.history.state?.openModal !== id) {
window.history.replaceState({closeModal: id}, '');
window.history.pushState({openModal: id}, '', window.location.href);
}
return () => {
// only close modal if the closing is not from `popstate` event
if (window.history.state?.closeModal !== id) window.history.back();
};
}
}, [id, isOpen]);
useEventListener('popstate', event => {
if (event.state?.closeModal === id) {
onChange(false);
}
if (event.state?.openModal === id) {
onChange(true);
}
});
}

另请注意,我使用了 https://usehooks-ts.com/react-hook/use-event-listener 中的useEventListener,您可以创建钩子或从包中使用它。

如果你使用react-router你可以这样写


export function useModalHistory(
id: string | undefined,
isOpen: boolean,
onChange: (open: boolean) => void,
) {
const history = useHistory<{openModal?: string; closeModal?: string}>();
useEffect(() => {
if (id && isOpen) {
if (history.location.state?.openModal !== id) {
history.replace({state: {closeModal: id}});
history.push({state: {openModal: id}});
}
return () => {
if (history.location.state?.closeModal !== id) history.goBack();
};
}
}, [id, isOpen, history]);
useEventListener('popstate', event => {
if (id) {
if (event.state.state?.closeModal === id) {
onChange(false);
}
if (event.state.state?.openModal === id) {
onChange(true);
}
}
});
}

用法

const [isModalOpen, setIsModalOpen] = useState(false);
// be aware id need to be unique for each modal
useModalHistory('my_modal', isModalOpen, setIsModalOpen);

最新更新