離開頁面前,如何防止表單數據丟失?

本文介紹瞭如何實現一個 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 class>
      <AppProvider>
        <Router>
          <div class>
            <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 class>
      <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 鉤子。通過將此功能合併到您的表單中,你可以幫助用戶避免失去未保存的工作而感到沮喪。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/p3jPpMgTTxvV5lVpOhc5Xg