表单 vs. 获取器
On this page

表单 vs. 获取器

¥Form vs. fetcher

使用 Remix 进行开发提供了一套丰富的工具,这些工具有时会在功能上重叠,给新手带来歧义。在 Remix 中高效开发的关键在于理解每个工具的细微差别和合适的用例。本文档旨在阐明何时以及为何使用特定 API。

¥Developing in Remix offers a rich set of tools that can sometimes overlap in functionality, creating a sense of ambiguity for newcomers. The key to effective development in Remix is understanding the nuances and appropriate use cases for each tool. This document seeks to provide clarity on when and why to use specific APIs.

重点 API

¥APIs in Focus

了解这些 API 的区别和交集对于高效且有效的 Remix 开发至关重要。

¥Understanding the distinctions and intersections of these APIs is vital for efficient and effective Remix development.

URL 注意事项

¥URL Considerations

选择这些工具的主要标准是你是否希望 URL 更改:

¥The primary criterion when choosing among these tools is whether you want the URL to change or not:

  • 需要更改 URL:在页面之间导航或转换时,或者在执行某些操作(例如创建或删除记录)之后。这可以确保用户的浏览器历史记录准确反映他们在你应用中的旅程。

    ¥URL Change Desired: When navigating or transitioning between pages, or after certain actions like creating or deleting records. This ensures that the user's browser history accurately reflects their journey through your application.

    • 预期行为:在许多情况下,当用户点击后退按钮时,他们应该被带到上一页。其他情况下,历史记录条目可能会被替换,但 URL 更改仍然很重要。

      ¥Expected Behavior: In many cases, when users hit the back button, they should be taken to the previous page. Other times the history entry may be replaced but the URL change is important nonetheless.

  • 无需更改 URL:对于不会显著改变当前视图上下文或主要内容的操作。这可能包括更新单个字段或不需要重新加载新 URL 或页面的细微数据操作。这也适用于使用抓取器加载数据,例如弹出窗口、组合框等。

    ¥No URL Change Desired: For actions that don't significantly change the context or primary content of the current view. This might include updating individual fields or minor data manipulations that don't warrant a new URL or page reload. This also applies to loading data with fetchers for things like popovers, combo boxes, etc.

具体用例

¥Specific Use Cases

URL 何时应该更改

¥When the URL Should Change

这些操作通常反映了用户上下文或状态的重大变化:

¥These actions typically reflect significant changes to the user's context or state:

  • 创建新记录:创建新记录后,通常会将用户重定向到该新记录的专用页面,以便用户查看或进一步修改该记录。

    ¥Creating a New Record: After creating a new record, it's common to redirect users to a page dedicated to that new record, where they can view or further modify it.

  • 删除记录:如果用户当前位于特定记录的页面上并决定删除该记录,那么下一步就是将其重定向到常规页面,例如所有记录的列表页面。

    ¥Deleting a Record: If a user is on a page dedicated to a specific record and decides to delete it, the logical next step is to redirect them to a general page, such as a list of all records.

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

¥For these cases, developers should consider using a combination of <Form>, useActionData, and useNavigation. Each of these tools can be coordinated to handle form submission, invoke specific actions, retrieve action-related data, and manage navigation respectively.

URL 何时不应该更改

¥When the URL Shouldn't Change

这些操作通常更微妙,不需要用户进行上下文切换:

¥These actions are generally more subtle and don't require a context switch for the user:

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

    ¥Updating a Single Field: Maybe a user wants to change the name of an item in a list or update a specific property of a record. This action is minor and doesn't necessitate a new page or URL.

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

    ¥Deleting a Record from a List: In a list view, if a user deletes an item, they likely expect to remain on the list view, with that item no longer in the list.

  • 在列表视图中创建记录:向列表中添加新项目时,用户通常希望停留在该上下文中,无需进行完整的页面转换即可看到新项目添加到列表中。

    ¥Creating a Record in a List View: When adding a new item to a list, it often makes sense for the user to remain in that context, seeing their new item added to the list without a full page transition.

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

    ¥Loading Data for a Popover or Combobox: When loading data for a popover or combobox, the user's context remains unchanged. The data is loaded in the background and displayed in a small, self-contained UI element.

