在 Remix 中进行开发时,会提供一套功能有时会重叠的丰富工具,这会让新手感到困惑。在 Remix 中有效开发的关键是理解每个工具的细微之处和适当的用例。本文档旨在阐明何时以及为何使用特定的 API。
理解这些 API 的区别和交叉点对于高效和有效的 Remix 开发至关重要。
在这些工具之间进行选择的主要标准是是否要更改 URL
需要更改 URL:在页面之间导航或切换时,或者在创建或删除记录等某些操作之后。这确保了用户的浏览器历史记录准确反映了他们在应用程序中的旅程。
不需要更改 URL:对于不显著更改当前视图的上下文或主要内容的操作。这可能包括更新单个字段或不需要新的 URL 或页面重新加载的轻微数据操作。这也适用于使用 fetcher 加载弹出窗口、组合框等数据。
这些操作通常反映用户上下文或状态的重大更改
创建新记录:创建新记录后,通常会将用户重定向到专门用于该新记录的页面,他们可以在其中查看或进一步修改它。
删除记录:如果用户在专门用于特定记录的页面上并决定删除它,则逻辑上的下一步是将他们重定向到通用页面,例如所有记录的列表。
对于这些情况,开发人员应考虑结合使用 <Form>
、useActionData
和 useNavigation
。可以协调每个工具来处理表单提交、调用特定操作、检索与操作相关的数据以及分别管理导航。
这些操作通常更细微,不需要用户切换上下文
更新单个字段:也许用户想要更改列表中项目的名称或更新记录的特定属性。此操作是次要的,不需要新页面或 URL。
从列表中删除记录:在列表视图中,如果用户删除一个项目,他们可能希望保留在列表视图中,并且该项目不再在列表中。
在列表视图中创建记录:向列表中添加新项目时,用户通常会保留在该上下文中,看到他们的新项目已添加到列表中,而无需进行完整的页面转换。
加载弹出窗口或组合框的数据:当加载弹出窗口或组合框的数据时,用户的上下文保持不变。数据在后台加载并显示在小的、独立的 UI 元素中。
对于此类操作,useFetcher
是首选 API。它用途广泛,结合了其他四个 API 的功能,并且非常适合 URL 应保持不变的任务。
如您所见,这两组 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>
、useActionData
和 useNavigation
来促进直观的记录创建过程。
使用 <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>
、useActionData
、useFetcher
和 useNavigation
的复杂性和理想应用,开发者可以创建更直观、响应更快、更用户友好的 Web 应用程序。