表单 vs. fetcher
本页

表单 vs. 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