コンテンツにスキップ

Data Writes

このコンテンツはまだ日本語訳がありません。

数据写入

Remix 中的数据写入(有些人称之为突变)建立在两个基本 Web API 之上:<form> 和 HTTP。然后我们使用渐进式增强来实现乐观的 UI、加载指示器和验证反馈 —— 但编程模型仍然建立在 HTML 表单之上。

当用户提交表单时,Remix 将:

  1. 调用表单的操作
  2. 重新加载页面上所有路由的所有数据

人们经常会使用 React 中的全局状态管理库(如 redux)、数据库(如 apollo)和获取包装器(如 React Query),以帮助管理将服务器状态放入组件中并在用户更改时保持 UI 与之同步。Remix 基于 HTML 的 API 取代了这些工具的大多数用例。当您使用标准 HTML API 时,Remix 知道如何加载数据以及如何在数据更改后重新验证数据。

有几种方法可以调用操作并重新验证路由:

本指南仅涵盖 <Form>。我们建议您阅读本指南之后的另外两个文档,以了解如何使用它们。本指南的大部分内容适用于 useSubmit,但 useFetcher 略有不同。

纯 HTML 表单

在我们公司 React Training 举办多年讲习班之后,我们了解到很多较新的 Web 开发人员(尽管这不是他们的错)实际上并不知道 <form> 是如何工作的!

由于 Remix <Form> 的工作方式与 <form> 完全相同(但有一些额外的好处,例如乐观的 UI 等),我们将复习一下普通的 HTML 表单,这样您就可以同时学习 HTML 和 Remix。

HTML 表单 HTTP 动词

原生表单支持两个 HTTP 动词:GETPOST。Remix 使用这些动词来理解您的意图。如果是 GET,Remix 将确定页面的哪些部分正在发生变化,并仅获取变化布局的数据,并使用缓存数据来处理不变的布局。如果是 POST,Remix 将重新加载所有数据以确保它从服务器捕获更新。让我们来看看这两个动词。

HTML 表单获取

GET 只是一种常规导航,其中表单数据在 URL 搜索参数中传递。您可以将其用于常规导航,就像 <a> 一样,只是用户需要通过表单在搜索参数中提供数据。除了搜索页面之外,它与 <form> 一起使用的情况相当少见。

考虑这种形式:

<form method="get" action="/search">
<label>Search <input name="term" type="text" /></label>
<button type="submit">Search</button>
</form>

当用户填写并点击提交时,浏览器会自动将表单值序列化为 URL 搜索参数字符串,并附加查询字符串导航到表单的操作。假设用户输入 remix。浏览器将导航到 /search?term=remix。如果我们将输入更改为 <input name="q"/>,则表单将导航到 /search?q=remix

这与我们创建此链接的行为相同:

<a href="/search?term=remix">Search for "remix"</a>

唯一的不同在于用户必须提供信息。

如果您有更多字段,浏览器将添加它们:

<form method="get" action="/search">
<fieldset>
<legend>Brand</legend>
<label>
<input name="brand" value="nike" type="checkbox" />
Nike
</label>
<label>
<input name="brand" value="reebok" type="checkbox" />
Reebok
</label>
<label>
<input name="color" value="white" type="checkbox" />
White
</label>
<label>
<input name="color" value="black" type="checkbox" />
Black
</label>
<button type="submit">Search</button>
</fieldset>
</form>

根据用户点击的复选框,浏览器将导航到如下 URL:

/search?brand=nike&color=black
/search?brand=nike&brand=reebok&color=white

HTML 表单 POST

当您想在网站上创建、删除或更新数据时,表单发布是最佳选择。我们指的不仅仅是用户个人资料编辑页面之类的大型表单。甚至按钮也可以通过表单处理。

让我们考虑一个新项目形式。

<form method="post" action="/projects">
<label><input name="name" type="text" /></label>
<label><textarea name="description"></textarea></label>
<button type="submit">Create</button>
</form>

