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

## 参考

下記のチュートリアルを参考にRemixを体感してみます。
https://remix.run/docs/en/main/start/tutorial

ZennのScrapでコメント形式にまとめたものを記事にしたものです。
https://zenn.dev/rurucun/scraps/d5d6e9c14ccb8d

### テンプレートから雛形を作ります

下記のcreate-remix@latestからひな形を作成します。

```sh
 npx create-remix@latest --template remix-run/remix/templates/remix-tutorial


 remix   v2.3.1 💿 Let's build a better website...

   dir   Where should we create your new project?
         ./my-remix-app

      ◼  Template: Using remix-run/remix/templates/remix-tutorial...
      ✔  Template copied

   git   Initialize a new git repository?
         Yes

  deps   Install dependencies with npm?
         Yes

      ✔  Dependencies installed

      ✔  Git initialized

  done   That's it!

         Enter your project directory using cd ./my-remix-app
         Check out README.md for development and deploy instructions.

         Join the community at https://rmx.as/discord

```

下記で、開発環境を立ち上げます。

```sh
cd remix-tutorial
npm run dev
```


## The Root Route

`app/root.tsx`RootRouteと呼び、UIで最初にレンダリングサれるコンポーネント。
通常はページのグローバルレイアウトを含みます。

## linksへスタイルを追加する

下記のコードで、CSSをJSに直接インポートできる。

### appstylesを追加する。
```diff_tsx:root.tsx
+import type { LinksFunction } from "@remix-run/node";
// existing imports

+import appStylesHref from "./app.css";

+export const links: LinksFunction = () => [
+  { rel: "stylesheet", href: appStylesHref },
+];
```

## 連絡先のルーティングを作成する。

サイドバーの`/contacts/1` `/contacts/2`を動くように修正します。

Remixファイルルーティングでは、`.`はURLに`/`を作り、$はセグメントを動的にする。

下記は、`/contacts/123` `/contacts/abc`などに対応するファイルを作成することになります。

```sh
mkdir app/routes
touch app/routes/contacts.\$contactId.tsx
```


### ComponentUIを作成します。

:::details 追加するファイル(サイトと同じため折りたたみ)
```jsx:app/routes/contacts.$contactId.tsx
import { Form } from "@remix-run/react";
import type { FunctionComponent } from "react";

import type { ContactRecord } from "../data";

export default function Contact() {
  const contact = {
    first: "Your",
    last: "Name",
    avatar: "https://placekitten.com/g/200/200",
    twitter: "your_handle",
    notes: "Some notes",
    favorite: true,
  };

  return (
    <div id="contact">
      <div>
        <img
          alt={`${contact.first} ${contact.last} avatar`}
          key={contact.avatar}
          src={contact.avatar}
        />
      </div>

      <div>
        <h1>
          {contact.first || contact.last ? (
            <>
              {contact.first} {contact.last}
            </>
          ) : (
            <i>No Name</i>
          )}{" "}
          <Favorite contact={contact} />
        </h1>

        {contact.twitter ? (
          <p>
            <a
              href={`https://twitter.com/${contact.twitter}`}
            >
              {contact.twitter}
            </a>
          </p>
        ) : null}

        {contact.notes ? <p>{contact.notes}</p> : null}

        <div>
          <Form action="edit">
            <button type="submit">Edit</button>
          </Form>

          <Form
            action="destroy"
            method="post"
            onSubmit={(event) => {
              const response = confirm(
                "Please confirm you want to delete this record."
              );
              if (!response) {
                event.preventDefault();
              }
            }}
          >
            <button type="submit">Delete</button>
          </Form>
        </div>
      </div>
    </div>
  );
}

const Favorite: FunctionComponent<{
  contact: Pick<ContactRecord, "favorite">;
}> = ({ contact }) => {
  const favorite = contact.favorite;

  return (
    <Form method="post">
      <button
        aria-label={
          favorite
            ? "Remove from favorites"
            : "Add to favorites"
        }
        name="favorite"
        value={favorite ? "false" : "true"}
      >
        {favorite ? "★" : "☆"}
      </button>
    </Form>
  );
};
```
:::

上記を追加しても、まだコンテンツが表示されません。

## ネストされたルーティングとアウトレット

RemixはReact Routerの上に構築されているので、ネストされたルーティングをサポートしています。

子ルートが親レイアウトの内部でレンダリングされるようにするには、親でアウトレットをレンダリングする必要があります。

app/root.tsxを開き、内部でアウトレットをレンダリングします。

### <Outlet />をRootへ追加する。


```diff_tsx:app/root.tsx
// existing imports
import {
  Form,
  Links,
  LiveReload,
  Meta,
+  Outlet,
  Scripts,
  ScrollRestoration,
} from "@remix-run/react";

