本文介绍了如何实现一个FormPrompt组件,在用户尝试离开具有未保存更改的页面时发出警告。文章讨论了如何使用纯JavaScript和beforeunload事件处理这类情况,以及使用React Router v5中的Prompt组件和useBeforeUnload以及unstable等React特定解决方案。向用户添加一个确认对话框,询问他们在具有未保存表单更改的情况下是否确认重定向是一种良好的用户体验实践。通过显示此提示,用户将意识到他们有未保存的更改,并允许在继续重定向之前保存或丢弃它们的工作。
下面是正文:
在今天的数字化环境中,为涉及表单提交的 Web 应用程序提供最佳用户体验非常重要。用户常见的一个烦恼来源是由于意外离开页面而丢失未保存的更改。
本文将演示如何实现一个 FormPrompt 组件,当用户尝试离开具有未保存更改的页面时,会发出警报,从而有效地提高整体用户体验。我们将讨论如何使用纯 JavaScript 处理此类情况,使用 React Router v5 中的 Prompt 组件以及在 React Router v6 中使用 useBeforeUnload 和 unstable_useBlocker 钩子的特定解决方案。
应用程序的最终版本可以在 CodeSandbox 上进行测试,代码可在 GitHub 上获得。
使用 beforeunload 事件检测页面离开
我们创建 FormPrompt 组件,在其中添加 beforeunload 事件的监听器。此事件将在用户离开页面之前触发。通过在事件上调用 preventDefault 方法,我们可以触发浏览器的确认对话框。仅当表单具有未保存的更改(由 hasUnsavedChanges 属性指示)时,才会激活此对话框。
// FormPrompt.js
import { useEffect } from "react";
export const FormPrompt = ({ hasUnsavedChanges }) => {
useEffect(() => {
const onBeforeUnload = (e) => {
if (hasUnsavedChanges) {
e.preventDefault();
e.returnValue = "";
}
};
window.addEventListener("beforeunload", onBeforeUnload);
return () => {
window.removeEventListener("beforeunload", onBeforeUnload);
};
}, [hasUnsavedChanges]);
};
作为示例,我们将在表单的 Contact 步骤中使用此组件:
// Steps/Contact.js
import { forwardRef } from "react";
import { useForm } from "react-hook-form";
import { useAppState } from "../state";
import { Button, Field, Form, Input } from "../Forms";
import { FormPrompt } from "../FormPrompt";
export const Contact = forwardRef((props, ref) => {
const [state, setState] = useAppState();
const {
handleSubmit,
register,
formState: { isDirty },
} = useForm({
defaultValues: state,
mode: "onSubmit",
});
const saveData = (data) => {
setState({ ...state, ...data });
};
return (
<Form onSubmit={handleSubmit(saveData)} nextStep={"/education"}>
<FormPrompt hasUnsavedChanges={isDirty} />
<fieldset>
<legend>Contact</legend>
<Field label="First name">
<Input {...register("firstName")} id="first-name" />
</Field>
<Field label="Last name">
<Input {...register("lastName")} id="last-name" />
</Field>
<Field label="Email">
<Input {...register("email")} type="email" id="email" />
</Field>
<Field label="Password">
<Input {...register("password")} type="password" id="password" />
</Field>
<Button ref={ref}>Next {">"}</Button>
</fieldset>
</Form>
);
});
当在表单字段中输入数据并在保存更改之前尝试重新加载页面或导航到外部URL时,浏览器将显示确认对话框。
使用React Router 5防止页面导航
这个组件已经足够好用于我们的应用程序,因为它的所有页面都是表单的一部分。然而,在实际情况下,这并不总是如此。为了使我们的示例更具代表性,我们添加一个名为 Home 的新路由,它将重定向到表单之外。 Home 组件很简单,只显示一个主页问候语。
// Home.js
export const Home = () => {
return <div>Welcome to the home page!</div>;
};
我们还需要对 App 组件进行一些调整,以适应这条新路由。
// App.js
import { useRef } from "react";
import {
BrowserRouter as Router,
Routes,
Route,
NavLink,
} from "react-router-dom";
import { AppProvider } from "./state";
import { Contact } from "./Steps/Contact";
import { Education } from "./Steps/Education";
import { About } from "./Steps/About";
import { Confirm } from "./Steps/Confirm";
import { Stepper } from "./Steps/Stepper";
import { Home } from "./Home";
export const App = () => {
const buttonRef = useRef();
const onStepChange = () => {
buttonRef.current?.click();
};
return (
<div className="App">
<AppProvider>
<Router>
<div className="nav-wrapper">
<NavLink to={"/"}>Home</NavLink>
<Stepper onStepChange={onStepChange} />
</div>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/contact" element={<Contact ref={buttonRef} />} />
<Route path="/education" element={<Education ref={buttonRef} />} />
<Route path="/about" element={<About ref={buttonRef} />} />
<Route path="/confirm" element={<Confirm />} />
</Routes>
</Router>
</AppProvider>
</div>
);
};
我们可以看到当我们在表格中输入信息并导航到主页时,输入的数据不会被保存,也不会出现任何确认对话框。这是因为导航由React Router处理,不会触发 beforeunload 事件,使浏览器API在这种情况下无效。幸运的是,React Router v5提供了 Prompt 组件,以在离开未保存更改的页面之前警告用户。该组件接受两个props: when 和 message 。 when 属性是一个布尔值,用于确定是否应该显示提示,而 message 属性表示向用户显示的文本。
使用 Prompt 时,导航到主页路由时行为正确,但是当用户输入表单数据并进入下一步时,确认对话框也会出现。这是不希望的,因为我们在导航到下一步时保存表单数据。
为了解决这个问题,我们需要验证下一个 URL 是否是表单步骤之一,然后再检查未保存的更改。可以使用 message 属性来实现这一点,它也可以是一个函数。该函数的第一个参数是下一个位置。如果函数返回 true ,则允许转换到下一个 URL;否则,它可以返回一个字符串来显示提示。
// FormPrompt.js
import { useEffect } from "react";
import { Prompt } from "react-router-dom";
const stepLinks = ["/contact", "/education", "/about", "/confirm"];
export const FormPrompt = ({ hasUnsavedChanges }) => {
useEffect(() => {
const onBeforeUnload = (e) => {
if (hasUnsavedChanges) {
e.preventDefault();
e.returnValue = "";
}
};
window.addEventListener("beforeunload", onBeforeUnload);
return () => {
window.removeEventListener("beforeunload", onBeforeUnload);
};
}, [hasUnsavedChanges]);
const onLocationChange = (location) => {
if (stepLinks.includes(location.pathname)) {
return true;
}
return "You have unsaved changes, are you sure you want to leave?";
};
return <Prompt when={hasUnsavedChanges} message={onLocationChange} />;
};
通过这些更改,我们可以安全地在表单步骤之间导航,并在尝试离开未保存更改的表单时收到警告。
使用 React Router 6 防止页面导航
件已被移除,而 unstable_usePrompt 钩子在 6.7.0 版本中被添加。正如其名称所示,该钩子的实现可能会发生变化,尚未记录文档。但是,它应该适用于我们的使用情况。
我们可以使用这个钩子来复制版本5中 Prompt 组件的行为,但首先,我们需要调整我们的 App 组件以使用新的数据路由器,因为它们是 unstable_usePrompt 钩子工作所必需的。
// App.js
import { useRef } from "react";
import { createBrowserRouter, RouterProvider, Outlet } from "react-router-dom";
import { AppProvider } from "./state";
import { Contact } from "./Steps/Contact";
import { Education } from "./Steps/Education";
import { About } from "./Steps/About";
import { Confirm } from "./Steps/Confirm";
import { Stepper } from "./Steps/Stepper";
import { Home } from "./Home";
export const App = () => {
const buttonRef = useRef();
const onStepChange = () => {
buttonRef.current?.click();
};
const router = createBrowserRouter([
{
element: (
<>
<Stepper onStepChange={onStepChange} />
<Outlet />
</>
),
children: [
{
path: "/",
element: <Home />,
},
{
path: "/contact",
element: <Contact ref={buttonRef} />,
},
{ path: "/education", element: <Education ref={buttonRef} /> },
{ path: "/about", element: <About ref={buttonRef} /> },
{ path: "/confirm", element: <Confirm /> },
],
},
]);
return (
<div className="App">
<AppProvider>
<RouterProvider router={router} />
</AppProvider>
</div>
);
};
我们使用 createBrowserRouter 函数来创建路由器。请注意, Stepper 没有单独的路径,所有其他路由都是它的子路由。它作为布局组件,在每个页面上呈现。每个页面的内容显示在特殊的 Outlet 组件的位置。为了简化 App 逻辑,我们还将主页导航链接移动到 Stepper 中。
设置完成后,我们现在可以实现重定向阻止功能。我们首先通过在 FormPrompt 中使用在6.6版本中引入的 useBeforeUnload 钩子来替换 onbeforeunload 逻辑。
// FormPrompt.js
import { useEffect, useCallback, useRef } from "react";
import { useBeforeUnload } from "react-router-dom";
const stepLinks = ["/contact", "/education", "/about", "/confirm"];
export const FormPrompt = ({ hasUnsavedChanges }) => {
useBeforeUnload(
useCallback(
(event) => {
if (hasUnsavedChanges) {
event.preventDefault();
event.returnValue = "";
}
},
[hasUnsavedChanges]
),
{ capture: true }
);
return null;
};
这个改变简化了我们组件的逻辑。现在,我们可以添加一个自定义的 usePrompt 钩子,并像版本5中的 Prompt 组件一样使用它。
// FormPrompt.js
import { useEffect, useCallback, useRef } from "react";
import {
useBeforeUnload,
unstable_useBlocker as useBlocker,
} from "react-router-dom";
const stepLinks = ["/contact", "/education", "/about", "/confirm"];
export const FormPrompt = ({ hasUnsavedChanges }) => {
const onLocationChange = useCallback(
({ nextLocation }) => {
if (!stepLinks.includes(nextLocation.pathname) && hasUnsavedChanges) {
return !window.confirm(
"You have unsaved changes, are you sure you want to leave?"
);
}
return false;
},
[hasUnsavedChanges]
);
usePrompt(onLocationChange, hasUnsavedChanges);
useBeforeUnload(
useCallback(
(event) => {
if (hasUnsavedChanges) {
event.preventDefault();
event.returnValue = "";
}
},
[hasUnsavedChanges]
),
{ capture: true }
);
return null;
};
function usePrompt(onLocationChange, hasUnsavedChanges) {
const blocker = useBlocker(hasUnsavedChanges ? onLocationChange : false);
const prevState = useRef(blocker.state);
useEffect(() => {
if (blocker.state === "blocked") {
blocker.reset();
}
prevState.current = blocker.state;
}, [blocker]);
}
useBlocker 钩子接受布尔值或阻止函数作为其参数,类似于 Prompt 组件中的 message 属性。该函数的一个参数是下一个位置,我们使用它来确定用户是否正在离开我们的表单。如果是这种情况,我们利用浏览器的 window.confirm 方法显示一个对话框,询问用户确认重定向或取消它。最后,我们在 usePrompt 钩子中抽象出阻止逻辑并管理阻止器的状态。
我们可以通过导航到联系步骤,填写一些字段并单击主页导航项来测试 FormPrompt 是否按预期工作。我们会看到一个确认对话框,询问我们是否要离开该页面。
总结
总之,为未保存的表单更改实现确认对话框是增强用户体验的重要实践。本文演示了如何创建一个 FormPrompt 组件,当用户尝试离开具有未保存更改的页面时,该组件会向用户发出警告。我们探讨了如何使用纯JavaScript处理这种情况,使用 beforeunload 事件以及在React中使用React Router v5中的 Prompt 组件和React Router v6中的 useBeforeUnload 和 unstable_useBlocker 钩子。通过将此功能合并到您的表单中,你可以帮助用户避免失去未保存的工作而感到沮丧。
本文转载自微信公众号「大迁世界」,可以通过以下二维码关注。转载本文请联系大迁世界公众号。