对于此类操作,useFetcher 是首选 API。它功能多样,结合了其他四个 API 的功能,非常适合需要保持不变 URL 的任务。

¥For such actions, useFetcher is the go-to API. It's versatile, combining functionalities of the other four APIs, and is perfectly suited for tasks where the URL should remain unchanged.

API 比较

¥API Comparison

如你所见,这两套 API 有很多相似之处:

¥As you can see, the two sets of APIs have a lot of similarities:

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

示例

¥Examples

创建新记录

¥Creating a New Record

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 来简化直观的记录创建过程。

¥The example leverages <Form>, useActionData, and useNavigation to facilitate an intuitive record creation process.

使用 <Form> 可以确保导航直接且合乎逻辑。创建记录后,用户会自然地被引导至新秘诀的唯一 URL,从而强化其操作的结果。

¥Using <Form> ensures direct and logical navigation. After creating a record, the user is naturally guided to the new recipe's unique URL, reinforcing the outcome of their action.

useActionData 连接服务器和客户端,对提交问题提供即时反馈。这种快速响应使用户能够毫无阻碍地纠正任何错误。

¥useActionData bridges server and client, providing immediate feedback on submission issues. This quick response enables users to rectify any errors without hindrance.

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

¥Lastly, useNavigation dynamically reflects the form's submission state. This subtle UI change, like toggling the button's label, assures users that their actions are being processed.

这些 API 结合起来,提供了结构化导航和反馈的平衡融合。

¥Combined, these APIs offer a balanced blend of structured navigation and feedback.

更新记录

¥Updating a Record

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

¥Now consider we're looking at a list of recipes that have delete buttons on each item. When a user clicks the delete button, we want to delete the recipe from the database and remove it from the list without navigating away from the list.

首先,考虑设置基本路由以获取页面上的秘诀列表:

¥First, consider the basic route setup to get a list of recipes on the page:

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>
  );
}

现在我们来看看删除配方的操作以及渲染列表中每个配方的组件。

¥Now we'll look at the action that deletes a recipe and the component that renders each recipe in the list.

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 效果很好。我们不需要离开页面或刷新整个页面,而是需要就地更新。当用户删除秘诀时,会调用 action,并且获取器会管理相应的状态转换。

¥Using useFetcher in this scenario works perfectly. Instead of navigating away or refreshing the entire page, we want in-place updates. When a user deletes a recipe, the action is called and the fetcher manages the corresponding state transitions.

这里的主要优势在于上下文的维护。删除完成后,用户仍保留在列表中。fetcher 的状态管理功能可用于提供实时反馈:它会在 "Deleting...""Delete" 之间切换,清晰地指示正在进行的进程。

¥The key advantage here is the maintenance of context. The user stays on the list when the deletion completes. The fetcher's state management capabilities are leveraged to give real-time feedback: it toggles between "Deleting..." and "Delete", providing a clear indication of the ongoing process.

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

¥Furthermore, with each fetcher having the autonomy to manage its own state, operations on individual list items become independent, ensuring that actions on one item don't affect the others (though revalidation of the page data is a shared concern covered in Network Concurrency Management).

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

¥In essence, useFetcher offers a seamless mechanism for actions that don't necessitate a change in the URL or navigation, enhancing the user experience by providing real-time feedback and context preservation.

将文章标记为已读

¥Mark Article as Read

想象一下,当当前用户在页面上停留一段时间并滚动到页面底部后,你想标记文章已被当前用户阅读。你可以创建一个类似这样的钩子:

¥Imagine you want to mark that an article has been read by the current user, after they've been on the page for a while and scrolled to the bottom. You could make a hook that looks something like this:

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

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

用户头像详情弹出窗口

¥User Avatar Details Popup

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

¥Anytime you show the user avatar, you could put a hover effect that fetches data from a loader and displays it in a popup.

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>
  );
}

结论

¥Conclusion

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

¥Remix offers a range of tools to cater to varied developmental needs. While some functionalities might seem to overlap, each tool has been crafted with specific scenarios in mind. By understanding the intricacies and ideal applications of <Form>, useActionData, useFetcher, and useNavigation, developers can create more intuitive, responsive, and user-friendly web applications.

Remix v2.17 中文网 - 粤ICP备13048890号