// existing imports & code

export default function App() {
  return (
    <html lang="en">
      {/* other elements */}
      <body>
        <div id="sidebar">{/* other elements */}</div>
+        <div id="detail">
+         <Outlet />
+        </div>
        {/* other elements */}
      </body>
    </html>
  );
}

```

 rootへ<Outlet />を追加したことで、`routes/contacts.$contactId.tsx`の内容が表示されます。

![](https://storage.googleapis.com/zenn-user-upload/6ce95a0dcf3f-20231205.png)


## Client Side Routing 

現状の実装では、サイドバーのリンクをクリックすると、URLのフルドキュメントリクエストを行っています。

ClientSideRoutingを実装することによって、アプリはサーバーにリクエストすることなくURLを更新できます。

### <a href> を <Link to>に変換します。

```jsx:app/root.tsx
// existing imports
import {
+  Link,
} from "@remix-run/react";

export default function App() {
  return (
      {/* other elements */}
          <nav>
            <ul>
              <li>
+                <Link to={`/contacts/1`}>Your Name</Link>
              </li>
              <li>
+               <Link to={`/contacts/2`}>Your Friend</Link>
              </li>
            </ul>
          </nav>
        {/* other elements */}
  );
}

```

## データの読み込み

URLセグメント、レイアウト、データは、多くの場合、3重結合するパターンが多い。

URL Segment | Component | Data
--- | --- | ---
/ | <Root> | list of contacts
contacts/:contactId | <Contact> | individual contact

これらの3つを自然に結合させるために、Remixはルートコンポーネントにデータを簡単に取り込むためのデータ規約を持っています。

データをロードするために、`loader`と`useLoaderData`という2つのAPIを使います。まず、ルートルートにloader関数を作成してエクスポートし、データをレンダリングします。


```diff_jsx:app/root.tsx

+ import { json } from "@remix-run/node";
import {
+  useLoaderData,
} from "@remix-run/react";

// 用意されているデータです。
import { getContacts } from "./data";

+ export const loader = async () => {
+  const contacts = await getContacts();
+ return json({ contacts });
+};

export default function App() {
+  const { contacts } = useLoaderData();

  return (
    <html lang="en">
      {/* other elements */}
      <body>
        <div id="sidebar">
          {/* other elements */}
          <nav>
+            {contacts.length ? (
+             <ul>
+                {contacts.map((contact) => (
+                 <li key={contact.id}>
+                   <Link to={`contacts/${contact.id}`}>
+                      {contact.first || contact.last ? (
+                        <>
+                          {contact.first} {contact.last}
+                        </>
+                      ) : (
+                        <i>No Name</i>
+                      )}{" "}
+                      {contact.favorite ? (
+                        <span>★</span>
+                      ) : null}
+                    </Link>
+                  </li>
+                ))}
+              </ul>
+            ) : (
+              <p>
+                <i>No contacts</i>
+             </p>
            )}
          </nav>
        </div>
        {/* other elements */}
      </body>
    </html>
  );
}
```

 サイドバーにデータが表示されるようになります。

![](https://storage.googleapis.com/zenn-user-upload/91708a7fcaf4-20231205.png)


## 型推論

次のようにして、簡単なアノテーションを追加して、データに関する型推論を取得できますtypeof loader。

```diff tsx
export default function App() {
+  const { contacts } = useLoaderData<typeof loader>();

  // existing code
}
```

## LoaderのURLパラメーター

loaderの処理でparamsからcontactIdを取得し、データを参照するように修正します。

```tsx app/routes/contacts.$contactId.tsx
+ import { json } from "@remix-run/node";
+ import { Form, useLoaderData } from "@remix-run/react";
// existing imports

+ import { getContact } from "../data";

+ export const loader = async ({ params }) => {
+  const contact = await getContact(params.contactId);
+  return json({ contact });
+};

export default function Contact() {
+  const { contact } = useLoaderData<typeof loader>();

  // existing code
}

```

paramsのcontactIdからローカルのjsonのデータを表示するようになったので、サイドバーで各ページへ移動すると、それぞれの名前と画像が表示されるようになりました。

![](https://storage.googleapis.com/zenn-user-upload/cdfa9ca9c806-20231205.png)


## パラメータの検証と404

ユーザーが見つからない場合の処理を追加する。

```diff_tsx:app/routes/contacts.$contactId.tsx
+ import type { LoaderFunctionArgs } from "@remix-run/node";
+ import invariant from "tiny-invariant";

+ 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 });
};
```