React Router v7 已发布。 查看文档
表单 vs. 获取器
本页内容

Form 与 fetcher 对比

在 Remix 中进行开发时,会提供一套功能有时会重叠的丰富工具,这会让新手感到困惑。在 Remix 中有效开发的关键是理解每个工具的细微之处和适当的用例。本文档旨在阐明何时以及为何使用特定的 API。

重点 API

理解这些 API 的区别和交叉点对于高效和有效的 Remix 开发至关重要。

URL 考虑事项

在这些工具之间进行选择的主要标准是是否要更改 URL

  • 需要更改 URL:在页面之间导航或切换时,或者在创建或删除记录等某些操作之后。这确保了用户的浏览器历史记录准确反映了他们在应用程序中的旅程。

    • 预期行为:在许多情况下,当用户点击后退按钮时,他们应该被带到上一页。其他时候,历史记录条目可能会被替换,但无论如何 URL 更改都很重要。
  • 不需要更改 URL:对于不显著更改当前视图的上下文或主要内容的操作。这可能包括更新单个字段或不需要新的 URL 或页面重新加载的轻微数据操作。这也适用于使用 fetcher 加载弹出窗口、组合框等数据。

具体用例

何时应该更改 URL

这些操作通常反映用户上下文或状态的重大更改

  • 创建新记录:创建新记录后,通常会将用户重定向到专门用于该新记录的页面,他们可以在其中查看或进一步修改它。

  • 删除记录:如果用户在专门用于特定记录的页面上并决定删除它,则逻辑上的下一步是将他们重定向到通用页面,例如所有记录的列表。

对于这些情况,开发人员应考虑结合使用 <Form>useActionDatauseNavigation。可以协调每个工具来处理表单提交、调用特定操作、检索与操作相关的数据以及分别管理导航。

何时不应该更改 URL

这些操作通常更细微,不需要用户切换上下文

  • 更新单个字段:也许用户想要更改列表中项目的名称或更新记录的特定属性。此操作是次要的,不需要新页面或 URL。

  • 从列表中删除记录:在列表视图中,如果用户删除一个项目,他们可能希望保留在列表视图中,并且该项目不再在列表中。

  • 在列表视图中创建记录:向列表中添加新项目时,用户通常会保留在该上下文中,看到他们的新项目已添加到列表中,而无需进行完整的页面转换。

  • 加载弹出窗口或组合框的数据:当加载弹出窗口或组合框的数据时,用户的上下文保持不变。数据在后台加载并显示在小的、独立的 UI 元素中。

对于此类操作,useFetcher 是首选 API。它用途广泛,结合了其他四个 API 的功能,并且非常适合 URL 应保持不变的任务。

API 对比

如您所见,这两组 API 有很多相似之处

导航/URL API Fetcher API
<Form> <fetcher.Form>
useActionData() fetcher.data
navigation.state fetcher.state
navigation.formAction fetcher.formAction
navigation.formData fetcher.formData

示例

创建新记录

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

export async function action({
  request,
}: ActionFunctionArgs) {
  const formData = await request.formData();
  const errors = await validateRecipeFormData(formData);
  if (errors) {
    return json({ errors });
  }
  const recipe = await db.recipes.create(formData);
  return redirect(`/recipes/${recipe.id}`);
}

export function NewRecipe() {
  const { errors } = useActionData<typeof action>();
  const navigation = useNavigation();
  const isSubmitting =
    navigation.formAction === "/recipes/new";

  return (
    <Form method="post">
      <label>
        Title: <input name="title" />
        {errors?.title ? <span>{errors.title}</span> : null}
      </label>
      <label>
        Ingredients: <textarea name="ingredients" />
        {errors?.ingredients ? (
          <span>{errors.ingredients}</span>
        ) : null}
      </label>
      <label>
        Directions: <textarea name="directions" />
        {errors?.directions ? (
          <span>{errors.directions}</span>
        ) : null}
      </label>
      <button type="submit">
        {isSubmitting ? "Saving..." : "Create Recipe"}
      </button>
    </Form>
  );
}

该示例利用 <Form>useActionDatauseNavigation 来促进直观的记录创建过程。

使用 <Form> 确保直接且逻辑的导航。创建记录后,用户会自然而然地被引导到新菜谱的唯一 URL,从而加强了他们操作的结果。

useActionData 连接服务器和客户端,提供有关提交问题的即时反馈。这种快速响应使用户能够纠正任何错误,而不会受到阻碍。

最后,useNavigation 动态反映表单的提交状态。这种细微的 UI 更改(例如切换按钮的标签)向用户保证他们的操作正在被处理。

