離開頁面前,如何防止表單資料丟失?

前端小智發表於2023-05-03
本文首發於微信公眾號:大遷世界, 我的微信:qq449245884,我會第一時間和你分享前端行業趨勢,學習途徑等等。
更多開源作品請看 GitHub https://github.com/qq449245884/xiaozhi ,包含一線大廠面試完整考點、資料以及我的系列文章。

快來免費體驗ChatGpt plus版本的,我們出的錢
體驗地址:https://chat.waixingyun.cn
可以加入網站底部技術群,一起找bug.

本文介紹瞭如何實現一個FormPrompt元件,在使用者嘗試離開具有未儲存更改的頁面時發出警告。文章討論瞭如何使用純JavaScript和beforeunload事件處理這類情況,以及使用React Router v5中的Prompt元件和useBeforeUnload以及unstable等React特定解決方案。向使用者新增一個確認對話方塊,詢問他們在具有未儲存表單更改的情況下是否確認重定向是一種良好的使用者體驗實踐。透過顯示此提示,使用者將意識到他們有未儲存的更改,並允許在繼續重定向之前儲存或丟棄它們的工作。

下面是正文~

在今天的數字化環境中,為涉及表單提交的 Web 應用程式提供最佳使用者體驗非常重要。使用者常見的一個煩惱來源是由於意外離開頁面而丟失未儲存的更改。

本文將演示如何實現一個 FormPrompt 元件,當使用者嘗試離開具有未儲存更改的頁面時,會發出警報,從而有效地提高整體使用者體驗。我們將討論如何使用純 JavaScript 處理此類情況,使用 React Router v5 中的 Prompt 元件以及在 React Router v6 中使用 useBeforeUnloadunstable_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 元件,以在離開未儲存更改的頁面之前警告使用者。該元件接受兩個propswhenmessagewhen 屬性是一個布林值,用於確定是否應該顯示提示,而 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 鉤子。透過將此功能合併到您的表單中,你可以幫助使用者避免失去未儲存的工作而感到沮喪。

程式碼部署後可能存在的BUG沒法實時知道,事後為了解決這些BUG,花了大量的時間進行log 除錯,這邊順便給大家推薦一個好用的BUG監控工具 Fundebug

原文:https://claritydev.net/blog/display-warning-for-unsaved-form-...

交流

有夢想,有乾貨,微信搜尋 【大遷世界】 關注這個在凌晨還在刷碗的刷碗智。

本文 GitHub https://github.com/qq449245884/xiaozhi 已收錄,有一線大廠面試完整考點、資料以及我的系列文章。

相關文章