Remix v2のHooks全部使う(useLoaderData～useOutletContext編)

# 序

前回の記事の続きです。経緯や前提などは割愛します。

https://qiita.com/kedama-t/items/f648183771585422a31d

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

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

なお、前回記事からこの記事までの間に、[Remix v2.11.0](https://github.com/remix-run/remix/blob/main/CHANGELOG.md#v2110)がリリースされていますが、Hookそのものというよりは、一部のHookについて`unstable`な機能と組み合わせた時の挙動が影響を受けている、という感じなので、とりあえずこのまま続けていきます。

## [useLoaderData](https://remix.run/docs/en/main/hooks/use-loader-data)
`useLoaderData`は[`loader`関数](https://remix.run/docs/en/main/route/loader)の結果をコンポーネントで取得するためのHookです。
`useActionData`([前回記事](https://qiita.com/kedama-t/items/f648183771585422a31d)参照)と並んで最頻出のHookといえます。

```tsx:useLoaderData
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](https://remix.run/docs/en/main/guides/single-fetch)が有効な場合は`loader`関数から`json()`関数を使わずに`return`することで、データ型を維持したまま返すことができます

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

`useLocation`は現在表示しているロケーションの情報を取得するためのHookです

```tsx:useLocation
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>`](https://remix.run/docs/en/main/components/scroll-restoration)コンポーネントは同じキーの場所は同じスクロール位置にリストアする、という動作がデフォルトです
- [`<Link>`](https://remix.run/docs/en/main/components/link)コンポーネントの`state`プロパティにオブジェクトを渡すと、ページ移動先で取得することができます
    - どこから来たかで動作を変えたり、前ページの情報を表示したりしたい場合に使えます
- [WebAPIのLocation](https://developer.mozilla.org/ja/docs/Web/API/Location)と違って、こいつは`origin`を持っていません
    - originが欲しい場合は、`loader`の`args.request.url`から取ってくる必要があります

## [useMatches](https://remix.run/docs/en/main/hooks/use-matches)
`useMatches`は、現在のルートがマッチしているルートのリストを返します。
[パンくずリスト](https://remix.run/docs/en/main/guides/breadcrumbs)の実装など、ルート(**Root**)～現在のロケーションまでのパスの情報が欲しいときに使います。

```tsx:useMatches.tsx
// 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 />
    </>
  );
}
```

```tsx:useMatches._index.tsx
// useMatches/indexの表示
import { Link } from '@remix-run/react';

export default function UseMatchesIndex() {
  return (
    <>
      <div className="card">
        <Link to="child">childに移動</Link>
      </div>
    </>
  );
}
```

```tsx:useMatches.$child.tsx
// useMatches/$childのレイアウト
import { Outlet } from '@remix-run/react';

export default function UseMatchesChild() {
  return (
    <>
      <Outlet />
    </>
  );
}
```
```tsx:useMatches.$child._index.tsx
// useMatches/$childのindex
import { Link } from '@remix-run/react';

export default function UseMatchesChildIndex() {
  return (
    <>
      <div className="card">
        <Link to="grandchild">grandchildに移動</Link>
      </div>
    </>
  );
}
```

```tsx:useMatched.$child.$grandchild.tsx
// useMatches/$child/$grandchildのindex
export default function UseMatchesGrandChild() {
  return <></>;
}
```

### ポイント
- サンプルコードには、2つの[Dynamic Segments](https://remix.run/docs/en/main/file-conventions/routes#dynamic-segments)が入ってます(`$child`、`$grandchild`)
    - これらは、`match.params`から実際の値を取得できます
- 各ルートモジュールから[`handle`関数](https://remix.run/docs/en/main/route/handle)をエクスポートしておくと、`match.handle`にそれが入ってきます

## [useNavigate](https://remix.run/docs/en/main/hooks/use-navigate)
`useNavigate`はページ遷移のための関数を返すHookです。

```tsx:useNavigate.tsx
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`も送れます
- [公式ドキュメント](https://remix.run/docs/en/main/hooks/use-navigate)曰く`It's often better to use redirect in actions and loaders than this hook, but it still has use cases.`
:::note info
拙訳
> このHookを使うよりも、`action`や`loader`関数内で`redirect`を使うほうがよいですが、まだ使いどころはあります
:::
- 結局、`<Form>`の`submit`なんかはサーバーに処理が行っちゃうので`redirect`すればいいし、クリックで遷移する通常のページ移動なら`<Link>`でよいので、公式ドキュメントの通り、正直あんまり使いどころがない印象です

## [useNavigation](https://remix.run/docs/en/main/hooks/use-navigation)
`useNavigation`は、ページ移動の状況を取得できるHookです。
`useNavigate`と名前が似てますが、役割は全然違います。
割とユーザー体験を左右する、重要なHookだと思います。

```tsx:useNavigation.tsx
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](https://remix.run/docs/en/main/hooks/use-navigation-type)
`useNavigationType`は、ユーザーが前のページからどんな方法で来たのかがわかります。
- navigation(navigate)と名の付くのフックがいろいろある中、全然役割が違うのが面白いですね

```tsx:useNavigationType
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](https://remix.run/docs/en/main/hooks/use-outlet)
`useOutlet`は、子ルートのコンポーネントを取得するためのHookです。
`<Outlet>`コンポーネントはこのHookを使って子コンポーネントを取得しています。

```tsx:useOutlet.tsx
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>
    </>
  );
}
```

```tsx:useOutlet.child.tsx
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](https://remix.run/docs/en/main/hooks/use-outlet-context)
`useOutletContext`は親ルートが`<Outlet context={*}>`の形で渡してきたデータを子ルートから取得するためのHookです。
親が`loader`で取得したデータを（ときに加工して）子に渡す、というシチュエーションは結構あるので、最頻出というほどではないですが、常に道具箱には入れておきたいHook、という印象[^1]

[^1]:[落葉樹 - MTG Wiki](http://mtgwiki.com/wiki/%E8%90%BD%E8%91%89%E6%A8%B9)

```tsx:useOutletContext.tsx
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} />
    </>
  );
}
```

```tsx:useOutletContext.child.tsx
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を置いておきます。

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


`useParams`以降のHookはまた別途書きます。

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

https://yomina.app

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