序
前回の記事の続きです。経緯や前提などは割愛します。
この記事では全26のHooksのうち、アルファベット順にuseLoaderData
~useOutletContext
を取り上げます
引き続き、内容の誤りや勘違い、モアベターな実装方法などありましたら、編集リクエスト、コメントで教えていただけると嬉しいです。
なお、前回記事からこの記事までの間に、Remix v2.11.0がリリースされていますが、Hookそのものというよりは、一部のHookについてunstable
な機能と組み合わせた時の挙動が影響を受けている、という感じなので、とりあえずこのまま続けていきます。
useLoaderData
useLoaderData
はloader
関数の結果をコンポーネントで取得するためのHookです。
useActionData
(前回記事参照)と並んで最頻出のHookといえます。
import { json, type LoaderFunctionArgs } from '@remix-run/node';
import { Form, useLoaderData } from '@remix-run/react';
// サンプルデータ
const USERS = [
{ id: '1', displayId: 'kedama-t', name: '毛玉T' },
{ id: '2', displayId: 'matake-d', name: '真竹D' },
{ id: '3', displayId: 'datema-k', name: 'だて巻き' },
];
export async function loader({ request }: LoaderFunctionArgs) {
const keyword = new URL(request.url).searchParams.get('keyword');
if (!keyword) {
return json({ users: [] });
}
// キーワードで検索
// 実際にはDBや外部APIからデータをとってくる
const searchResult = USERS.filter(
(user) => user.name.includes(keyword) || user.displayId.includes(keyword)
);
return json({ users: searchResult });
}
export default function UseLoaderData() {
// loaderの戻り値を取得
// 型引数に"typeof loader"を渡すと、dataがloader関数の戻り値に沿った型になる
const data = useLoaderData<typeof loader>();
return (
<>
<h2>useLoaderData</h2>
{/* Form.methodを"get"にすると、loaderが動く */}
<Form className="card" method="get">
<label>ユーザーを検索</label>
<input type="text" name="keyword" />
<button className="button" type="submit">
送信
</button>
</Form>
{/* loaderからの戻り値を表示する */}
{data.users.length > 0 && (
<div className="card">
{data.users.length}人見つかりました
{data.users.map((user) => (
<p>
{user.name}(@{user.displayId})
</p>
))}
</div>
)}
</>
);
}
ポイント
- ルートへアクセスしたときに
loader
が定義されていれば必ず動くので、useLoaderData
はuseActionData
と違ってundefined
が返ってくることはないです- ただ、
Form.method="get"
な<Form>
がある場合は、そのsubmit
がloader
をキックしますので、一つのloader
に初回取得処理とフォーム送信時の処理両方を書く必要があります
- ただ、
- ドキュメントには
Returns the serialized data from the "closest" route loader.
(ダブルクォートは筆者)と書いてあるのですが、じゃあ子ルートにloader
がないときに親ルートのloader
の結果が取得できるかというと、それはできないです。なんでだよ- おそらくは、ルートモジュールのコンポーネントじゃないところで呼んだときに、そいつが使われてるルートモジュールの
loader
からデータを読むよ、という意味だと思います- 型の問題もあるので、個人的にはよほどのことがない限りはルートモジュール以外では使いたくないなぁというお気持ち
- おそらくは、ルートモジュールのコンポーネントじゃないところで呼んだときに、そいつが使われてるルートモジュールの
-
useActionData
と同じく戻り値はシリアライズされていますが、Single Fetchが有効な場合はloader
関数からjson()
関数を使わずにreturn
することで、データ型を維持したまま返すことができます
useLocation
useLocation
は現在表示しているロケーションの情報を取得するためのHookです
import { json, type LoaderFunctionArgs } from '@remix-run/node';
import { Link, useLocation, useLoaderData } from '@remix-run/react';
export async function loader({ request }: LoaderFunctionArgs) {
return json({ origin: new URL(request.url).origin });
}
export default function UseLocation() {
const location = useLocation();
// useLocationからはoriginが取れない
// ほしい場合はloaderからもらう必要がある
const data = useLoaderData<typeof loader>();
return (
<>
<h2>useLocation</h2>
<div className="card">
<ul>
<li>{data.origin}</li>
<li>{location.key}</li>
<li>{location.pathname}</li>
<li>{location.state?.message}</li>
<li>{location.search}</li>
<li>{location.hash}</li>
</ul>
</div>
<div className="card">
<Link
to="../useLocation?search=hoge#hash"
state={{ message: 'メッセージ' }}
>
./?search=hoge#hashに移動
</Link>
</div>
</>
);
}
ポイント
-
key
はそのロケーションのユニークキーです- ここでは深入りしませんが、
<ScrollRestoration>
コンポーネントは同じキーの場所は同じスクロール位置にリストアする、という動作がデフォルトです
- ここでは深入りしませんが、
-
<Link>
コンポーネントのstate
プロパティにオブジェクトを渡すと、ページ移動先で取得することができます- どこから来たかで動作を変えたり、前ページの情報を表示したりしたい場合に使えます
-
WebAPIのLocationと違って、こいつは
origin
を持っていません- originが欲しい場合は、
loader
のargs.request.url
から取ってくる必要があります
- originが欲しい場合は、
useMatches
useMatches
は、現在のルートがマッチしているルートのリストを返します。
パンくずリストの実装など、ルート(Root)~現在のロケーションまでのパスの情報が欲しいときに使います。
// useMatches/のレイアウト
import { Link, Outlet, useMatches } from '@remix-run/react';
export default function UseMatches() {
const matches = useMatches();
return (
<>
<h2>useMatches</h2>
{matches.map((match) => (
<div className="card">
<ul>
<li>id: {match.id}</li>
<li>$child: {match.params['child']}</li>
<li>$grandchild: {match.params['grandchild']}</li>
<li>pathname: {match.pathname}</li>
</ul>
</div>
))}
<Outlet />
</>
);
}
// useMatches/indexの表示
import { Link } from '@remix-run/react';
export default function UseMatchesIndex() {
return (
<>
<div className="card">
<Link to="child">childに移動</Link>
</div>
</>
);
}
// useMatches/$childのレイアウト
import { Outlet } from '@remix-run/react';
export default function UseMatchesChild() {
return (
<>
<Outlet />
</>
);
}
// useMatches/$childのindex
import { Link } from '@remix-run/react';
export default function UseMatchesChildIndex() {
return (
<>
<div className="card">
<Link to="grandchild">grandchildに移動</Link>
</div>
</>
);
}
// useMatches/$child/$grandchildのindex
export default function UseMatchesGrandChild() {
return <></>;
}
ポイント
- サンプルコードには、2つのDynamic Segmentsが入ってます(
$child
、$grandchild
)- これらは、
match.params
から実際の値を取得できます
- これらは、
- 各ルートモジュールから
handle
関数をエクスポートしておくと、match.handle
にそれが入ってきます
useNavigate
useNavigate
はページ遷移のための関数を返すHookです。
import { useNavigate, useBlocker } from '@remix-run/react';
import { useState } from 'react';
export default function UseNavigate() {
const navigate = useNavigate();
// navigateはRemixのルーティングを通して移動するので、
// useBlockerが効く
const [isBlocked, setIsBlocked] = useState(false);
const blocker = useBlocker(() => isBlocked);
return (
<>
<h2>useNavigate</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={() => navigate(-1)}>
戻る
</button>
<button className="button" onClick={() => navigate('../useMatches/a/b')}>
/useMatches/a/b
</button>
</>
);
}
ポイント
- つくりとしては、
<Link>
コンポーネントの関数版という感じです- 引数は
To
型ですし、state
も送れます
- 引数は
-
公式ドキュメント曰く
It's often better to use redirect in actions and loaders than this hook, but it still has use cases.
拙訳
このHookを使うよりも、
action
やloader
関数内でredirect
を使うほうがよいですが、まだ使いどころはあります
- 結局、
<Form>
のsubmit
なんかはサーバーに処理が行っちゃうのでredirect
すればいいし、クリックで遷移する通常のページ移動なら<Link>
でよいので、公式ドキュメントの通り、正直あんまり使いどころがない印象です
useNavigation
useNavigation
は、ページ移動の状況を取得できるHookです。
useNavigate
と名前が似てますが、役割は全然違います。
割とユーザー体験を左右する、重要なHookだと思います。
import { json, type LoaderFunctionArgs } from '@remix-run/node';
import { Form, useNavigation } from '@remix-run/react';
export async function loader() {
await new Promise((res) => setTimeout(res, 500));
return null;
}
export async function action() {
await new Promise((res) => setTimeout(res, 2000));
return null;
}
export default function UseNavigation() {
const navigation = useNavigation();
// locationも取れる
const location = navigation.location;
return (
<>
<h2>useNavigation</h2>
<Form className="card" method="post">
<button className="button" name="submit" value="value" type="submit">
送信
</button>
</Form>
<div className="card">
<ul>
<li>{navigation.state}</li>
<li>{navigation.formAction}</li>
<li>{location?.pathname}</li>
</ul>
</div>
</>
);
}
ポイント
-
navigation.state
はuseFetcher
のそれと同じで、idle
、submitting
、loading
の3種類です-
loader
を待ってるときはloading
、action
を待っているときはsubmitting
、それ以外の状況ではidle
になります - ロード中のスケルトン表示なんかに使うとよいです
-
useNavigationType
useNavigationType
は、ユーザーが前のページからどんな方法で来たのかがわかります。
- navigation(navigate)と名の付くのフックがいろいろある中、全然役割が違うのが面白いですね
import { useNavigationType } from '@remix-run/react';
export default function UseNavigationType() {
const navigationType = useNavigationType();
return (
<>
<h2>useNavigationType</h2>
<div className="card">{navigationType}</div>
</>
);
}
ポイント
- 通常の
<Link>
などでのナビゲーションでアクセスするとPUSH
、ブラウザの戻る/進むボタンで来るとPOP
、<Link replace>
などブラウザ履歴を書き換えるようなナビゲーションで来た場合、REPLACE
になります - アニメーションのあるページで、通常の移動で来た時は動かすけど、戻るボタンで来たときに動かすとくどい、みたいなときに使えそうです
useOutlet
useOutlet
は、子ルートのコンポーネントを取得するためのHookです。
<Outlet>
コンポーネントはこのHookを使って子コンポーネントを取得しています。
import { Link, useOutlet } from '@remix-run/react';
export default function UseOutlet() {
// 子ルートのコンポーネントがあるときはそれだけ返す
const outlet = useOutlet();
if (outlet) {
return outlet;
}
return (
<>
<h2>useOutlet</h2>
<div className="card">
<Link to="child">childに移動</Link>
</div>
</>
);
}
import { Link } from '@remix-run/react';
export default function UseOutletChild() {
return (
<>
<h2>useOutlet-child</h2>
<div className="card">
<Link to="../">親ルートに移動</Link>
</div>
</>
);
}
ポイント
-
<Outlet>
コンポーネントの場合、基本的には親ルートのどこに置くかを決める形になりますが、useOutlet
であれば、子ルートの有無に応じて返すコンポーネントを変えることができるようになります - 使い方によっては、
*._index.tsx
ルートを作らなくて済みます-
{ outlet ?? <></> }
のような感じ - ただ、
_index.tsx
が明示的にあった方がよい気もするので、これは良し悪しかなという気はしますが
-
useOutletContext
useOutletContext
は親ルートが<Outlet context={*}>
の形で渡してきたデータを子ルートから取得するためのHookです。
親がloader
で取得したデータを(ときに加工して)子に渡す、というシチュエーションは結構あるので、最頻出というほどではないですが、常に道具箱には入れておきたいHook、という印象1
import { json } from '@remix-run/node';
import { Link, Outlet, useLoaderData } from '@remix-run/react';
// サンプルデータ
const USERS = [
{ id: '1', displayId: 'kedama-t', name: '毛玉T' },
{ id: '2', displayId: 'matake-d', name: '真竹D' },
{ id: '3', displayId: 'datema-k', name: 'だて巻き' },
];
export async function loader() {
return json({ users: USERS });
}
export type ContextType = typeof USERS;
export default function UseOutletContext() {
const data = useLoaderData<typeof loader>();
return (
<>
<h2>useOutletContext</h2>
<div className="card">
<Link to="child">childに移動</Link>
</div>
{/* 子ルートにLoaderからのデータを渡す */}
<Outlet context={data.users} />
</>
);
}
import { useOutletContext } from '@remix-run/react';
import type { ContextType } from './useOutletContext';
export default function UseOutletContextChild() {
// 親ルートからコンテキストを取得
const data = useOutletContext<ContextType>();
return (
<>
{data.map((user) => (
<div className="card">
<p>
{user.name}(@{user.displayId})
</p>
</div>
))}
</>
);
}
ポイント
- データ型は親ルートが責任をもって子ルートに渡してあげるべきかなと思っています
- 子ルート側でアドホックに定義してしまうと、即地獄です
- 印象の話で恐縮ですが、Remixは割とそういうところがあります
- フレームワーク任せではなく、TypeScriptのセマンティックの範囲で、開発者がちゃんと型をつける、Remixはそういう世界観なんだと思っています
- 子ルート側でアドホックに定義してしまうと、即地獄です
終わりに
前回もそうでしたが、書いていて「アッ、こんな書き方できたんだ!」という瞬間がたくさんあります。
やっぱり自分で動くところを見ながらドキュメントを読むと、すごく勉強になりますね。
例によって、サンプルが動作するStackblitzを置いておきます。
useParams
以降のHookはまた別途書きます。
宣伝
Remixで作った読書習慣応援Webアプリ「YOMINA」を開発・運営中です。