当用户提交此表单时,浏览器会将字段序列化为请求主体(而不是 URL 搜索参数)并将其 POST 到服务器。这仍然是正常导航,就像用户单击链接一样。区别在于两个方面:用户为服务器提供数据,浏览器以 POST 而不是 GET 的形式发送请求。

数据已提供给服务器的请求处理程序,因此您可以创建记录。之后,您返回响应。在这种情况下,您可能会重定向到新创建的项目。重新混合操作看起来如下所示:

app/routes/projects.tsx
export async function action({
request,
}: ActionFunctionArgs) {
const body = await request.formData();
const project = await createProject(body);
return redirect(`/projects/${project.id}`);
}

浏览器从 /projects/new 开始,然后使用请求中的表单数据发布到 /projects,然后服务器将浏览器重定向到 /projects/123。当这一切发生时,浏览器进入其正常的加载状态:地址进度条填满,图标变成旋转器等。这实际上是一种不错的用户体验。

如果您是 Web 开发新手,您可能从未以这种方式使用过表单。许多人一直这样做:

<form onSubmit={(event) => { event.preventDefault(); // good
luck! }} />

如果这是你,当你看到当你使用浏览器(和 Remix)内置的功能时变异是多么容易时,你会很高兴!

Remix Mutation,从头到尾

我们将从头到尾构建一个突变:

  1. JavaScript 可选
  2. 验证
  3. 错误处理
  4. 逐步增强的加载指示器
  5. 逐步增强的错误显示

使用 Remix <Form> 组件进行数据变更的方式与使用 HTML 表单的方式相同。不同之处在于,现在您可以访问待定表单状态,以构建更好的用户体验:例如上下文加载指示器和乐观 UI

不管你使用 <form> 还是 <Form>,你编写的代码都是相同的。你可以从 <form> 开始,然后将其升级为 <Form>,而无需进行任何更改。之后,添加特殊的加载指示器和乐观的 UI。但是,如果你不打算这样做,或者截止日期很紧,只需使用 <form>,让浏览器处理用户反馈!Remix <Form> 是突变渐进增强的实现。

构建表单

让我们从之前的项目表单开始,但要使其可用:

假设您有包含以下形式的路由 app/routes/projects.new.tsx

app/routes/projects.new.tsx
export default function NewProject() {
return (
<form method="post" action="/projects/new">
<p>
<label>
Name: <input name="name" type="text" />
</label>
</p>
<p>
<label>
Description:
<br />
<textarea name="description" />
</label>
</p>
<p>
<button type="submit">Create</button>
</p>
</form>
);
}

现在添加路由操作。任何 post 形式的表单提交都将调用您的数据 action。任何 get 提交(<Form method="get">)都将由您的 loader 处理。

import type { ActionFunctionArgs } from "@remix-run/node"; // or cloudflare/deno
import { redirect } from "@remix-run/node"; // or cloudflare/deno
// Note the "action" export name, this will handle our form POST
export const action = async ({
request,
}: ActionFunctionArgs) => {
const formData = await request.formData();
const project = await createProject(formData);
return redirect(`/projects/${project.id}`);
};
export default function NewProject() {
// ... same as before
}

就是这样!假设 createProject 按照我们的要求执行,这就是您要做的。请注意,无论您过去构建了哪种 SPA,您始终都需要服务器端操作和表单来从用户那里获取数据。Remix 的不同之处在于 这就是您所需要的(网络过去也是这样的。)

当然,我们开始让事情变得复杂,以尝试创造比默认浏览器行为更好的用户体验。继续努力,我们会实现目标,但我们不必更改任何已编写的代码即可获得核心功能。

表单验证

验证表单在客户端和服务器端都很常见。但不幸的是,只验证客户端也很常见,这会导致数据出现各种问题,我们现在没有时间讨论这些问题。重点是,如果您只在一个地方验证,请在服务器上进行验证。您会发现,使用 Remix 后,这是您唯一关心的地方(发送到浏览器的信息越少越好!)。

我们知道,我们知道,您想要在漂亮的验证错误和其他内容中添加动画。我们会做到的。但现在我们只是在构建一个基本的 HTML 表单和用户流程。我们先让它保持简单,然后再让它变得花哨。

