PHP/laravelのエンジニアがRemixのチュートリアルを途中までやってみた

## 概要

Remix（TypeScript）のプロジェクトに参画させてもらえそうなのでまずはRemixのチュートリアルをやってみてざっくりどういうものか知ろうと思う。

## すみません

「laravelのエンジニアが〜」というタイトルですが、別にRemixの開発方法をlaravelに置き換えて説明したりしてません。すみませんやろうと思いましたが、そんな器用なことはできなかった。

## 余談

「取っ掛かりは動画教材がいいな〜」っと思ってRemixの口座をUdemyで探したが海外講師さんの動画しかなく、公式チュートリアルをやってみることにした。（英語がぜんぜんできない自分にとってこの辺のハードルはできるだけ下げたかった。。。）

ちなみに自分のレベル感はフロントエンドはほぼやってなくて、laravelでAPIの開発とか、やったとしてもlaravelのMPAでbladeのテンプレートエンジンを使ってちょっと画面表示を修正したことがあるくらい。JavaScriptはProgateを一周したくらいでTypeScriptはほぼ知見はない。Reactもほぼ触ったことは無い。

## やってみること

下記のチュートリアルを試してみる。（最近のフロントエンド由来のフレームワークは公式のチュートリアルがちゃんとしてて、すごくいいなあ。）

https://remix.run/docs/en/main/start/tutorial

自分は「ちゃんと理解してから触り始める」ことができない人間なので、まずは真似しながらざっくりとどういうものか知りたい。あまりいいことではないがかなり惰性でこのチュートリアルに沿って体験してみようと思う。

おそらくDBやらデータ取得のサーバーやらはすでにようにされている模様なので比較的楽にチュートリアルを体験することができそう。

## 環境

今回は初めてなのでMacに直接環境を作ってチュートリアルを完走してみようと思う。（Dockerとかは使わない）
どうやら`npx`コマンドが通る必要があるらしい、node.jsが入っていれば当該コマンドは使えるらしい。
自身のMacにはvoltaを使ってnode.jsが入ってる。

voltaのインストールの記事は自分で書いてなかったが、node.jsのバージョン切り替え記事はあったので記載しておく。

https://qiita.com/miriwo/items/9e9add6c78d1cb578513

長々書いたが、`npx -h`のコマンドがMacのローカルでnot foundにならなければ多分大丈夫だろう。問題が出たらそのときに考えよう。

```terminal
$ node -v
v18.18.2

$ npx -h
Run a command from a local or remote npm package

Usage:
npm exec -- <pkg>[@<version>] [args...]
npm exec --package=<pkg>[@<version>] -- <cmd> [args...]
npm exec -c '<cmd> [args...]'
npm exec --package=foo -c '<cmd> [args...]'

Options:
[--package <package-spec> [--package <package-spec> ...]] [-c|--call <call>]
[-w|--workspace <workspace-name> [-w|--workspace <workspace-name> ...]]
[-ws|--workspaces] [--include-workspace-root]

alias: x

Run "npm help exec" for more info
```

## やってみる

### テンプレートの作成

何はともあれ公式のチュートリアルに沿ってやってみよう。テンプレートなるものを用意するらしい。
どうやら下記のコマンドを実行するらしいので任意のディレクトリに移動してコピーして実行してみよう。

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

![CleanShot 2024-05-05 at 18.16.00@2x.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/306417/1c517691-93a0-f79c-15e6-b191a954060f.png)

https://remix.run/docs/en/main/start/tutorial#setup

実行したら下記のようにインストール可否を問われるので`y`を入力後Enterを押下した。（`y`を入力する必要無いかもしれない）

```terminal
$ npx create-remix@latest --template remix-run/remix/templates/remix-tutorial
Need to install the following packages:
create-remix@2.9.1
Ok to proceed? (y)
```

下記のようにプロジェクト名を問われる。まあチュートリアルだし、サジェストされているデフォルトのままでEnterを押下するでいいだろう。

