Remix v2のHooks全部使う(useParams～useSubmit編)

# 序

Remix v2のHooks全部使うシリーズの最終回です。
前々回の記事に経緯や前提などを書いているので、この記事では割愛します。

#### 前回(useLoaderData～useOutletContext編)
https://qiita.com/kedama-t/items/29972d958f640285abbc

#### 前々回(useActionData～useHref編)
https://qiita.com/kedama-t/items/f648183771585422a31d

この記事では全26のHooksのうち、アルファベット順に`useParams`～`useSubmit`を取り上げます

引き続き、内容の誤りや勘違い、モアベターな実装方法などありましたら、編集リクエスト、コメントで教えていただけると嬉しいです。

## [useParams](https://remix.run/docs/en/main/hooks/use-params)
`useParams`は動的に変化するパラメータの部分をURLから取得するためのHookです。

```tsx:useParams.$.tsx
import { Link, Outlet, useParams } from '@remix-run/react';

export default function UseParams() {
  const params = useParams();
  // Splat Routesの場合は、`*`にパスが全部入ってくる
  const splat = params['*'];

  return (
    <>
      <h2>useParams</h2>
      <div className="card">
        <Link to="./hoge/fuga">./hoge/fugaに移動</Link>
        <br />
        <Link to="child/hoge">child/hogeに移動</Link>
      </div>
      <div className="card">{splat}</div>
      <Outlet />
    </>
  );
}
```

```tsx:useParams.child.$param.tsx
import { Outlet, useParams } from '@remix-run/react';

export default function UseParamsChild() {
  const params = useParams();
  // Dynamic Segmentsは名前を指定して取得
  const param = params['param'];

  return (
    <>
      <h2>useParams-child</h2>
      <div className="card">{param}</div>
      <Outlet />
    </>
  );
}
```

