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

## 参考
[Part2](https://qiita.com/RuruCun/items/7142f53024030d39c417)に続き、Remixのチュートリアルを走ってみます。

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

## URLSearchParams と GET メソッドによる送信

これまでのインタラクティブUIはすべて下記のいずれかでした。
- URLを変更するリンクか
- formデータをaction関数にポストする


```diff_tsx:app/root.tsx
import type {
  LinksFunction,
+ LoaderFunctionArgs,
} from "@remix-run/node";

// existing imports & exports

export const loader = async ({
  request,
}: LoaderFunctionArgs) => {
+  const url = new URL(request.url);
+  const q = url.searchParams.get("q");
+  const contacts = await getContacts(q);
  return json({ contacts });
};

```

### コードの解説

検索フィールドは、URLを変更するリンクとデータをポストするフォームの両方の特性を持っています。ユーザーが検索フィールドに名前（例えば "ryan"）を入力してEnterキーを押すと、ブラウザのURLにそのクエリが URLSearchParams の形式で追加されます
（例: http://localhost:3000/?q=ryan）。

FormDataのシリアライズ:
このフォームは <Form method="post"> ではないため、Remixはブラウザのように動作し、FormDataをリクエストボディの代わりにURLSearchParamsにシリアライズします。

loader関数によるフィルタリング:
関数はリクエストから検索パラメータにアクセスできます。この例では、URLから検索クエリ（q）を取得し、それを使用して連絡先リストをフィルタリングします。

GETリクエストの特性
この操作はGETリクエストなので、POSTリクエストとは異なり、Remixは action 関数を呼び出しません。GETフォームの送信は、リンクをクリックするのと同じであり、URLだけが変更されます。


## URLをフォーム状態に同期する

ここには、すぐに対処できる UX の問題がいくつかあります。

1. 検索後に [戻る] をクリックすると、リストのフィルタリングが解除されても、フォーム フィールドには入力した値が残ります。
2. 検索後にページを更新すると、リストがフィルターされていても、フォーム フィールドに値が含まれなくなります。
言い換えれば、URL と入力の状態が同期していません。

URLからの値を入力してみましょう。

### URLと入力を同期する。

```diff_tsx:app/root.tsx
// existing imports & exports

export const loader = async ({
  request,
}: LoaderFunctionArgs) => {
  const url = new URL(request.url);
  const q = url.searchParams.get("q");
  const contacts = await getContacts(q);
+ return json({ contacts, q });
};

export default function App() {
+ const { contacts, q } = useLoaderData<typeof loader>();
  const navigation = useNavigation();

  return (
    <html lang="en">
      {/* existing elements */}
      <body>
        <div id="sidebar">
          {/* existing elements */}
          <div>
            <Form id="search-form" role="search">
              <input
                aria-label="Search contacts"
+               defaultValue={q || ""}
                id="q"
                name="q"
                placeholder="Search"
                type="search"
              />
              {/* existing elements */}
```

inputのdefaultValueにqを入れるようにしたため、検索後にページを更新しても、入力フィールドにクエリが表示されます。


### URLのParamsとInputの値を同期する

```diff_tsx:app/root.tsx
// existing imports
+ import { useEffect } from "react";

// existing imports & exports

export default function App() {
  const { contacts, q } = useLoaderData<typeof loader>();
  const navigation = useNavigation();

+  useEffect(() => {
+    const searchField = document.getElementById("q");
+    if (searchField instanceof HTMLInputElement) {
+      searchField.value = q || "";
+    }
+  }, [q]);

  // existing code
```

### 入力のたびに、Listを更新する処理の追加

```diff_tsx:app/root.tsx
+ useSubmit,
} from "@remix-run/react";
// existing imports & exports

export default function App() {
+ const submit = useSubmit();

  // existing code

  return (
    <html lang="en">
      {/* existing elements */}
      <body>
        <div id="sidebar">
          {/* existing elements */}
          <div>
            <Form
              id="search-form"
+              onChange={(event) =>
+                submit(event.currentTarget)
+              }
              role="search"
            >
              {/* existing elements */}
            </Form>
```


#### useSubmitフックの使用
useSubmitフックがRemixによって提供されています。
このフックを使ってフォームを自動的に送信する機能を実装します。

#### フォームの自動送信
FormのonChangeでsubmit関数を呼び出すことで、文字が入力されるたびに、Submitされる処理となります。



## 検索へスピナーの追加

`useNavigation`を使用して、ユーザーが検索を実行しているかどうかを判断する変数`searching`を作成します。
これは、`navigation.location`がundefined以外かつ、searchパラメータに "q"（検索クエリ）が存在するかをチェックします。
つまり、navigation.locationがpendingの状態かつ、検索中の場合searchingになります。
ページを読み込み後はｍnavigation.locationがundefinedになるので、qに値を持っていてもfalseになります。

```diff_tsx:app/root.tsx
// existing imports & exports

export default function App() {
+  const searching =
+    navigation.location &&
+    new URLSearchParams(navigation.location.search).has(
+      "q"
+    );

  // existing code
  
  <input
    aria-label="Search contacts"
+    className={searching ? "loading" : ""}
    defaultValue={q || ""}
    id="q"
    name="q"
    placeholder="Search"
    type="search"
  />
  <div
    aria-hidden
+    hidden={!searching}
    id="search-spinner"
  />


    <div
      className={
+        navigation.state === "loading" && !searching
          ? "loading"
          : ""
      }
      id="detail"
    >
      <Outlet />
    </div>

}
```

## ブラウザの履歴を制御する。

現状では、1文字ごとにページ移動したことになってしまうため、これを修正する。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3589058/b8e9e392-412e-671a-97a7-8d6031243df0.png)