```terminal
 remix   v2.9.1 💿 Let's build a better website...

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

下記のようにgitのイニシャライズをするか聞かれる。なんて親切なんだ、、、！Macのローカル直接だしYesでEnterを押下した。

```terminal
   git   Initialize a new git repository? (recommended)
         ● Yes  ○ No
```

npmで依存関係のインストールするか聞かれるのでYESを選んだ。

```terminal
  deps   Install dependencies with npm? (recommended)
         ● Yes  ○ No 
```

お、なんか終わったっぽいぞ。いやー親切、、！

```terminal
  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
```

今作ってもらったプロジェクトのアプリケーションを起動してみようと思う。下記コマンドを実行してみよう。（このコマンドは公式ドキュメントのチュートリアルにも記載されている。）

```terminal
cd ./my-remix-app
npm install
npm run dev
```

![CleanShot 2024-05-05 at 18.24.23@2x.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/306417/fedd7e36-4f57-c0be-df7d-2139618a1da6.png)

https://remix.run/docs/en/main/start/tutorial#setup

npm installでなんか脆弱性が指摘されているけど、まあエラーになったわけじゃないので続行する。

```terminal
$ npm install

up to date, audited 796 packages in 3s

248 packages are looking for funding
  run `npm fund` for details

2 vulnerabilities (1 moderate, 1 high)

To address all issues (including breaking changes), run:
  npm audit fix --force

Run `npm audit` for details.
```

http://localhost:5173/ でどうやらアクセスできるらしい。アクセスしてみよう。

```terminal
$ npm run dev

> dev
> remix vite:dev

  ➜  Local:   http://localhost:5173/
  ➜  Network: use --host to expose
  ➜  press h + enter to show help
```

おおお、チュートリアル通りの画面が開いた。安心する。ココまでで詰まらないようにできていることに感動する。

![CleanShot 2024-05-05 at 18.30.21@2x.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/306417/3db4829f-f374-b828-5337-6327cbe7eb19.png)

### スタイルを当てる

先ほどまでの手順で表示された下記の画面だが、これはスタイルが当たっておらず表示内容が左によってしまっている。
自分専用のTODOリストならまだしもこの状態ではわかりにくい。そのためチュートリアルの内容に沿ってスタイルを当ててみる。

![CleanShot 2024-05-05 at 18.30.21@2x.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/306417/3db4829f-f374-b828-5337-6327cbe7eb19.png)

どうやら上記の画面表示を司っているファイルは`my-remix-app/app/root.tsx`らしい。

チュートリアルに従って編集してスタイルを当ててみる。（紹介されているコードをまるっと、すでに記載されているimportの前に追記した。）

![CleanShot 2024-05-05 at 18.40.27@2x.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/306417/cf3dc2ce-7958-f632-05f6-a88e6aae7f72.png)

指定されたコードを雑に既存のimport文の前に突っ込んでみる。

![CleanShot 2024-05-05 at 18.41.52@2x.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/306417/1e395e41-7788-87cc-7e57-d1b017b106ab.png)

スタイルが当たった。

![CleanShot 2024-05-05 at 18.42.57@2x.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/306417/715021be-d914-c027-8761-2964d223bdb5.png)

一旦コミットして次に行ってみよう。

### 表示リンク先の画面を作ろう

今の状態はあくまでスタイルを当てただけだ。画面上のリンクをクリックしても404の画面が表示されるだけである。このリンク先の画面をこれから作っていく。

https://remix.run/docs/en/main/start/tutorial#the-contact-route-ui

どうやら`/contacts/1`にアクセスしたときに開く画面を作るらしい。コマンドが明記されているので順に実行しよう。

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

![CleanShot 2024-05-05 at 18.51.35@2x.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/306417/0fc8f1af-1478-cc9b-a98a-54f04214296c.png)

まだチュートリアルの先を読んでないんだけど、おそらく`app/routes/`直下のディレクトリ構造がルーティングのパス表現担っているんだろうなと予想した。`$contactId`の部分でパスパラメーターの値を取ってる感じかな？？
というか[個人開発](https://qiita.com/miriwo/items/46c5e74a1b6abd30a0d5)の方でReactを使ってて、ディレクトリ構造がそのままルーティングのパスになっているって[Sado](https://qiita.com/derasado)さんが教えてくれた。

パスパラメーターの裏付けとして下記のように書かれてたので間違いないと思う。

![CleanShot 2024-05-05 at 18.58.16@2x.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/306417/62d8714e-f7aa-39dc-28f6-ffafac84e48b.png)

`my-remix-app/app/routes/contacts.$contactId.tsx`のファイルにチュートリアルの画面からコピーした内容を貼り付けてみる。

![CleanShot 2024-05-05 at 18.58.58@2x.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/306417/ec474df1-00a4-af4e-85e4-2ff904a564f2.png)

これで、画面上の「Your Name」とかのリンクをクリックしてみようと思う

![CleanShot 2024-05-05 at 18.42.57@2x.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/306417/715021be-d914-c027-8761-2964d223bdb5.png)

特段画面に変化は無いように思えるが、まず404が返らなくなったのと、URLのパス部分がちゃんと`/contact/1`になっており、先程まで404が出ていたページであることがわかる。

![CleanShot 2024-05-05 at 22.04.56@2x.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/306417/5b24e0c3-9603-a5e6-0ebd-d72fb60c6c3e.png)

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

コンポーネントの共有的なことがおそらくできるっぽい。「ネストされたルーティング」って言葉がなんか難しく感じるし、自分の認識が間違えている可能性もあるけどとりあえず真似してみよう。

https://remix.run/docs/en/main/start/tutorial#nested-routes-and-outlets

`my-remix-app/app/root.tsx`を開き、impoortに`Outlet,`を追記する。その後、57行目付近に下記のコードを追記する。

```
<div id="detail">
  <Outlet />
