React Router v7 已發布。 查看文件
待處理 UI
本頁內容

待處理和樂觀 UI

網路上優良的使用者體驗與平庸的使用者體驗之間的差異,在於開發者如何藉由在網路密集操作期間提供視覺提示,來實現網路感知的使用者介面回饋。主要有三種待處理的 UI 類型:忙碌指示器、樂觀 UI 和骨架回溯。本文檔提供了根據特定情境選擇和實作適當的回饋機制的指南。

待處理的 UI 回饋機制

忙碌指示器:忙碌指示器在伺服器處理動作時,會向使用者顯示視覺提示。當應用程式無法預測動作的結果,且必須等待伺服器的回應才能更新 UI 時,會使用此回饋機制。

樂觀 UI:樂觀 UI 藉由在收到伺服器的回應之前,立即使用預期的狀態更新 UI,來增強感知速度和回應能力。當應用程式可以根據內容和使用者輸入來預測動作的結果時,會使用此方法,以便立即回應動作。

骨架回溯:骨架回溯用於 UI 初始載入時,為使用者提供視覺佔位符,概述即將出現的內容結構。此回饋機制在儘快呈現有用的內容時特別有用。

回饋選擇的指導原則

使用樂觀 UI

  • 下一個狀態可預測性:應用程式可以根據使用者的動作,準確預測 UI 的下一個狀態。
  • 錯誤處理:已實作健全的錯誤處理機制,以處理過程中可能發生的潛在錯誤。
  • URL 穩定性:動作不會導致 URL 變更,確保使用者停留在同一頁面內。

使用忙碌指示器

  • 下一個狀態不確定性:無法可靠地預測動作的結果,因此需要等待伺服器的回應。
  • URL 變更:動作會導致 URL 變更,表示導覽至新頁面或區段。
  • 錯誤邊界:錯誤處理方法主要依賴於管理例外和非預期行為的錯誤邊界。
  • 副作用:動作會觸發涉及關鍵流程的副作用,例如傳送電子郵件、處理付款等。

使用骨架回溯

  • 初始載入:UI 正在載入過程中,為使用者提供即將出現的內容結構的視覺指示。
  • 關鍵資料:資料對於頁面的初始呈現不是關鍵的,因此可以在載入資料時顯示骨架回溯。
  • 應用程式般的感受:應用程式旨在模仿獨立應用程式的行為,允許立即轉換至回溯。

範例

忙碌指示器:您可以使用 useNavigation 指示使用者正在導覽至新頁面。

import { useNavigation } from "@remix-run/react";

function PendingNavigation() {
  const navigation = useNavigation();
  return navigation.state === "loading" ? (
    <div className="spinner" />
  ) : null;
}

忙碌指示器:您可以使用 <NavLink className> 回呼,在導覽連結本身上指示使用者正在導覽至該連結。

import { NavLink } from "@remix-run/react";

export function ProjectList({ projects }) {
  return (
    <nav>
      {projects.map((project) => (
        <NavLink
          key={project.id}
          to={project.id}
          className={({ isPending }) =>
            isPending ? "pending" : null
          }
        >
          {project.name}
        </NavLink>
      ))}
    </nav>
  );
}

或者藉由檢查參數在旁邊新增一個旋轉符號

import { useParams } from "@remix-run/react";

export function ProjectList({ projects }) {
  const params = useParams();
  return (
    <nav>
      {projects.map((project) => (
        <NavLink key={project.id} to={project.id}>
          {project.name}
          {params.projectId === project.id ? (
            <Spinner />
          ) : null}
        </NavLink>
      ))}
    </nav>
  );
}

雖然連結上的局部指示器很好,但它們並不完整。還有許多其他方式可以觸發導覽:表單提交、瀏覽器中的上一步和下一步按鈕點擊、動作重新導向和必要的 navigate(path) 呼叫,因此您通常需要一個全域指示器來捕捉所有內容。

記錄建立

忙碌指示器:由於在完成之前,ID 和其他欄位之類的東西都是未知的,因此通常最好等待建立記錄,而不是使用樂觀 UI。另請注意,此動作會從動作重新導向至新的記錄。

import type { ActionFunctionArgs } from "@remix-run/node"; // or cloudflare/deno
import { redirect } from "@remix-run/node"; // or cloudflare/deno
import { useNavigation } from "@remix-run/react";

export async function action({
  request,
}: ActionFunctionArgs) {
  const formData = await request.formData();
  const project = await createRecord({
    name: formData.get("name"),
    owner: formData.get("owner"),
  });
  return redirect(`/projects/${project.id}`);
}