初期の検索かどうか、を q === null で判断し、最初の検索以外では、submitのreplaceにｔrueを与えることで、現在のブラウザの履歴エントリを置き換えるよう指示しています。

```diff_tsx
            <Form
              id="search-form"
+              onChange={(event) => {
+                const isFirstSearch = q === null;
+                submit(event.currentTarget, {
+                  replace: !isFirstSearch,
+                });
+              }}
              role="search"
            >

```


## ページ遷移なしでフォーム送信を実装する

これまでは、ページ遷移するようになっていたが、ページ遷移なしで実装したいパターンもある。
下記の2つを実装して、ページ遷移なしのフォーム送信を実装します。

### FormをFetcherに変更する。
`<Form>`を`useFetcher`に置き換えます。

### actionを作成する。
これまでのチュートリアル通り、データを更新するactionを追加します。


```diff_tsx:contacts.$contactId.tsx
// existing imports
import {
  Form,
  useFetcher,
  useLoaderData,
} from "@remix-run/react";
import type {
  ActionFunctionArgs,
+  LoaderFunctionArgs,
} from "@remix-run/node";
// existing imports & exports

+ import { getContact, updateContact } from "../data";
// existing imports

+ export const action = async ({
+  params,
+  request,
+}: ActionFunctionArgs) => {
+  invariant(params.contactId, "Missing contactId param");
+  const formData = await request.formData();
+  return updateContact(params.contactId, {
+    favorite: formData.get("favorite") === "true",
+  });
+};

// existing code

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

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

```

コードの変更自体は、`<fetcher.Form>`へ置き換えるだけです。
`<fetcher.Form>`を使用すると、アクションの実行後にページのデータが自動的に再検証されます。
これにより、ユーザーインターフェース上での変更がリアルタイムに反映され、エラーも同じように処理されます。

サンプルでは、管理画面でお気に入りに追加すると、サイドバーも自動で更新され星が表示されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3589058/7bc170fe-e7ee-4842-b392-f9ae5493f48e.png)


## Optimistic UI (楽観的UI)を実装する

Optimistic UIを実装する(サーバーの反応を待たずに、画面を変更すること)

contact:favorite

```diff_tsx:contacts.$contactId.tsx
// existing imports

const Favorite: FunctionComponent<{
  contact: Pick<ContactRecord, "favorite">;
}> = ({ contact }) => {
  const fetcher = useFetcher();
+ const favorite = fetcher.formData
+   ? fetcher.formData.get("favorite") === "true"
+   : contact.favorite;

```

#### フォームデータの確認
最初に `fetcher.formData` が存在するかどうかをチェックします。
これは、フォームが送信された後に設定されます。 ページを開いたタイミングではundefinedです。
```
fetcher.formData ? 
```

#### オプティミスティックなUI更新
もし `fetcher.formData` が存在する場合、それはユーザーが最後にフォームに入力した値を表します。
この値に基づいて、お気に入りの状態（favorite）を即座に更新します。
これにより、サーバーからの応答がまだないにもかかわらず、UIはユーザーの操作を反映します。

```
fetcher.formData.get("favorite") === "true"
```

#### 失敗時のUI戻し
ネットワークリクエストが失敗すると、`fetcher.formData` はリセットされます。
その結果、条件式は`contact.favorite`（元の読み込んだデータの状態）に戻ります。
これにより、UIはサーバーの実際のデータ（リクエスト前の状態）を反映するようになります。

```
+ const favorite = fetcher.formData
+   ? fetcher.formData.get("favorite") === "true"
+   : contact.favorite;
 ```