</div>
```

すべての修正後は下記のようになる。

```my-remix-app/app/root.tsx
import type { LinksFunction } from "@remix-run/node";
// existing imports

import appStylesHref from "./app.css?url";

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

import {
  Form,
  Links,
  Meta,
  Outlet,
  Scripts,
  ScrollRestoration,
} from "@remix-run/react";

export default function App() {
  return (
    <html lang="en">
      <head>
        <meta charSet="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <Meta />
        <Links />
      </head>
      <body>
        <div id="sidebar">
          <h1>Remix Contacts</h1>
          <div>
            <Form id="search-form" role="search">
              <input
                id="q"
                aria-label="Search contacts"
                placeholder="Search"
                type="search"
                name="q"
              />
              <div id="search-spinner" aria-hidden hidden={true} />
            </Form>
            <Form method="post">
              <button type="submit">New</button>
            </Form>
          </div>
          <nav>
            <ul>
              <li>
                <a href={`/contacts/1`}>Your Name</a>
              </li>
              <li>
                <a href={`/contacts/2`}>Your Friend</a>
              </li>
            </ul>
          </nav>
        </div>
        <div id="detail">
          <Outlet />
        </div>

        <ScrollRestoration />
        <Scripts />
      </body>
    </html>
  );
}

```

コンポーネントを使いまわしたり、ベースとなるコンポーネントを定義しておいて共通化を図れるよねってことだと思う。

`/contacts/1`にアクセスすると下記のように表示されたのでチュートリアルと一致している。

![CleanShot 2024-05-05 at 22.43.33@2x.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/306417/f41dbf99-f02d-dedf-2fcc-294b49594a56.png)

しかし、、、`/contacts/1`の画面表示に変更が加わったのに、なぜ`root.tsx`にOutletを追加したんだろう？なぜ`my-remix-app/app/routes/contacts.$contactId.tsx`に編集を加えないんだろう？ `/`の画面表示を変更したならまだしもなんでだ！？！？

### 部分的なローディング（クライアント側ルーティング）

今のコードの状態でサイドバーの「Your Name」などのリンクをクリックすると、都度サーバーにリクエストを送っている。それを証明するためにサイドバーのリンクをクリックすると、ブラウザの「再読み込みマーク」が一時的に「✕」になる。このことから都度都度リクエストを送っていることがわかる。これをチュートリアルでは「URL に対する完全なドキュメント リクエスト」と言っているようだ。

![CleanShot 2024-05-05 at 23.06.23.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/306417/d0d3e3e9-7ea6-a6e5-6dcc-4f8bee016755.gif)

まあこれでも悪くないが、最近のリッチなフロントエンドはコンポーネントごとに情報を取りに行ったりして、部分的にページを更新することもある。これをチュートリアルでは「クライアント側のルーティング」と言っているっぽい。

それを実現するには現在`<a>`要素を使っているサイドバーのリンクを書き換えて上げる必要があるらしい。

https://remix.run/docs/en/main/start/tutorial#client-side-routing

`my-remix-app/app/root.tsx`を開き、impoortに`Link,`を追記する。その後、サイドバーの「Your Name」などのタグ部分を`<a>`から`Link`を使ったものに書き換える。

すべての修正後は下記のようになる。

```my-remix-app/app/root.tsx
import type { LinksFunction } from "@remix-run/node";
// existing imports

