序
Remix v2のHooks全部使うシリーズの最終回です。
前々回の記事に経緯や前提などを書いているので、この記事では割愛します。
前回(useLoaderData~useOutletContext編)
前々回(useActionData~useHref編)
この記事では全26のHooksのうち、アルファベット順にuseParams
~useSubmit
を取り上げます
引き続き、内容の誤りや勘違い、モアベターな実装方法などありましたら、編集リクエスト、コメントで教えていただけると嬉しいです。
useParams
useParams
は動的に変化するパラメータの部分をURLから取得するためのHookです。
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 />
</>
);
}
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
のように、user
とlog
が固定で、ユーザーidとログidが変動するパターン- これはルートモジュール
user.$userId.log.$logId.tsx
にマッチします
- これはルートモジュール
- 例:
- もう一つは、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で、URLのパスのうち、特定の箇所が動的に変化するものです
- 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 usinguseBlocker
instead which also gives you control over the confirmation UX.
拙訳
プロンプトが開いているときの振る舞いはブラウザによって非決定的であり、React Routerはすべてのシナリオにおいて正しい振る舞いを保証できないため、このHookの
unstable_
プレフィクスを外す予定はありません。この非決定性を避けるために、同様に確認のUXを制御できるuseBlocker
の使用をお勧めします。
useResovledPath
useResolvedPath
は、与えられたパスを現在のルートに対して解決し、Path
オブジェクトを返すhookです。
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における通常のデータ変更(loader
⇒Component
⇒action
)以外の方法でデータを再検証するためのHookです。
で、Remixにおける「再検証(revalidate)」ってなんすかって話なんですが、要はloader
の再実行です。
つまり、useRevalidator
は任意のタイミングでloader
を再実行するHookです。
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
関数をexport
しておくと、loader
の再実行を制御できます
- ルートモジュールから
useRouteError
useRouteError
は、ルートモジュールのloader
関数やaction
関数で発生したエラーの情報を取得するためのHookです。
ルートモジュール関数のErrorBoundary
と組み合わせて使います
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
useRouteLoaderData
は、自分の親~祖先のルートモジュールのloader
関数の戻り値を取得することができるHookです。
親ルートの情報を子ルートに渡すには、<Outlet context={*} />
を使う方法がありますが、あんまり深いといわゆるバケツリレー、ドリリングといわれるような事態に陥ります。
useRouteLoaderData
を使うと、子ルート側から親ルートのloader
に直接アクセスすることができます。
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>
</>
);
}
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>
</>
);
}
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です。
検索キーワードを画面に表示したり、検索条件フォームの設定値を復元するのが主な用途でしょうか。
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のルーティングを介したナビゲートなので、
useBlocker
やstate
の受け渡しなど、<Link>
コンポーネントやuseNavigate
でナビゲートするのと同様の動きをします - このとき、
loader
も走ります
- これはRemixのルーティングを介したナビゲートなので、
useSubmit
useSubmit
は<Form>
を送信するための関数を返すHookです。
ユーザーが<button type="submit" />
を押して送信する代わりに、プログラム側でフォームを送信することができます。
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」を開発・運営中です。