Remix 中的資料寫入(有些人稱之為變更)是建立在兩個基本網路 API 之上:<form>
和 HTTP。然後,我們使用漸進增強來啟用樂觀 UI、載入指示器和驗證回饋 - 但程式設計模型仍然是建立在 HTML 表單之上。
當使用者提交表單時,Remix 會
許多時候,人們會使用 React 中的全域狀態管理函式庫(如 redux)、資料函式庫(如 apollo)和 fetch 包裝器(如 React Query),以協助管理將伺服器狀態載入元件並在使用者變更時保持 UI 與之同步。當您使用標準 HTML API 時,Remix 基於 HTML 的 API 取代了這些工具的大部分使用案例。Remix 知道如何載入資料,以及如何在資料變更後重新驗證資料。
有幾種方法可以呼叫 action 並讓路由重新驗證
本指南僅涵蓋 <Form>
。我們建議您閱讀本指南後,閱讀其他兩個的文檔,以了解如何使用它們。本指南的大部分內容適用於 useSubmit
,但 useFetcher
有點不同。
在我們公司 React Training 舉辦多年的研討會後,我們了解到許多較新的網路開發人員(雖然不是他們自己的錯)實際上並不知道 <form>
的運作方式!
由於 Remix <Form>
的運作方式與 <form>
完全相同(具有一些用於樂觀 UI 等的額外好處),我們將複習一下普通的 HTML 表單,以便您可以同時學習 HTML 和 Remix。
原生表單支援兩種 HTTP 動詞:GET
和 POST
。Remix 使用這些動詞來了解您的意圖。如果是 GET,Remix 將會找出頁面的哪些部分正在變更,並且只會擷取變更的版面配置的資料,並使用未變更的版面配置的快取資料。如果是 POST,Remix 將會重新載入所有資料,以確保它能擷取伺服器的更新。讓我們看看這兩種情況。
GET
就像正常的導覽,表單資料會傳遞在 URL 搜尋參數中。您可以將其用於正常的導覽,就像 <a>
一樣,只是使用者可以透過表單提供搜尋參數中的資料。除了搜尋頁面之外,它與 <form>
一起使用的情況非常罕見。
考慮一下這個表單
<form method="get" action="/search">
<label>Search <input name="term" type="text" /></label>
<button type="submit">Search</button>
</form>
當使用者填寫表單並點擊提交時,瀏覽器會自動將表單值序列化為 URL 搜尋參數字串,並使用附加的查詢字串導覽至表單的 action
。假設使用者輸入了「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
當您想要在您的網站上建立、刪除或更新資料時,表單 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」傳送。
伺服器的請求處理程式可以使用資料,因此您可以建立記錄。之後,您會傳回回應。在這種情況下,您可能會重新導向到新建立的專案。Remix action 看起來會像這樣
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
。當這一切發生時,瀏覽器會進入其正常的「載入」狀態:網址進度列填滿、favicon 變成微調器等等。這實際上是不錯的使用者體驗。
如果您是剛接觸 Web 開發的新手,您可能從未使用過這種方式的表單。許多人一直都這樣做
<form onSubmit={(event) => { event.preventDefault(); // good
luck! }} />
如果您是這種情況,當您看到僅使用瀏覽器(和 Remix)內建的功能就能讓變更變得如此容易時,您會感到非常高興!
我們將從頭到尾建立一個變更,其中包含
您使用 Remix <Form>
元件進行資料變更的方式,與使用 HTML 表單的方式相同。不同之處在於,現在您可以存取待處理的表單狀態,以建立更佳的使用者體驗:例如內容相關的載入指示器和「樂觀 UI」。
無論您是使用 <form>
還是 <Form>
,您都會撰寫相同的程式碼。您可以從 <form>
開始,然後將其升級到 <Form>
而無需變更任何內容。之後,加入特殊的載入指示器和樂觀 UI。但是,如果您覺得不想這樣做,或截止期限很緊,只需使用 <form>
並讓瀏覽器處理使用者回饋!Remix <Form>
是變更「漸進增強」的實現。
讓我們從先前的專案表單開始,但使其可用
假設您有路由 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>
);
}
現在新增路由 action。任何「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 表單和使用者流程。我們先保持簡單,然後再把它弄得花俏。
回到我們的 action 中,也許我們有一個 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
會返回 action 的資料。它只會在導航是表單提交時才會存在,因此你總是需要檢查它是否存在。
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 reloadDocument>
。這讓瀏覽器繼續處理掛起中的 UI 狀態(標籤頁的 favicon 中的旋轉器、網址列中的進度條等)。如果你只是使用 <Form>
而沒有實作掛起中的 UI,使用者在提交表單時不會知道任何事情正在發生。
<Form reloadDocument>
prop。
現在讓我們新增一些掛起中的 UI,讓使用者在提交時知道發生了某些事情。有一個名為 useNavigation
的 hook。當有掛起中的表單提交時,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.formMethod
, navigation.formAction
, navigation.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。一個簡單的表單,提交到伺服器端的 action。歡迎來到 1998 年。
一旦它開始運作,我們就使用 JavaScript 通過將 <form>
更改為 <Form>
來提交表單,但我們不必做任何其他事情!
現在有一個包含 React 的有狀態頁面,我們通過簡單地向 Remix 詢問導航狀態,為驗證錯誤新增了載入指示器和動畫。
從你的元件的角度來看,所有發生的事情都是 useNavigation
hook 在提交表單時觸發了狀態更新,然後在資料返回時觸發了另一個狀態更新。當然,在 Remix 內部發生了很多事情,但就你的元件而言,就這樣了。只有幾個狀態更新。這使得裝飾任何使用者流程變得非常容易。