总而言之,这些 API 提供了结构化导航和反馈的平衡组合。

更新记录

现在考虑一下,我们正在查看一个配方列表,每个项目上都有删除按钮。当用户单击删除按钮时,我们希望从数据库中删除该配方,并将其从列表中删除,而无需离开列表。

首先考虑基本路由设置,以在页面上获取配方列表

import type { LoaderFunctionArgs } from "@remix-run/node"; // or cloudflare/deno
import { json } from "@remix-run/node"; // or cloudflare/deno
import { useLoaderData } from "@remix-run/react";

export async function loader({
  request,
}: LoaderFunctionArgs) {
  return json({
    recipes: await db.recipes.findAll({ limit: 30 }),
  });
}

export default function Recipes() {
  const { recipes } = useLoaderData<typeof loader>();
  return (
    <ul>
      {recipes.map((recipe) => (
        <RecipeListItem key={recipe.id} recipe={recipe} />
      ))}
    </ul>
  );
}

现在,我们将查看删除配方的操作以及在列表中呈现每个配方的组件。

export async function action({
  request,
}: ActionFunctionArgs) {
  const formData = await request.formData();
  const id = formData.get("id");
  await db.recipes.delete(id);
  return json({ ok: true });
}

const RecipeListItem: FunctionComponent<{
  recipe: Recipe;
}> = ({ recipe }) => {
  const fetcher = useFetcher();
  const isDeleting = fetcher.state !== "idle";

  return (
    <li>
      <h2>{recipe.title}</h2>
      <fetcher.Form method="post">
        <button disabled={isDeleting} type="submit">
          {isDeleting ? "Deleting..." : "Delete"}
        </button>
      </fetcher.Form>
    </li>
  );
};

在这种情况下使用 useFetcher 非常有效。我们希望进行就地更新,而不是离开或刷新整个页面。当用户删除配方时,将调用该操作,并且 fetcher 管理相应的状态转换。

这里的关键优势在于上下文的维护。删除完成后,用户将留在列表上。利用 fetcher 的状态管理功能来提供实时反馈:它在 "正在删除...""删除" 之间切换,清楚地指示正在进行的过程。

此外,由于每个 fetcher 都有自主管理其自身状态的权利,因此对单个列表项的操作变得独立,从而确保对一个项目执行的操作不会影响其他项目(尽管页面数据的重新验证是 网络并发管理中涵盖的共同关注点)。

本质上,useFetcher 为不需要更改 URL 或导航的操作提供了一种无缝机制,通过提供实时反馈和上下文保留来增强用户体验。

标记文章为已读

假设您希望在当前用户在该页面停留一段时间并滚动到底部后,标记该文章已被当前用户阅读。您可以创建一个如下所示的钩子

function useMarkAsRead({ articleId, userId }) {
  const marker = useFetcher();

  useSpentSomeTimeHereAndScrolledToTheBottom(() => {
    marker.submit(
      { userId },
      {
        action: `/article/${articleId}/mark-as-read`,
        method: "post",
      }
    );
  });
}

用户头像详情弹出框

每当您显示用户头像时,您可以添加一个悬停效果,该效果从加载器中获取数据并在弹出窗口中显示。

export async function loader({
  params,
}: LoaderFunctionArgs) {
  return json(
    await fakeDb.user.find({ where: { id: params.id } })
  );
}

function UserAvatar({ partialUser }) {
  const userDetails = useFetcher<typeof loader>();
  const [showDetails, setShowDetails] = useState(false);

  useEffect(() => {
    if (
      showDetails &&
      userDetails.state === "idle" &&
      !userDetails.data
    ) {
      userDetails.load(`/users/${user.id}/details`);
    }
  }, [showDetails, userDetails]);

  return (
    <div
      onMouseEnter={() => setShowDetails(true)}
      onMouseLeave={() => setShowDetails(false)}
    >
      <img src={partialUser.profileImageUrl} />
      {showDetails ? (
        userDetails.state === "idle" && userDetails.data ? (
          <UserPopup user={userDetails.data} />
        ) : (
          <UserPopupLoading />
        )
      ) : null}
    </div>
  );
}

结论

Remix 提供一系列工具以满足各种开发需求。虽然某些功能可能看起来有所重叠,但每个工具都是针对特定场景而设计的。通过理解 <Form>useActionDatauseFetcheruseNavigation 的复杂性和理想应用,开发者可以创建更直观、响应更快、更用户友好的 Web 应用程序。

文档和示例根据以下许可 MIT