2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Posted at

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

前回(useLoaderData~useOutletContext編)

前々回(useActionData~useHref編)

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

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

useParams

useParamsは動的に変化するパラメータの部分をURLから取得するためのHookです。

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 />
    </>
  );
}
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で、URLのパスのうち、特定の箇所が動的に変化するものです
      • 例: /articles/1/post/articles/2/postみたいな、記事idが変わるパターン
        • これはルートモジュールarticles.$id.post.tsxにマッチします
      • 例: /user/kedama-t/log/1のように、userlogが固定で、ユーザーidとログidが変動するパターン
        • これはルートモジュールuser.$userId.log.$logId.tsxにマッチします
    • もう一つは、Splat Routesで、URLのうち、特定の箇所以降が動的に変化するものです
      • 例: /files/images/avatars/kedama-t.jpg
        • この例では、imagesavetarskedama-t.jpgの3か所が、動的に変化する可能性がありますが、ルートモジュールfiles.$.tsxを作っておくと、imagesavetarskedama-t.jpgの部分がいかに変化しても、また、階層がもう一つ増えて/files/images/avatars/lg/kedama-t.jpgのようになったとしても、全部files.$.tsxにマッチします
    • こうしてマッチしたときの動的に変化する部分の値を取得するのがuseParamsです
  • Dynamic Segmentsの場合は、ファイル名に付けた${セグメント名}のセグメント名で取れます
  • Splat Routesの場合は、['*']$の部分に相当するURLの文字列が得られます

unstable_usePrompt

これは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.

拙訳

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

useResovledPath

useResolvedPathは、与えられたパスを現在のルートに対して解決し、Pathオブジェクトを返すhookです。

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

useRevalidatorはRemixにおける通常のデータ変更(loaderComponentaction)以外の方法でデータを再検証するためのHookです。
で、Remixにおける「再検証(revalidate)」ってなんすかって話なんですが、要はloaderの再実行です。
つまり、useRevalidatorは任意のタイミングでloaderを再実行するHookです。

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.stateloadingになり、そのルートまでのloaderが実行されます
  • loaderの処理が終わるとrevalidator.stateidleになります
    • このタイミングで、useDataLoaderで取得できるデータが再実行時のものに変わります
    • すると、コンポーネントが再レンダリングされて、コンポーネントには最新のデータが表示されます
  • 現在のルートのloaderだけではなく、祖先のルートモジュールにあるloaderも実行されます
    • ルートモジュールからshouldRevalidate関数をexportしておくと、loaderの再実行を制御できます

useRouteError

useRouteErrorは、ルートモジュールのloader関数やaction関数で発生したエラーの情報を取得するためのHookです。
ルートモジュール関数のErrorBoundaryと組み合わせて使います

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

ポイント

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

useRouteLoaderData

useRouteLoaderDataは、自分の親~祖先のルートモジュールのloader関数の戻り値を取得することができるHookです。
親ルートの情報を子ルートに渡すには、<Outlet context={*} />を使う方法がありますが、あんまり深いといわゆるバケツリレー、ドリリングといわれるような事態に陥ります。
useRouteLoaderDataを使うと、子ルート側から親ルートのloaderに直接アクセスすることができます。

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>
    </>
  );
}
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>
    </>
  );
}
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(前々回記事参照)の出番でしょうか
  • Dynamic Segmentsを含むidを指定した場合、$hogeのところは今自分がいるルートに合わせて自動的に設定されます
    • これも、任意の$hogeを渡して取得することはできないです

useSearchParams

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

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です
  • 取得できる更新用の関数setSearchParamsは、値のセットと同時にナビゲートが走ります
    • これはRemixのルーティングを介したナビゲートなので、useBlockerstateの受け渡しなど、<Link>コンポーネントやuseNavigateでナビゲートするのと同様の動きをします
    • このとき、loaderも走ります

useSubmit

useSubmit<Form>を送信するための関数を返すHookです。
ユーザーが<button type="submit" />を押して送信する代わりに、プログラム側でフォームを送信することができます。

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でも、Submit時点のローカルタイムスタンプをサーバーに送ったり、ブラウザ側でpushManager側の処理が成功してからコールバックで送信したりするために使いました

unstable_useViewTransitionState

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

このHookは現在ViewTransition中かどうかを取得するHookのようですが、ViewTransitionに対する私の解像度が低すぎるので割愛します。

終わりに

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

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

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

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

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

宣伝

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

2
2
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
2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?