回到我们的行动中,也许我们有一个返回像这样的验证错误的 API。

const [errors, project] = await createProject(formData);

如果存在验证错误,我们希望返回表单并显示它们。

import { json, redirect } from "@remix-run/node"; // or cloudflare/deno
export const action = async ({
request,
}: ActionFunctionArgs) => {
const formData = await request.formData();
const [errors, project] = await createProject(formData);
if (errors) {
const values = Object.fromEntries(formData);
return json({ errors, values });
}
return redirect(`/projects/${project.id}`);
};

就像 useLoaderData 返回来自 loader 的值一样,useActionData 将返回来自操作的数据。只有当导航是表单提交时,它才会出现,因此您始终必须检查是否已获得它。

import type { ActionFunctionArgs } from "@remix-run/node"; // or cloudflare/deno
import { json, redirect } from "@remix-run/node"; // or cloudflare/deno
import { useActionData } from "@remix-run/react";
export const action = async ({
request,
}: ActionFunctionArgs) => {
// ...
};
export default function NewProject() {
const actionData = useActionData<typeof action>();
return (
<form method="post" action="/projects/new">
<p>
<label>
Name:{" "}
<input
name="name"
type="text"
defaultValue={actionData?.values.name}
/>
</label>
</p>
{actionData?.errors.name ? (
<p style={{ color: "red" }}>
{actionData.errors.name}
</p>
) : null}
<p>
<label>
Description:
<br />
<textarea
name="description"
defaultValue={actionData?.values.description}
/>
</label>
</p>
{actionData?.errors.description ? (
<p style={{ color: "red" }}>
{actionData.errors.description}
</p>
) : null}
<p>
<button type="submit">Create</button>
</p>
</form>
);
}

注意我们如何将 defaultValue 添加到所有输入中。请记住,这是常规 HTML<form>,因此这只是浏览器 / 服务器发生的正常事情。我们从服务器获取值,因此用户不必重新输入他们之前输入的内容。

您可以按原样发送此代码。浏览器将为您处理待处理的 UI 和中断。享受您的周末,并在周一过得愉快。

升级到 <Form> 并添加待处理的 UI

让我们使用渐进式增强功能让此 UX 更加美观。通过将其从 <form> 更改为 <Form>,Remix 将使用 fetch 模拟浏览器行为。它还将允许您访问待处理的表单数据,以便您可以构建待处理的 UI。

import { json, redirect } from "@remix-run/node"; // or cloudflare/deno
import { useActionData, Form } from "@remix-run/react";
// ...
export default function NewProject() {
const actionData = useActionData<typeof action>();
return (
// note the capital "F" <Form> now
<Form method="post">{/* ... */}</Form>
);
}

稍等!如果你所做的只是将表单更改为 Form,那么用户体验会变得更糟!

如果您没有时间或动力完成此处的其余工作,请使用 <Form reloadDocument>。这允许浏览器继续处理待处理的 UI 状态(选项卡图标中的微调器、地址栏中的进度条等)。如果您只是使用 <Form> 而不实现待处理的 UI,则用户在提交表单时将不知道发生了什么。

我们建议始终使用大写 F 表单,如果您想让浏览器处理待处理的 UI,请使用 <Form reloadDocument> 属性。

现在让我们添加一些待处理的 UI,以便用户在提交时了解发生了什么。有一个名为 useNavigation 的钩子。当有待处理的表单提交时,Remix 会将表单的序列化版本作为 FormData 对象提供给您。您最感兴趣的是 formData.get() 方法。

