データ書き込み
Remix におけるデータ書き込み(ミューテーションと呼ぶ人もいます)は、2 つの基本的な Web API である <form> と HTTP の上に構築されています。そして、プログレッシブエンハンスメントを使用して、楽観的な UI、ローディングインジケーター、およびバリデーションフィードバックを有効にしますが、プログラミングモデルは依然として HTML フォームに基づいています。
ユーザーがフォームを送信すると、Remix は次の処理を行います。
- フォームのアクションを呼び出す
- ページ上のすべてのルートのすべてのデータをリロードする
多くの場合、人々は、サーバーの状態をコンポーネントに取り込み、ユーザーが変更したときに UI を同期させるために、redux のような React のグローバル状態管理ライブラリ、apollo のようなデータライブラリ、React Query のような fetch ラッパーを利用します。Remix の HTML ベースの API は、これらのツールのほとんどのユースケースを置き換えます。Remix は、標準の HTML API を使用すると、データのロード方法と、変更後に再検証する方法を認識します。
アクションを呼び出してルートを再検証する方法はいくつかあります。
このガイドでは、<Form> のみを取り上げます。これら 2 つの使用方法を理解するために、このガイドの後にドキュメントを読むことをお勧めします。このガイドのほとんどは useSubmit に適用されますが、useFetcher は少し異なります。
プレーンな HTML フォーム
当社 React Training で長年ワークショップを開催してきた結果、多くの新しい Web 開発者(彼らのせいではありませんが)が、実際には <form> の仕組みを知らないことがわかりました。
Remix の <Form> は <form> と同じように機能するため(楽観的な UI などのためのいくつかの追加機能があります)、プレーンな HTML フォームについて復習し、HTML と Remix の両方を同時に学習できるようにします。
HTML フォームの HTTP 動詞
ネイティブフォームは、GET と POST の 2 つの HTTP 動詞をサポートしています。Remix は、これらの動詞を使用して、あなたの意図を理解します。GET の場合、Remix はページのどの部分が変更されているかを判断し、変更されているレイアウトのデータのみをフェッチし、変更されていないレイアウトにはキャッシュされたデータを使用します。POST の場合、Remix はすべてのデータをリロードして、サーバーからの更新を確実にキャプチャします。両方を見てみましょう。
HTML フォーム GET
GET は、フォームデータが URL 検索パラメーターで渡される通常のナビゲーションです。通常のナビゲーションに使用します。<a> と同じですが、ユーザーはフォームを介して検索パラメーターでデータを提供できます。検索ページを除いて、<form> での使用は非常にまれです。
次のフォームを考えてみましょう。
<form method="get" action="/search">
<label>検索 <input name="term" type="text" /></label>
<button type="submit">検索</button>
</form>ユーザーがフォームに入力して送信をクリックすると、ブラウザーはフォームの値を自動的に URL 検索パラメーター文字列にシリアル化し、クエリ文字列を追加してフォームの action に移動します。ユーザーが「remix」と入力したとしましょう。ブラウザーは /search?term=remix に移動します。入力を <input name="q"/> に変更すると、フォームは /search?q=remix に移動します。
これは、次のリンクを作成した場合と同じ動作です。
<a href="/search?term=remix">「remix」を検索</a>ただし、ユーザーが情報を提供したという独自の違いがあります。
フィールドが多い場合、ブラウザーはそれらを追加します。
<form method="get" action="/search">
<fieldset>
<legend>ブランド</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">検索</button>
</fieldset>
</form>ユーザーがクリックしたチェックボックスに応じて、ブラウザーは次のような URL に移動します。
/search?brand=nike&color=black
/search?brand=nike&brand=reebok&color=white
HTML フォーム POST
Web サイトでデータを作成、削除、または更新する場合は、フォームの POST を使用します。そして、ユーザープロファイルの編集ページのような大きなフォームだけを意味するわけではありません。「いいね」ボタンでさえ、フォームで処理できます。
「新しいプロジェクト」フォームを考えてみましょう。
<form method="post" action="/projects">
<label><input name="name" type="text" /></label>
<label><textarea name="description"></textarea></label>
<button type="submit">作成</button>
</form>ユーザーがこのフォームを送信すると、ブラウザーはフィールドをリクエスト「ボディ」(URL 検索パラメーターではなく)にシリアル化し、サーバーに「POST」します。これは、ユーザーがリンクをクリックした場合と同じように、通常のナビゲーションです。違いは 2 つあります。ユーザーがサーバーにデータを提供し、ブラウザーがリクエストを「GET」ではなく「POST」として送信したことです。
データはサーバーのリクエストハンドラーで利用できるため、レコードを作成できます。その後、レスポンスを返します。この場合、おそらく新しく作成されたプロジェクトにリダイレクトします。Remix アクションは次のようになります。
export async function action({
request,
}: ActionFunctionArgs) {
const body = await request.formData();
const project = await createProject(body);
return redirect(`/projects/${project.id}`);
}ブラウザーは /projects/new から開始し、リクエストにフォームデータを含めて /projects に POST し、サーバーはブラウザーを /projects/123 にリダイレクトしました。これがすべて発生している間、ブラウザーは通常の「読み込み中」状態になります。アドレスのプログレスバーが埋まり、ファビコンがスピナーに変わるなどです。実際には、まともなユーザーエクスペリエンスです。
Web 開発を始めたばかりの場合は、この方法でフォームを使用したことがないかもしれません。多くの人が常に次のように行ってきました。
<form onSubmit={(event) => { event.preventDefault(); // good
luck! }} />もしあなたがそうなら、ブラウザー(および Remix)に組み込まれているものを使用するだけで、ミューテーションがいかに簡単になるかを知って喜ぶでしょう。
Remix ミューテーション、最初から最後まで
次の手順で、ミューテーションを最初から最後まで構築します。
- JavaScript はオプション
- バリデーション
- エラー処理
- プログレッシブエンハンスメントされたローディングインジケーター
- プログレッシブエンハンスメントされたエラー表示
データミューテーションには、HTML フォームと同じように Remix の <Form> コンポーネントを使用します。違いは、コンテキストローディングインジケーターや「楽観的な 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>
名前: <input name="name" type="text" />
</label>
</p>
<p>
<label>
説明:
<br />
<textarea name="description" />
</label>
</p>
<p>
<button type="submit">作成</button>
</p>
</form>
);
}次に、ルートアクションを追加します。「post」であるフォーム送信は、データ「アクション」を呼び出します。「get」送信(<Form method="get">)は、「ローダー」によって処理されます。
import type { ActionFunctionArgs } from "@remix-run/node"; // または cloudflare/deno
import { redirect } from "@remix-run/node"; // または cloudflare/deno
// 「action」エクスポート名に注意してください。これがフォームの 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() {
// ... 前と同じ
}これで完了です!createProject が期待どおりに機能すると仮定すると、これだけですべてです。過去に構築した可能性のある SPA の種類に関係なく、ユーザーからデータを取得するには、常にサーバー側のアクションとフォームが必要であることに注意してください。Remix の違いは、これだけが必要であるということです(そして、それが Web の昔の姿でもありました)。
もちろん、デフォルトのブラウザーの動作よりも優れたユーザーエクスペリエンスを作成しようとして、複雑なことを始めました。続行しましょう。そこに到達しますが、コア機能を実行するためにすでに記述したコードを変更する必要はありません。
フォームのバリデーション
フォームをクライアント側とサーバー側の両方で検証するのが一般的です。また、(残念ながら)クライアント側でのみ検証することも一般的であり、これにより、今すぐ説明する時間がないさまざまなデータの問題が発生します。重要なのは、1 か所でのみ検証する場合は、サーバーで検証することです。Remix を使用すると、それが唯一の場所であることがわかります(ブラウザーに送信するものが少ないほど良いです!)。
わかっています、わかっています、素敵なバリデーションエラーなどをアニメーション化したいのですよね。それについては後で説明します。しかし、今は基本的な HTML フォームとユーザーフローを構築しているだけです。最初はシンプルにして、後で凝ったものにします。
アクションに戻ると、次のようなバリデーションエラーを返す API があるかもしれません。
const [errors, project] = await createProject(formData);バリデーションエラーがある場合は、フォームに戻って表示する必要があります。
import { json, redirect } from "@remix-run/node"; // または 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"; // または cloudflare/deno
import { json, redirect } from "@remix-run/node"; // または 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>
名前:{" "}
<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>
説明:
<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">作成</button>
</p>
</form>
);
}すべての入力に defaultValue を追加する方法に注目してください。これは通常の HTML <form> であるため、通常のブラウザー/サーバーの処理が行われているだけです。サーバーから値が返されるため、ユーザーは入力した内容を再入力する必要はありません。
このコードをそのまま出荷できます。ブラウザーは、保留中の UI と中断を処理します。週末を楽しんで、月曜日に凝ったものにしましょう。
<Form> に移行して保留中の UI を追加する
プログレッシブエンハンスメントを使用して、この UX を少し凝ったものにしましょう。<form> から <Form> に変更すると、Remix は fetch でブラウザーの動作をエミュレートします。また、保留中のフォームデータにアクセスできるため、保留中の UI を構築できます。
import { json, redirect } from "@remix-run/node"; // または cloudflare/deno
import { useActionData, Form } from "@remix-run/react";
// ...
export default function NewProject() {
const actionData = useActionData<typeof action>();
return (
// 大文字の「F」の <Form> に注目してください
<Form method="post">{/* ... */}</Form>
);
}ここで残りの作業を行う時間や意欲がない場合は、<Form reloadDocument> を使用してください。これにより、ブラウザーは保留中の UI 状態(タブのファビコンのスピナー、アドレスバーのプログレスバーなど)を処理し続けることができます。保留中の UI を実装せずに <Form> を使用するだけの場合、ユーザーはフォームを送信したときに何も起こっていないことに気づきません。
<Form reloadDocument> プロパティを使用することをお勧めします。
次に、ユーザーが送信時に何かが起こったことを知るための保留中の UI を追加しましょう。useNavigation というフックがあります。保留中のフォーム送信がある場合、Remix はフォームのシリアル化されたバージョンを FormData オブジェクトとして提供します。最も関心があるのは、formData.get() メソッドです。
import { json, redirect } from "@remix-run/node"; // または cloudflare/deno
import {
useActionData,
Form,
useNavigation,
} from "@remix-run/react";
// ...
export default function NewProject() {
// フォームがサーバーで処理されている場合、これは保留中および楽観的な UI を構築するのに役立つさまざまなナビゲーション状態を返します。
const navigation = useNavigation();
const actionData = useActionData<typeof action>();
return (
<Form method="post">
<fieldset
disabled={navigation.state === "submitting"}
>
<p>
<label>
名前:{" "}
<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>
説明:
<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"
? "作成中..."
: "作成"}
</button>
</p>
</fieldset>
</Form>
);
}かなり洗練されています!ユーザーが「作成」をクリックすると、入力が無効になり、送信ボタンのテキストが変更されます。ページ全体のリロード(ネットワークリクエストの増加、ブラウザーキャッシュからのアセットの読み取り、JavaScript の解析、CSS の解析などが発生する可能性があります)ではなく、1 つのネットワークリクエストのみが発生するため、操作全体も高速になるはずです。
このページでは 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>
名前:{" "}
<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>
説明:
<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"
? "作成中..."
: "作成"}
</button>
</p>
</fieldset>
</Form>
);
}やった!サーバーとの通信方法を変更することなく、凝った UI ができました。また、JS の読み込みを妨げるネットワーク状態にも対応できます。
レビュー
-
まず、JavaScript を念頭に置かずにプロジェクトフォームを構築しました。サーバー側のアクションに投稿するシンプルなフォームです。1998 年へようこそ。
-
それが機能したら、
<form>を<Form>に変更して JavaScript を使用してフォームを送信しましたが、他に何もする必要はありませんでした。 -
React を使用したステートフルなページになったので、Remix にナビゲーションの状態を要求するだけで、ローディングインジケーターとバリデーションエラーのアニメーションを追加しました。
コンポーネントの観点から見ると、フォームが送信されたときに useNavigation フックが状態の更新を引き起こし、データが返ってきたときに別の状態の更新が発生しただけです。もちろん、Remix の内部ではさらに多くのことが起こりましたが、コンポーネントに関する限り、それだけです。いくつかの状態の更新だけです。これにより、ユーザーフローを非常に簡単に装飾できます。