参考
Part1に続き、Remixのチュートリアルを走ってみます。
連絡先の作成
下記のコードをroot.tsxへ追加することで、空のユーザーが追加されるようになります。
+ import { createEmptyContact, getContacts } from "./data";
+export const action = async () => {
+ const contact = await createEmptyContact();
+ return json({ contact });
+};
この辺よく分からなかったので、ChatGPTにも協力してもらう。
action関数は、Remixにおいてサーバー側で実行される特別な関数です。
この関数は、フォームの送信やAPIリクエストなどに対応してサーバー側で何らかの操作を実行し、結果を返すために使用されます。
Remixの動作モデル
このコードは、Remixの「従来のウェブ」に基づくプログラミングモデルを示しています。
フォーム送信やデータ変更に関連する操作が発生した場合、Remixはその操作を自動的に検知し、必要なデータの再検証やページの再レンダリングを行います。このモデルでは、JavaScriptが無効化されていても、HTMLとHTTPのみでアプリケーションが機能するように設計されています。
とのこと。
データの更新を実装する
👉 Create the edit component
touch app/routes/contacts.\$contactId_.edit.tsx
サンプルコード
import type { LoaderFunctionArgs } from "@remix-run/node";
import { json } from "@remix-run/node";
import { Form, useLoaderData } from "@remix-run/react";
import invariant from "tiny-invariant";
import { getContact } from "../data";
export const loader = async ({
params,
}: LoaderFunctionArgs) => {
invariant(params.contactId, "Missing contactId param");
const contact = await getContact(params.contactId);
if (!contact) {
throw new Response("Not Found", { status: 404 });
}
return json({ contact });
};
export default function EditContact() {
const { contact } = useLoaderData<typeof loader>();
return (
<Form id="contact-form" method="post">
<p>
<span>Name</span>
<input
defaultValue={contact.first}
aria-label="First name"
name="first"
type="text"
placeholder="First"
/>
<input
aria-label="Last name"
defaultValue={contact.last}
name="last"
placeholder="Last"
type="text"
/>
</p>
<label>
<span>Twitter</span>
<input
defaultValue={contact.twitter}
name="twitter"
placeholder="@jack"
type="text"
/>
</label>
<label>
<span>Avatar URL</span>
<input
aria-label="Avatar URL"
defaultValue={contact.avatar}
name="avatar"
placeholder="https://example.com/avatar.jpg"
type="text"
/>
</label>
<label>
<span>Notes</span>
<textarea
defaultValue={contact.notes}
name="notes"
rows={6}
/>
</label>
<p>
<button type="submit">Save</button>
<button type="button">Cancel</button>
</p>
</Form>
);
}
自動的なネスティング
Remixでは、デフォルトで同じ接頭辞を持つルートは自動的にネスティング(入れ子構造)されます。
例えば、app/routes/contacts.tsx と app/routes/contacts.$contactId.tsx というファイルがある場合、後者は前者の子ルートとして扱われます。
ネスティングの回避
場合によってはこの自動ネスティングを回避したいことがあります。そのためには、ファイル名にアンダースコア_を追加します。例えば、contacts.$contactId_.tsx という名前を使用すると、このルートは contacts.tsx の子ルートとしてネスティングされなくなります。
今回の例では、contactsのネスティングを外すために_をつけています。
_をつけていないと、ページの内容が情報ページから編集ページに変わりません。
Editを押すことで、下記のような編集画面に移動できるようになります。
連絡先の作成の実装
ルートで関数をエクスポートして、新しい連絡先を作成します。ユーザーが「新規」ボタンをクリックすると、OSTします。
// existing imports
+import { createEmptyContact, getContacts } from "./data";
+export const action = async () => {
+ const contact = await createEmptyContact();
+ return json({ contact });
+};
// existing code
データの更新の実装
データの更新処理の実装に必要なのは、action関数を追加することだけです。
import type {
+ ActionFunctionArgs,
LoaderFunctionArgs,
} from "@remix-run/node";
+ import { json, redirect } from "@remix-run/node";
// existing imports
+ import { getContact, updateContact } from "../data";
+ export const action = async ({
+ params,
+ request,
+}: ActionFunctionArgs) => {
+ invariant(params.contactId, "Missing contactId param");
+ const formData = await request.formData();
+ const updates = Object.fromEntries(formData);
+ await updateContact(params.contactId, updates);
+ return redirect(`/contacts/${params.contactId}`);
+};
// existing code
新しいレコードを追加時に編集ページにリダイレクトする
新しいレコードを追加時に、リダイレクトするように修正します。
// existing imports
+import { json, redirect } from "@remix-run/node";
// existing imports
export const action = async () => {
const contact = await createEmptyContact();
+ return redirect(`/contacts/${contact.id}/edit`);
};
Page LoadingのUIの実装
この場合、useNavigationフックを使用します。
useNavigationは現在のナビゲーション状態を返します。
idle
loading
submitting
のいずれかになります。
今回の例では、loadingの際に、クラスを追加して、画面遷移時にフェードするようにしています。
// existing imports
import {
+ useNavigation,
} from "@remix-run/react";
// existing imports & exports
export default function App() {
const { contacts } = useLoaderData<typeof loader>();
+ const navigation = useNavigation();
return (
<html lang="en">
{/* existing elements */}
<body>
{/* existing elements */}
<div
+ className={
+ navigation.state === "loading" ? "loading" : ""
+ }
id="detail"
>
<Outlet />
</div>
{/* existing elements */}
</body>
</html>
);
}
レコードの削除の実装
問い合わせルートのコードを確認すると、削除ボタンが次のようになっていることがわかります。
<Form
+ action="destroy"
アクションポイントは "destroy" と指定されています。
リンクのように、フォームのアクションには相対的な値を使用できます。
フォームは contacts.$contactId.tsx でレンダリングされるので、相対的なアクションで "destroy" を指定すると、クリック時に contacts.$contactId.destroy にフォームが送信されます。
この時点で、削除ボタンを動作させるために必要なすべてを知っているはずです。
次に進む前に試してみてはどうでしょうか?
必要なものは以下の通りです
- 新しいルート
- そのルートでのアクション
- app/data.ts からの deleteContact
- 何かにリダイレクトする
とのこと。1つずつ実装していく。
1. 新しいdestroyルートの作成
touch app/routes/contacts.\$contactId.destroy.tsx
2. DeleteActionの実装
import type { ActionFunctionArgs } from "@remix-run/node";
import { redirect } from "@remix-run/node";
import invariant from "tiny-invariant";
import { deleteContact } from "../data";
export const action = async ({
params,
}: ActionFunctionArgs) => {
invariant(params.contactId, "Missing contactId param");
await deleteContact(params.contactId);
return redirect("/");
};
Formのデフォルトの動作の防止
通常、ブラウザはフォームを送信すると新しいドキュメントのPOSTリクエストをサーバーに送信します。しかし、Remixでは、<Form>
タグはこのデフォルトの動作を防止します。代わりに、クライアントサイドのルーティングとfetchを使ってPOSTリクエストを作成し、ブラウザの動作をエミュレートします。
Form action="destroy"のマッチング
ユーザーが送信ボタンをクリックすると、<Form action="destroy">
は "contacts.$contactId.destroy" という新しいルートと一致し、そのルートにリクエストを送信します。
リダイレクト後のアクションとローダーの呼び出し
アクションがリダイレクトした後、Remixはページ上のデータのための全てのローダーを呼び出し、最新の値を取得します(これを "revalidation" と言います)。useLoaderData
は新しい値を返し、これによってコンポーネントが更新されます!
要するに、「フォームを追加し、アクションを設定すれば、Remixが残りを処理します」ということです。
これにより、Remixは開発者がサーバーとクライアントの両方での動作をより簡単に管理できるように設計されています。ユーザーのインタラクション(この場合は削除ボタンのクリック)とデータの更新がシームレスに連携し、効率的なウェブアプリケーションの開発が可能になります。
Index Routeの作成
現状 app/root.tsx
はでレンダリングするものがありません。
Index Routeは、そのスペースを埋めるためのデフォルトの子ルートと考えることができます。
Index Routeファイルを作成する。
touch app/routes/_index.tsx
👉インデックスコンポーネントの要素を入力します
サンプルコード
export default function Index() {
return (
<p id="index-page">
This is a demo for Remix.
<br />
Check out{" "}
<a href="https://remix.run">the docs at remix.run</a>.
</p>
);
}
キャンセルボタンの実装
編集ページには、動かないキャンセルボタンがあります。
ブラウザの戻るボタンと同じことを実装してみます。
キャンセルボタンのクリックハンドラーをuseNavigateで追加します。
// existing imports
import {
Form,
useLoaderData,
+ useNavigate,
} from "@remix-run/react";
// existing imports & exports
export default function EditContact() {
const { contact } = useLoaderData<typeof loader>();
+ const navigate = useNavigate();
return (
<Form id="contact-form" method="post">
{/* existing elements */}
<p>
<button type="submit">Save</button>
+ <button onClick={() => navigate(-1)} type="button">
Cancel
</button>
</p>
</Form>
);
}
onClickeにnavigateが走るようにしただけです。
<button>
にtype="button"を設定することで、
ボタンに event.preventDefault() を記述する必要をなくしています。