import appStylesHref from "./app.css?url";

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

import {
  Form,
  Link,
  Links,
  Meta,
  Outlet,
  Scripts,
  ScrollRestoration,
} from "@remix-run/react";

export default function App() {
  return (
    <html lang="en">
      <head>
        <meta charSet="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <Meta />
        <Links />
      </head>
      <body>
        <div id="sidebar">
          <h1>Remix Contacts</h1>
          <div>
            <Form id="search-form" role="search">
              <input
                id="q"
                aria-label="Search contacts"
                placeholder="Search"
                type="search"
                name="q"
              />
              <div id="search-spinner" aria-hidden hidden={true} />
            </Form>
            <Form method="post">
              <button type="submit">New</button>
            </Form>
          </div>
          <nav>
            <ul>
              <li>
                <Link to={`/contacts/1`}>Your Name</Link>
              </li>
              <li>
                <Link to={`/contacts/2`}>Your Friend</Link>
              </li>
            </ul>
          </nav>
        </div>
        <div id="detail">
          <Outlet />
        </div>

        <ScrollRestoration />
        <Scripts />
      </body>
    </html>
  );
}
```

この状態で`/`からサイドバー内部のリンクをクリックして`/contacts/1`などに遷移しても画面の読み込みが行われない。「クライアント側のルーティング」が行われていることがわかる。

![CleanShot 2024-05-06 at 10.34.17.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/306417/34a3bfe2-5f1d-8bec-af90-b9896d8f138a.gif)


### コンポーネント単位でデータを取得（データのロード）

コンポーネント単位（今回はサイドバー）でサーバーからデータを取得して表示する方法を体験する。
今までサイドバーは「Your Name」などのリンクが存在していた。
しかしこれは、いわばハードコーディングされたリンクが設置されているだけだった。
この部分をサーバーから取得した文字列でリンクを作りたい場合、サイドバーのコンポーネント単位でデータを取得、表示するようにしたい。
その方法が下記にかかれている。追加するコードが比較的多く、ちょっと不安だが実際はサイドバーのコンポーネント内でサーバーからデータを取得して、取得したデータをサイドバーに表示しているだけなのでシンプルだったりする。

https://remix.run/docs/en/main/start/tutorial#loading-data

指示通りに`my-remix-app/app/root.tsx`を修正してみた↓（修正範囲が大きいので個々の内容には触れない）

```my-remix-app/app/root.tsx
import type { LinksFunction } from "@remix-run/node";
// existing imports

import appStylesHref from "./app.css?url";

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