import { json, redirect } from "@remix-run/node"; // or cloudflare/deno
import {
useActionData,
Form,
useNavigation,
} from "@remix-run/react";
// ...
export default function NewProject() {
// when the form is being processed on the server, this returns different
// navigation states to help us build pending and optimistic UI.
const navigation = useNavigation();
const actionData = useActionData<typeof action>();
return (
<Form method="post">
<fieldset
disabled={navigation.state === "submitting"}
>
<p>
<label>
Name:{" "}
<input
name="name"
type="text"
defaultValue={
actionData
? actionData.values.name
: undefined
}
/>
</label>
</p>
{actionData && actionData.errors.name ? (
<p style={{ color: "red" }}>
{actionData.errors.name}
</p>
) : null}
<p>
<label>
Description:
<br />
<textarea
name="description"
defaultValue={
actionData
? actionData.values.description
: undefined
}
/>
</label>
</p>
{actionData && actionData.errors.description ? (
<p style={{ color: "red" }}>
{actionData.errors.description}
</p>
) : null}
<p>
<button type="submit">
{navigation.state === "submitting"
? "Creating..."
: "Create"}
</button>
</p>
</fieldset>
</Form>
);
}

非常巧妙!现在,当用户点击创建时,输入将被禁用,提交按钮的文本会发生变化。整个操作现在也应该更快,因为只发生一次网络请求,而不是整个页面重新加载(这可能涉及更多网络请求、从浏览器缓存读取资产、解析 JavaScript、解析 CSS 等)。

我们没有在这个页面上对 navigation 做太多的操作,但是它包含了有关提交的所有信息(navigation.formMethodnavigation.formActionnavigation.formEncType),以及服务器上 navigation.formData 上正在处理的所有值。

验证错误中的动画

现在我们使用 JavaScript 提交此页面,我们的验证错误可以以动画形式呈现,因为该页面是有状态的。首先,我们将制作一个动画高度和不透明度的精美组件:

function ValidationMessage({ error, isSubmitting }) {
const [show, setShow] = useState(!!error);
useEffect(() => {
const id = setTimeout(() => {
const hasError = !!error;
setShow(hasError && !isSubmitting);
});
return () => clearTimeout(id);
}, [error, isSubmitting]);
return (
<div
style={{
opacity: show ? 1 : 0,
height: show ? "1em" : 0,
color: "red",
transition: "all 300ms ease-in-out",
}}
>
{error}
</div>
);
}

现在,我们可以将旧的错误消息包装在这个新的组件中,甚至将有错误的字段的边框变成红色:

export default function NewProject() {
const navigation = useNavigation();
const actionData = useActionData<typeof action>();
return (
<Form method="post">
<fieldset
disabled={navigation.state === "submitting"}
>
<p>
<label>
Name:{" "}
<input
name="name"
type="text"
defaultValue={
actionData
? actionData.values.name
: undefined
}
style={{
borderColor: actionData?.errors.name
? "red"
: "",
}}
/>
</label>
</p>
{actionData?.errors.name ? (
<ValidationMessage
isSubmitting={navigation.state === "submitting"}
error={actionData?.errors?.name}
/>
) : null}
<p>
<label>
Description:
<br />
<textarea
name="description"
defaultValue={actionData?.values.description}
style={{
borderColor: actionData?.errors.description
? "red"
: "",
}}
/>
</label>
</p>
<ValidationMessage
isSubmitting={navigation.state === "submitting"}
error={actionData?.errors.description}
/>
<p>
<button type="submit">
{navigation.state === "submitting"
? "Creating..."
: "Create"}
</button>
</p>
</fieldset>
</Form>
);
}

太棒了!无需更改与服务器的通信方式,即可获得精美的 UI。它还能抵御阻止 JS 加载的网络条件。

审查

  • 首先,我们构建项目表单时没有考虑 JavaScript。一个简单的表单,发布到服务器端操作。欢迎来到 1998 年。

  • 一旦成功,我们就使用 JavaScript 通过将 <form> 更改为 <Form> 来提交表单,但我们不需要做任何其他事情!

  • 现在有了带有 React 的状态页面,我们通过简单地向 Remix 询问导航状态来添加加载指示器和验证错误动画。

从组件的角度来看,所发生的一切就是 useNavigation 钩子在提交表单时导致状态更新,然后在数据返回时导致另一次状态更新。当然,Remix 内部还发生了更多事情,但就您的组件而言,仅此而已。只有几个状态更新。这使得修饰任何用户流程变得非常容易。

另请参阅