1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

RuruCun個人開発Advent Calendar 2023

Day 5

Remixのチュートリアルで入門してみる Part 2

Posted at

参考

Part1に続き、Remixのチュートリアルを走ってみます。

連絡先の作成

下記のコードをroot.tsxへ追加することで、空のユーザーが追加されるようになります。

app/root.tsx
+ import { createEmptyContact, getContacts } from "./data";

+export const action = async () => {
+  const contact = await createEmptyContact();
+  return json({ contact });
+};

スクリーンショット 2023-12-06 1.38.55.png

この辺よく分からなかったので、ChatGPTにも協力してもらう。

action関数は、Remixにおいてサーバー側で実行される特別な関数です。
この関数は、フォームの送信やAPIリクエストなどに対応してサーバー側で何らかの操作を実行し、結果を返すために使用されます。

Remixの動作モデル

このコードは、Remixの「従来のウェブ」に基づくプログラミングモデルを示しています。
フォーム送信やデータ変更に関連する操作が発生した場合、Remixはその操作を自動的に検知し、必要なデータの再検証やページの再レンダリングを行います。このモデルでは、JavaScriptが無効化されていても、HTMLとHTTPのみでアプリケーションが機能するように設計されています。

とのこと。

データの更新を実装する

👉 Create the edit component

touch app/routes/contacts.\$contactId_.edit.tsx
サンプルコード
tsx 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のネスティングを外すために_をつけています。

_をつけていないと、ページの内容が情報ページから編集ページに変わりません。
image.png

Editを押すことで、下記のような編集画面に移動できるようになります。

image.png

連絡先の作成の実装

ルートで関数をエクスポートして、新しい連絡先を作成します。ユーザーが「新規」ボタンをクリックすると、OSTします。

app/root.tsx
// existing imports
+import { createEmptyContact, getContacts } from "./data";

+export const action = async () => {
+  const contact = await createEmptyContact();
+  return json({ contact });
+};
// existing code

データの更新の実装

データの更新処理の実装に必要なのは、action関数を追加することだけです。

app/routes/contacts.$contactId_.edit.tsx
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

新しいレコードを追加時に編集ページにリダイレクトする

新しいレコードを追加時に、リダイレクトするように修正します。

app/route.tsx
// 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の際に、クラスを追加して、画面遷移時にフェードするようにしています。

app/root.tsx
// 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>
  );
}

レコードの削除の実装

問い合わせルートのコードを確認すると、削除ボタンが次のようになっていることがわかります。

contact.$contactId.tsx
<Form
+ action="destroy"
  

アクションポイントは "destroy" と指定されています。
リンクのように、フォームのアクションには相対的な値を使用できます。
フォームは contacts.$contactId.tsx でレンダリングされるので、相対的なアクションで "destroy" を指定すると、クリック時に contacts.$contactId.destroy にフォームが送信されます。

この時点で、削除ボタンを動作させるために必要なすべてを知っているはずです。
次に進む前に試してみてはどうでしょうか?

必要なものは以下の通りです

  1. 新しいルート
  2. そのルートでのアクション
  3. app/data.ts からの deleteContact
  4. 何かにリダイレクトする

とのこと。1つずつ実装していく。

1. 新しいdestroyルートの作成

touch app/routes/contacts.\$contactId.destroy.tsx

2. DeleteActionの実装

app/routes/contacts.contactId.destroy.tsx
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の作成

image.png

現状 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>
  );
}

image.png

キャンセルボタンの実装

編集ページには、動かないキャンセルボタンがあります。
ブラウザの戻るボタンと同じことを実装してみます。

キャンセルボタンのクリックハンドラーをuseNavigateで追加します。

app/routes/contacts.$contactId_.edit.tsx
// 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() を記述する必要をなくしています。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?