import { json } from "@remix-run/node";
import {
  Form,
  Link,
  Links,
  Meta,
  Outlet,
  Scripts,
  ScrollRestoration,
  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">
      <head>
        <meta charSet="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <Meta />
        <Links />
      </head>
      <body>
        <div id="sidebar">
          <h1>Remix Contacts</h1>
          <div>
            <Form id="search-form" role="search">
              <input
                id="q"
                aria-label="Search contacts"
                placeholder="Search"
                type="search"
                name="q"
              />
              <div id="search-spinner" aria-hidden hidden={true} />
            </Form>
            <Form method="post">
              <button type="submit">New</button>
            </Form>
          </div>
          <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>
        <div id="detail">
          <Outlet />
        </div>

        <ScrollRestoration />
        <Scripts />
      </body>
    </html>
  );
}

```

この状態で`/`にアクセスするとサイドバーの内容が更新され、ハードコーディングしたものではなくサーバーから取得したデータが表示されていることがわかる。

![CleanShot 2024-05-06 at 11.02.52@2x.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/306417/06af1c55-16f3-2077-0b59-48301a68a2a5.png)

ちなみに、上記のコードは30行目と60行目でTypeScriptの型のエラーが出るらしい。これは次で修正するから今は無視していいと書いてある。

### 型の指定（型推論）

TypeScriptは型に厳しい。簡単に言うと曖昧なデータ型は基本許されないと思っていいっぽい。
現在エディタ上で型エラーとなっている30行目と60行目を見てみよう。

まずは30行目、、、

`useLoaderData()`の戻り値の型が指定されていないっぽくて定数`contacts`にどのような型の値が入るかわかっていない状態っぽい。

![CleanShot 2024-05-06 at 11.16.45@2x.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/306417/84baacb7-1b1a-2506-faf4-392d086aab90.png)

更に60行明、、、

おそらく上記で触れた定数`contatcts`の内容を一個一個取り出して`contact`に入れ込むなどしてサイドバーの表示をループ的に作っているところだろう。もともとの定数`contatcts`の型が指定されていないから`contact`はany型になると言っている。any型って「何でも入ります」みたいなデータ型なはずで、かなり広義になってしまうので型に厳密なTypeScriptだと多様してほしくなくてエラーになるんじゃなかろうか。（TypeScriptのanyはPHPでいうところのmixedてきな感じ？）

![CleanShot 2024-05-06 at 11.13.24@2x.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/306417/99745677-377f-2db6-b408-c96ba019aa4e.png)

本セクションでは、アノテーションを使って型をちゃんと指定してあげようぜ〜という内容っぽい。`useLoaderData()`の戻り値の型を明示的に指定してあげる感じ。（これはRemixの内容というよりはTypeScript側の話なのかな？？）

https://remix.run/docs/en/main/start/tutorial#type-inference

30行目を`const { contacts } = useLoaderData();`から`const { contacts } = useLoaderData<typeof loader>();`のように書き換えてあげればおそらく型のエラーは消えるはず。

ココまでのコードを下記に記載する。

```my-remix-app/app/root.tsx
import type { LinksFunction } from "@remix-run/node";
// existing imports

import appStylesHref from "./app.css?url";

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

import { json } from "@remix-run/node";
import {
  Form,
  Link,
  Links,
  Meta,
  Outlet,
  Scripts,
  ScrollRestoration,
  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<typeof loader>();
  return (
    <html lang="en">
      <head>
        <meta charSet="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <Meta />
        <Links />
      </head>
      <body>
        <div id="sidebar">
          <h1>Remix Contacts</h1>
          <div>
            <Form id="search-form" role="search">
              <input
                id="q"
                aria-label="Search contacts"
                placeholder="Search"
                type="search"
                name="q"
              />
              <div id="search-spinner" aria-hidden hidden={true} />
            </Form>
            <Form method="post">
              <button type="submit">New</button>
            </Form>
          </div>
          <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>
        <div id="detail">
          <Outlet />
        </div>

        <ScrollRestoration />
        <Scripts />
      </body>
    </html>
  );
}