### ポイント
- Remixにおいては、動的なルーティングは2種類あります
    - 一つは[Dynamic Segments](https://remix.run/docs/en/main/file-conventions/routes#dynamic-segments)で、URLのパスのうち、特定の箇所が動的に変化するものです
        - 例: `/articles/1/post`、`/articles/2/post`みたいな、記事idが変わるパターン
            - これはルートモジュール`articles.$id.post.tsx`にマッチします
        - 例: `/user/kedama-t/log/1`のように、`user`と`log`が固定で、ユーザーidとログidが変動するパターン
            - これはルートモジュール`user.$userId.log.$logId.tsx`にマッチします
    - もう一つは、[Splat Routes](https://remix.run/docs/en/main/file-conventions/routes#splat-routes)で、URLのうち、特定の箇所以降が動的に変化するものです
        - 例: `/files/images/avatars/kedama-t.jpg`
            - この例では、`images`と`avetars`と`kedama-t.jpg`の3か所が、動的に変化する可能性がありますが、ルートモジュール`files.$.tsx`を作っておくと、`images`、`avetars`、`kedama-t.jpg`の部分がいかに変化しても、また、階層がもう一つ増えて`/files/images/avatars/lg/kedama-t.jpg`のようになったとしても、全部`files.$.tsx`にマッチします
    - こうしてマッチしたときの動的に変化する部分の値を取得するのが`useParams`です
- Dynamic Segmentsの場合は、ファイル名に付けた`${セグメント名}`のセグメント名で取れます
- Splat Routesの場合は、`['*']`で`$`の部分に相当するURLの文字列が得られます

## [unstable_usePrompt](https://remix.run/docs/en/main/hooks/use-prompt)

これは`unstable`で、しかも`unstable`を外す予定がないHookということなので、割愛します。
> We do not plan to remove the `unstable_` prefix from this hook because the behavior is non-deterministic across browsers when the prompt is open, so React Router cannot guarantee correct behavior in all scenarios. To avoid this non-determinism, we recommend using `useBlocker` instead which also gives you control over the confirmation UX.

:::note info
拙訳
> プロンプトが開いているときの振る舞いはブラウザによって非決定的であり、React Routerはすべてのシナリオにおいて正しい振る舞いを保証できないため、このHookの`unstable_`プレフィクスを外す予定はありません。この非決定性を避けるために、同様に確認のUXを制御できる`useBlocker`の使用をお勧めします。
:::

## [useResovledPath](https://remix.run/docs/en/main/hooks/use-resolved-path)
`useResolvedPath`は、与えられたパスを現在のルートに対して解決し、`Path`オブジェクトを返すhookです。

```tsx:useResolvedPath.tsx
import { useHref, useResolvedPath } from '@remix-run/react';

export default function UseResolvedPath() {
  const resolvedPath = useResolvedPath('../useLocation?search=hoge#hash');
  //　useHrefに渡すと絶対パスの文字列になる
  const href = useHref(resolvedPath);

  return (
    <>
      <h2>useResolvedPath</h2>
      <div className="card">
        <ul>
          <li>{resolvedPath.pathname}</li>
          <li>{resolvedPath.search}</li>
          <li>{resolvedPath.hash}</li>
          <li>{href}</li>
        </ul>
      </div>
    </>
  );
}
```

### ポイント
- これもいまいち使いどころがよくわからないHook
    - よほど規模の大きなWebアプリで複雑な相対パスでのルーティングが必要、みたいなときには使えるのかもですが…

### [useRevalidator](https://remix.run/docs/en/main/hooks/use-revalidator)
`useRevalidator`はRemixにおける通常のデータ変更(`loader`⇒`Component`⇒`action`)以外の方法でデータを再検証するためのHookです。
で、Remixにおける「再検証(revalidate)」って**なんすか**って話なんですが、要は`loader`の再実行です。
つまり、`useRevalidator`は任意のタイミングで`loader`を再実行するHookです。

```tsx:useRevalidator.tsx
import { useLoaderData, useRevalidator } from '@remix-run/react';
import { randomUUID } from 'crypto';

export async function loader() {
  // 読み込みに0.5秒かかる
  await new Promise((resolve) => setTimeout(() => resolve(null), 500));
  return { uuid: randomUUID().toString() };
}

export default function UseRevalidator() {
  const { uuid } = useLoaderData<typeof loader>();
  const revalidator = useRevalidator();

  return (
    <>
      <h2>useRevalidator</h2>
      <div className="card">
        <p>{revalidator.state}</p>
        <p>{uuid}</p>
      </div>
      <button className="button" onClick={() => revalidator.revalidate()}>
        revalidate
      </button>
    </>
  );
}
```

### ポイント
- `revalidator.revalidate()`を実行すると、`revalidator.state`が`loading`になり、そのルートまでの`loader`が実行されます
- `loader`の処理が終わると`revalidator.state`は`idle`になります
    - このタイミングで、`useDataLoader`で取得できるデータが再実行時のものに変わります
    - すると、コンポーネントが再レンダリングされて、コンポーネントには最新のデータが表示されます
- 現在のルートの`loader`だけではなく、祖先のルートモジュールにある`loader`も実行されます
    - ルートモジュールから[`shouldRevalidate`](https://remix.run/docs/en/main/route/should-revalidate)関数を`export`しておくと、`loader`の再実行を制御できます

## [useRouteError](https://remix.run/docs/en/main/hooks/use-route-error)
`useRouteError`は、ルートモジュールの`loader`関数や`action`関数で発生したエラーの情報を取得するためのHookです。
ルートモジュール関数の[`ErrorBoundary`](https://remix.run/docs/en/main/route/error-boundary)と組み合わせて使います

```tsx:useRouteError.tsx
import { useRouteError } from '@remix-run/react';

export async function loader() {
  try {
    throw new Error('エラーが発生しました');
  } catch (error: any) {
    throw new Response(error.message, {
      status: 500,
    });
  }
}

export default function UseRouteError() {
  return (
    <>
      <h2>useRouteError</h2>
      <div className="card">このコンポーネントは表示されません</div>
    </>
  );
}

export function ErrorBoundary() {
  const error = useRouteError() as { status: number; data: string };
  return (
    <>
      <h2>useRouteError</h2>
      <div className="card">
        {error.status}:{error.data}
      </div>
    </>
  );
}
```

### ポイント
- `loader`や`action`でエラーが発生した場合に、`Response`を`throw`することで、ルートコンポーネントの代わりに`ErrorBoundary`コンポーネントがレンダリングされます
- `ErrorBoundary`コンポーネント内で`useRouteError`を使うと、ステータスコードやエラーメッセージなどを受け取ってレンダリングすることができます

## [useRouteLoaderData](https://remix.run/docs/en/main/hooks/use-route-loader-data)
`useRouteLoaderData`は、自分の親～祖先のルートモジュールの`loader`関数の戻り値を取得することができるHookです。
親ルートの情報を子ルートに渡すには、`<Outlet context={*} />`を使う方法がありますが、あんまり深いといわゆるバケツリレー、ドリリングといわれるような事態に陥ります。
`useRouteLoaderData`を使うと、子ルート側から親ルートの`loader`に直接アクセスすることができます。

```tsx:useRouteLoaderData.tsx
import { useLoaderData, useOutlet, Link } from '@remix-run/react';

export async function loader() {
  return { message: 'from useRouteLoader' };
}

export default function UseRouteLoaderData() {
  const data = useLoaderData<typeof loader>();
  const outlet = useOutlet();

  if (outlet) {
    return outlet;
  }
  return (
    <>
      <h2>useRouteLoaderData</h2>
      <div className="card">
        <p>{data.message}</p>
      </div>

      <div className="card">
        <Link to="hoge">hogeに移動</Link>
      </div>
    </>
  );
}
```

```tsx:useRouteLoaderData.$id.tsx
import { useLoaderData, useOutlet, Link } from '@remix-run/react';
import type { LoaderFunctionArgs } from '@remix-run/node';

export async function loader(args: LoaderFunctionArgs) {
  const id = args.params['id'];
  return { message: `from useRouteLoader.${id}` };
}

export default function UseRouteLoaderData() {
  const data = useLoaderData<typeof loader>();
  const outlet = useOutlet();

  if (outlet) {
    return outlet;
  }
  return (
    <>
      <h2>useRouteLoaderData-$id</h2>
      <div className="card">
        <p>{data.message}</p>
      </div>

      <div className="card">
        <Link to="child">hoge/childに移動</Link>
      </div>
    </>
  );
}
```

```tsx:useRouteLoaderData.$id.child.tsx
import { useRouteLoaderData } from '@remix-run/react';
import type { loader } from './useRouteLoaderData';

export default function UseRouteLoaderDataChild() {
  const dataFromUseRouteLoaderData = useRouteLoaderData<typeof loader>(
    'routes/useRouteLoaderData'
  );
  const dataFromUseRouteLoaderDataId = useRouteLoaderData<typeof loader>(
    'routes/useRouteLoaderData.$id'
  );

  return (
    <>
      <h2>useRouteLoaderData-$id-child</h2>
      <div className="card">
        <p>{dataFromUseRouteLoaderData?.message}</p>
      </div>
      <div className="card">
        <p>{dataFromUseRouteLoaderDataId?.message}</p>
      </div>
    </>
  );
}
```

### ポイント
- 引数の`id`は、`app`フォルダからの相対パスで、拡張子を抜いたものになります
- 自分の祖先にあたるルートからじゃないと取れません
    - `loader`だけのAPIルートを別途`/api/hoge`とかに作っても、自分が`users/$id`だとすると、祖先じゃないので取れないです
    - この場合は`useFetcher`([前々回記事](https://qiita.com/kedama-t/items/f648183771585422a31d)参照)の出番でしょうか
- [Dynamic Segments](https://remix.run/docs/en/main/file-conventions/routes#dynamic-segments)を含む`id`を指定した場合、`$hoge`のところは今自分がいるルートに合わせて自動的に設定されます
    - これも、任意の`$hoge`を渡して取得することはできないです

## [useSearchParams](https://remix.run/docs/en/main/hooks/use-search-params)

`useSearchParams`は現在のURLから`searchParams`(=URLの`?`の後ろの部分)を取得・操作するためのHookです。
検索キーワードを画面に表示したり、検索条件フォームの設定値を復元するのが主な用途でしょうか。

```tsx:useSearchParams.tsx
import { json, type LoaderFunctionArgs } from '@remix-run/node';
import {
  useSearchParams,
  useBlocker,
  useLocation,
  useLoaderData,
} from '@remix-run/react';
import { useState } from 'react';

export async function loader(args: LoaderFunctionArgs) {
  const searchParams = new URL(args.request.url).searchParams;
  return json({ searchParams: Array.from(searchParams.entries()) });
}

export default function UseSearchParams() {
  const [searchParams, setSearchParams] = useSearchParams();
  // setSearchParamsはナビゲートを誘発する
  // このナビゲートはRemixのルーティングを通る
  // ということは、useBlockerが効く
  const [isBlocked, setIsBlocked] = useState(false);
  const blocker = useBlocker(() => isBlocked);

  // stateを取得する
  const location = useLocation();

  // loaderのsearchParams
  const { searchParams: searchParamsFromLoader } =
    useLoaderData<typeof loader>();

  return (
    <>
      <h2>useSearchParams</h2>
      <div className="card">
        {blocker.state === 'blocked' ? (
          <p>ブロック！</p>
        ) : (
          <p>ブロックされていません</p>
        )}
        <input
          id="blocker"
          type="checkbox"
          onChange={() => setIsBlocked(!isBlocked)}
        />
        <label htmlFor="blocker">useBlockerを有効にする</label>
      </div>
      <button
        className="button"
        onClick={() => {
          // setSearchParamsでナビゲート
          // navigateOptsで、履歴の置き換えやstateの引き渡しもできる
          setSearchParams(
            (prev) => [...prev, [`key${prev.size}`, `value${prev.size}`]],
            { state: `size = ${searchParams.size}` }
          );
        }}
      >
        SearchParamsを追加する
      </button>

      <div className="card">
        <h3>searchParams</h3>
        <ul>
          {Array.from(searchParams.entries()).map((param) => (
            <li>
              {param[0]}:{param[1]}
            </li>
          ))}
        </ul>
      </div>
      <div className="card">
        <h3>searchParamsFromLoader</h3>
        <ul>
          {searchParamsFromLoader.map((param) => (
            <li>
              {param[0]}:{param[1]}
            </li>
          ))}
        </ul>
      </div>
      <div className="card">
        <h3>state</h3>
        {location.state}
      </div>
    </>
  );
}
```

### ポイント
- 取得できる`searchParams`はWeb標準の[`URLSearchParams`](https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams)です
- 取得できる更新用の関数`setSearchParams`は、値のセットと同時にナビゲートが走ります
    - これはRemixのルーティングを介したナビゲートなので、`useBlocker`や`state`の受け渡しなど、`<Link>`コンポーネントや`useNavigate`でナビゲートするのと同様の動きをします
    - このとき、`loader`も走ります

## [useSubmit](https://remix.run/docs/en/main/hooks/use-submit)
`useSubmit`は`<Form>`を送信するための関数を返すHookです。
ユーザーが`<button type="submit" />`を押して送信する代わりに、プログラム側でフォームを送信することができます。

```tsx:useSubmit.tsx
import { json, type ActionFunctionArgs } from '@remix-run/node';
import { Form, useSubmit, useActionData } from '@remix-run/react';

export async function action(args: ActionFunctionArgs) {
  // formDataを返す
  const formData = await args.request.formData();

  return json({
    text: formData.get('text')?.toString(),
    submittedOn: formData.get('submittedOn')?.toString(),
  });
}

export default function UseSubmit() {
  const data = useActionData<typeof action>();
  const submit = useSubmit();

  return (
    <>
      <h2>useSubmit</h2>
      {data && (
        <div className="card">
          <ul>
            {data.text && <li>text: {data.text}</li>}
            {data.submittedOn && <li>submittedOn: {data.submittedOn}</li>}
          </ul>
        </div>
      )}
      <Form
        method="post"
        className="card"
        onChange={(e) => {
          // 入力のたびにFormDataを加工してSubmitする
          const formData = new FormData(e.currentTarget);
          formData.append('submittedOn', new Date().toISOString());
          submit(formData, { method: 'post' });
        }}
      >
        <label htmlFor="text">テキスト</label>
        <input type="text" id="text" name="text" />
      </Form>
    </>
  );
}
```

### ポイント
- サンプルのように`onChange`で送信するとか、`onSubmit`で事前にデータを加工してから送信するなどのケースで有用です
- 拙アプリの[YOMINA](https://yomina.app)でも、`Submit`時点のローカルタイムスタンプをサーバーに送ったり、ブラウザ側で`pushManager`側の処理が成功してからコールバックで送信したりするために使いました

## [unstable_useViewTransitionState](https://remix.run/docs/en/main/hooks/use-view-transition-state)

これは`unstable`なHookですが、`unstable_usePrompt`と違って、ブラウザ側のViewTransitionの実装が進めば`unstable`が外れそうな雰囲気です。

このHookは現在[`ViewTransition`](https://developer.mozilla.org/ja/docs/Web/API/View_Transitions_API)中かどうかを取得するHookのようですが、ViewTransitionに対する私の解像度が低すぎるので割愛します。

# 終わりに
例によって、サンプルが動作するStackblitzを置いておきます。

https://stackblitz.com/edit/remix-run-remix-ytrjxj?file=app%2Froutes%2FuseSubmit.tsx,app%2Froutes%2F_index.tsx

全3回にわたってRemix v2のHooksを見てきました。

一通り見てきて、なんとなく雰囲気でRemixを使ってしまっていたなぁというのを痛感しているところです。
特に私は`useHref`などの相対パス関連のことを全然わかってなかったんですが、改めて自分のコードを見直すと、これを活用すれば変に複雑な実装しなくてよさそうだったなぁ、という箇所もいくつか見つかりました。

検索もAIも使える今の時代、フレームワークの`Get Started`はそう難しくないので、つい使えている気になってしまいますが、使いこなす、活かし切るのは決して簡単ではないですね。流れのはやいWebの世界において、栄枯盛衰がある中で、一つのフレームワークに習熟するということはある種賭けみたいなものですが、RemixはWeb標準へのリスペクトがありますし、比較的シンプルなフレームワークだと思うので、深く勉強しても無駄になりにくいかなぁと思っています。

RemixとReact Routerの統合が近づいている予感がするのでちょっと様子を見てからになりますが、ルートモジュールとかコンポーネントもそのうち一つずつ見ていこうかなと思いました。

## 宣伝
Remixで作った読書習慣応援Webアプリ「**YOMINA**」を開発・運営中です。

https://yomina.app

https://qiita.com/kedama-t/items/1e93352ac2f7e3851001