export default function CreateProject() {
  const navigation = useNavigation();

  // important to check you're submitting to the action
  // for the pending UI, not just any action
  const isSubmitting =
    navigation.formAction === "/create-project";

  return (
    <Form method="post" action="/create-project">
      <fieldset disabled={isSubmitting}>
        <label>
          Name: <input type="text" name="projectName" />
        </label>
        <label>
          Owner: <UserSelect />
        </label>
        <button type="submit">Create</button>
      </fieldset>
      {isSubmitting ? <BusyIndicator /> : null}
    </Form>
  );
}

您可以使用 useFetcher 執行相同的操作,如果您沒有變更 URL(可能將記錄新增至清單),這會很有用。

import { useFetcher } from "@remix-run/react";

function CreateProject() {
  const fetcher = useFetcher();
  const isSubmitting = fetcher.state === "submitting";

  return (
    <fetcher.Form method="post" action="/create-project">
      {/* ... */}
    </fetcher.Form>
  );
}

記錄更新

樂觀 UI:當 UI 僅更新記錄上的欄位時,樂觀 UI 是很好的選擇。如果不是大多數,那麼網頁應用程式中大多數的使用者互動往往是更新,因此這是一種常見的模式。

import { useFetcher } from "@remix-run/react";

function ProjectListItem({ project }) {
  const fetcher = useFetcher();

  const starred = fetcher.formData
    ? // use optimistic value if submitting
      fetcher.formData.get("starred") === "1"
    : // fall back to the database state
      project.starred;

  return (
    <>
      <div>{project.name}</div>
      <fetcher.Form method="post">
        <button
          type="submit"
          name="starred"
          // use optimistic value to allow interruptions
          value={starred ? "0" : "1"}
        >
          {/* 👇 display optimistic value */}
          {starred ? "" : ""}
        </button>
      </fetcher.Form>
    </>
  );
}

延遲資料載入

骨架回溯:當資料延遲時,您可以使用 <Suspense> 新增回溯。這允許 UI 無需等待資料載入即可呈現,從而加快應用程式的感知和實際效能。

import type { LoaderFunctionArgs } from "@remix-run/node"; // or cloudflare/deno
import { defer } from "@remix-run/node"; // or cloudflare/deno
import { Await } from "@remix-run/react";
import { Suspense } from "react";

export async function loader({
  params,
}: LoaderFunctionArgs) {
  const reviewsPromise = getReviews(params.productId);
  const product = await getProduct(params.productId);
  return defer({
    product: product,
    reviews: reviewsPromise,
  });
}

export default function ProductRoute() {
  const { product, reviews } =
    useLoaderData<typeof loader>();
  return (
    <>
      <ProductPage product={product} />

      <Suspense fallback={<ReviewsSkeleton />}>
        <Await resolve={reviews}>
          {(reviews) => <Reviews reviews={reviews} />}
        </Await>
      </Suspense>
    </>
  );
}

建立骨架回溯時,請考量下列原則

  • 一致的大小:確保骨架回溯與實際內容的尺寸相符。這可防止突然的版面配置位移,提供更流暢且視覺上更具凝聚力的載入體驗。在網頁效能方面,這種權衡會盡量減少 累計版面配置位移 (CLS),以利於改善 首次內容繪製 (FCP)。您可以使用回溯中的精確尺寸來盡量減少權衡。
  • 關鍵資料:避免將回溯用於基本資訊,即頁面的主要內容。這對於 SEO 和中繼標籤特別重要。如果您延遲顯示關鍵資料,則無法提供精確的中繼標籤,且搜尋引擎將無法正確索引您的頁面。
  • 應用程式般的感受:對於沒有 SEO 疑慮的網頁應用程式 UI,更廣泛地使用骨架回溯可能會有好處。這會建立類似於獨立應用程式行為的介面。當使用者按一下連結時,它們會立即轉換至骨架回溯。
  • 連結預先提取:使用 <Link prefetch="intent"> 通常可以完全跳過回溯。當使用者將滑鼠游標懸停在連結上或將焦點放在連結上時,此方法會預先載入所需的資料,讓網路有短暫的時間在使用者按一下之前提取內容。這通常會立即導覽至下一個頁面。

結論

藉由在需要網路互動的動作期間顯示視覺提示,透過忙碌指示器、樂觀 UI 和骨架回溯建立網路感知 UI 可顯著改善使用者體驗。擅長此方面是建置使用者信任的應用程式的最佳方式。

文件和範例依據以下授權條款授權: MIT