```

### パスパラメーター（ローダーの URL パラメータ）

現在、サイドバーの任意の人名のリンクをクリックすると、すべて同一の静的なページが開かれる。
猫のアイコンが表示される静的なページのURLのパスを見ると、サイドバーでクリックした任意の人名のケバブケースがパスに入っている。（`/contacts/christopher-chedeau`など）
このURLのパスの`christopher-chedeau`の部分はパスパラメーターと言って可変の文字列を与え、文字列によって画面のふるまい分けに使われる事がある。

このセクションではおそらく、「パスパラメーターの値を受け取って、受け取った値を使ってサーバーにアクセスして、表示内容を出し分けよう」ということを体験させたいのだと思う。深く考えずとりあえず指示通りやってみよう。
コードを見る感じ、やはりパスパラメーターの値を使って情報を取得して、表示に使っていることがわかる。

https://remix.run/docs/en/main/start/tutorial#url-params-in-loaders

今回編集を加えるのは今までいじってきたmy-remix-app/root.tsxではなく**my-remix-app/app/routes/contacts.$contactId.tsx**になるので注意してほしい。お恥ずかしながら自分はめっちゃ詰まった。

ココまでのコードを下記に記載する。（ちなみにここまでの修正だけだと型のえラーがまた発生する。）

```my-remix-app/app/routes/contacts.$contactId.tsx
import { json } from "@remix-run/node";
import { Form, useLoaderData } from "@remix-run/react";
import { getContact } from "../data";
import type { FunctionComponent } from "react";
import type { ContactRecord } 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>();

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

```

### 再びの型の指定とパスパラメーターでデータが取れなかったときのふるまい（パラメータの検証と応答のスロー）

先にも記載したが型のエラーが発生している。本当は原理原則を理解して修正したほうがいいのかもしれないが一旦チュートリアルに沿って変更してみる。
まずは当該のファイルを下記のように修正をした。

```my-remix-app/app/routes/contacts.$contactId.tsx
import { json } from "@remix-run/node";
import { Form, useLoaderData } from "@remix-run/react";
import { getContact } from "../data";
import type { FunctionComponent } from "react";
import type { ContactRecord } from "../data";
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);
  return json({ contact });
};

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

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

```

まだ一部エラーは出るがかなり減ったように思える。

サイドバーの人名をクリックして正常に表示されることを確認しよう。
ここで一つ問題なのは、今はサイドバーの人名をクリックすると人名のケバブケースがパスパラメーターに含まれ、パスパラメーターの値を使ってサーバーに人のデータを取りに行っている。サイドバーからのクリックならもちろんサーバーに当該のデータが存在することがわかっているのでエラーにはならない。

例えば　`/contacts/hoge-fuga`にブラウザのURL入力部分直接指定でアクセスを行った場合どうなるだろうか？（hoge-fugaという人物の情報はサーバーは持っていないこととする）

下記のようにデータにアクセスできない旨のエラーが画面に出てしまうのだ。

![CleanShot 2024-05-06 at 13.23.35@2x.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/306417/8d3714c8-824a-8fdd-7f4d-e81eb128e91e.png)

なので、チュートリアルに沿ってサーバーからnullが返った場合の対応を追加しよう。
下記のようにコードを修正した。

```my-remix-app/app/routes/contacts.$contactId.tsx
import { json } from "@remix-run/node";
import { Form, useLoaderData } from "@remix-run/react";
import { getContact } from "../data";
import type { FunctionComponent } from "react";
import type { ContactRecord } from "../data";
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 });
};

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

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

```

この状態で`/contacts/hoge-fuga`にアクセスすると404が返るようになって先程のようなエラーは出なくなる。

![CleanShot 2024-05-06 at 13.27.02@2x.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/306417/283d25e4-e165-8504-8707-190863c78e2d.png)


一旦ここまで！！続きは別の記事でまとめます。尻切れトンボですみません。