離開頁面前,如何防止表單數(shù)據(jù)丟失?
本文介紹了如何實現(xiàn)一個FormPrompt組件,在用戶嘗試離開具有未保存更改的頁面時發(fā)出警告。文章討論了如何使用純JavaScript和beforeunload事件處理這類情況,以及使用React Router v5中的Prompt組件和useBeforeUnload以及unstable等React特定解決方案。向用戶添加一個確認對話框,詢問他們在具有未保存表單更改的情況下是否確認重定向是一種良好的用戶體驗實踐。通過顯示此提示,用戶將意識到他們有未保存的更改,并允許在繼續(xù)重定向之前保存或丟棄它們的工作。
下面是正文:
在今天的數(shù)字化環(huán)境中,為涉及表單提交的 Web 應(yīng)用程序提供最佳用戶體驗非常重要。用戶常見的一個煩惱來源是由于意外離開頁面而丟失未保存的更改。
本文將演示如何實現(xiàn)一個 FormPrompt 組件,當(dāng)用戶嘗試離開具有未保存更改的頁面時,會發(fā)出警報,從而有效地提高整體用戶體驗。我們將討論如何使用純 JavaScript 處理此類情況,使用 React Router v5 中的 Prompt 組件以及在 React Router v6 中使用 useBeforeUnload 和 unstable_useBlocker 鉤子的特定解決方案。
應(yīng)用程序的最終版本可以在 CodeSandbox 上進行測試,代碼可在 GitHub 上獲得。
使用 beforeunload 事件檢測頁面離開
我們創(chuàng)建 FormPrompt 組件,在其中添加 beforeunload 事件的監(jiān)聽器。此事件將在用戶離開頁面之前觸發(fā)。通過在事件上調(diào)用 preventDefault 方法,我們可以觸發(fā)瀏覽器的確認對話框。僅當(dāng)表單具有未保存的更改(由 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>
);
});
當(dāng)在表單字段中輸入數(shù)據(jù)并在保存更改之前嘗試重新加載頁面或?qū)Ш降酵獠縐RL時,瀏覽器將顯示確認對話框。
使用React Router 5防止頁面導(dǎo)航
這個組件已經(jīng)足夠好用于我們的應(yīng)用程序,因為它的所有頁面都是表單的一部分。然而,在實際情況下,這并不總是如此。為了使我們的示例更具代表性,我們添加一個名為 Home 的新路由,它將重定向到表單之外。 Home 組件很簡單,只顯示一個主頁問候語。
// Home.js
export const Home = () => {
return <div>Welcome to the home page!</div>;
};
我們還需要對 App 組件進行一些調(diào)整,以適應(yīng)這條新路由。
// 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>
);
};
我們可以看到當(dāng)我們在表格中輸入信息并導(dǎo)航到主頁時,輸入的數(shù)據(jù)不會被保存,也不會出現(xiàn)任何確認對話框。這是因為導(dǎo)航由React Router處理,不會觸發(fā) beforeunload 事件,使瀏覽器API在這種情況下無效。幸運的是,React Router v5提供了 Prompt 組件,以在離開未保存更改的頁面之前警告用戶。該組件接受兩個props: when 和 message 。 when 屬性是一個布爾值,用于確定是否應(yīng)該顯示提示,而 message 屬性表示向用戶顯示的文本。
使用 Prompt 時,導(dǎo)航到主頁路由時行為正確,但是當(dāng)用戶輸入表單數(shù)據(jù)并進入下一步時,確認對話框也會出現(xiàn)。這是不希望的,因為我們在導(dǎo)航到下一步時保存表單數(shù)據(jù)。
為了解決這個問題,我們需要驗證下一個 URL 是否是表單步驟之一,然后再檢查未保存的更改??梢允褂?nbsp;message 屬性來實現(xiàn)這一點,它也可以是一個函數(shù)。該函數(shù)的第一個參數(shù)是下一個位置。如果函數(shù)返回 true ,則允許轉(zhuǎn)換到下一個 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} />;
};
通過這些更改,我們可以安全地在表單步驟之間導(dǎo)航,并在嘗試離開未保存更改的表單時收到警告。
使用 React Router 6 防止頁面導(dǎo)航
件已被移除,而 unstable_usePrompt 鉤子在 6.7.0 版本中被添加。正如其名稱所示,該鉤子的實現(xiàn)可能會發(fā)生變化,尚未記錄文檔。但是,它應(yīng)該適用于我們的使用情況。
我們可以使用這個鉤子來復(fù)制版本5中 Prompt 組件的行為,但首先,我們需要調(diào)整我們的 App 組件以使用新的數(shù)據(jù)路由器,因為它們是 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 函數(shù)來創(chuàng)建路由器。請注意, Stepper 沒有單獨的路徑,所有其他路由都是它的子路由。它作為布局組件,在每個頁面上呈現(xiàn)。每個頁面的內(nèi)容顯示在特殊的 Outlet 組件的位置。為了簡化 App 邏輯,我們還將主頁導(dǎo)航鏈接移動到 Stepper 中。
設(shè)置完成后,我們現(xiàn)在可以實現(xiàn)重定向阻止功能。我們首先通過在 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;
};
這個改變簡化了我們組件的邏輯?,F(xiàn)在,我們可以添加一個自定義的 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 鉤子接受布爾值或阻止函數(shù)作為其參數(shù),類似于 Prompt 組件中的 message 屬性。該函數(shù)的一個參數(shù)是下一個位置,我們使用它來確定用戶是否正在離開我們的表單。如果是這種情況,我們利用瀏覽器的 window.confirm 方法顯示一個對話框,詢問用戶確認重定向或取消它。最后,我們在 usePrompt 鉤子中抽象出阻止邏輯并管理阻止器的狀態(tài)。
我們可以通過導(dǎo)航到聯(lián)系步驟,填寫一些字段并單擊主頁導(dǎo)航項來測試 FormPrompt 是否按預(yù)期工作。我們會看到一個確認對話框,詢問我們是否要離開該頁面。
總結(jié)
總之,為未保存的表單更改實現(xiàn)確認對話框是增強用戶體驗的重要實踐。本文演示了如何創(chuàng)建一個 FormPrompt 組件,當(dāng)用戶嘗試離開具有未保存更改的頁面時,該組件會向用戶發(fā)出警告。我們探討了如何使用純JavaScript處理這種情況,使用 beforeunload 事件以及在React中使用React Router v5中的 Prompt 組件和React Router v6中的 useBeforeUnload 和 unstable_useBlocker 鉤子。通過將此功能合并到您的表單中,你可以幫助用戶避免失去未保存的工作而感到沮喪。
本文轉(zhuǎn)載自微信公眾號「大遷世界」,可以通過以下二維碼關(guān)注。轉(zhuǎn)載本文請聯(lián)系大遷世界公眾號。