7
3

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全部使う(useLoaderData~useOutletContext編)

Last updated at Posted at 2024-08-04

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

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

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

なお、前回記事からこの記事までの間に、Remix v2.11.0がリリースされていますが、Hookそのものというよりは、一部のHookについてunstableな機能と組み合わせた時の挙動が影響を受けている、という感じなので、とりあえずこのまま続けていきます。

useLoaderData

useLoaderDataloader関数の結果をコンポーネントで取得するためのHookです。
useActionData(前回記事参照)と並んで最頻出のHookといえます。

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が定義されていれば必ず動くので、useLoaderDatauseActionDataと違ってundefinedが返ってくることはないです
    • ただ、Form.method="get"<Form>がある場合は、そのsubmitloaderをキックしますので、一つのloaderに初回取得処理とフォーム送信時の処理両方を書く必要があります
  • ドキュメントにはReturns the serialized data from the "closest" route loader.(ダブルクォートは筆者)と書いてあるのですが、じゃあ子ルートにloaderがないときに親ルートのloaderの結果が取得できるかというと、それはできないです。なんでだよ
    • おそらくは、ルートモジュールのコンポーネントじゃないところで呼んだときに、そいつが使われてるルートモジュールのloaderからデータを読むよ、という意味だと思います
      • 型の問題もあるので、個人的にはよほどのことがない限りはルートモジュール以外では使いたくないなぁというお気持ち
  • useActionDataと同じく戻り値はシリアライズされていますが、Single Fetchが有効な場合はloader関数からjson()関数を使わずにreturnすることで、データ型を維持したまま返すことができます

useLocation

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

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>コンポーネントは同じキーの場所は同じスクロール位置にリストアする、という動作がデフォルトです
  • <Link>コンポーネントのstateプロパティにオブジェクトを渡すと、ページ移動先で取得することができます
    • どこから来たかで動作を変えたり、前ページの情報を表示したりしたい場合に使えます
  • WebAPIのLocationと違って、こいつはoriginを持っていません
    • originが欲しい場合は、loaderargs.request.urlから取ってくる必要があります

useMatches

useMatchesは、現在のルートがマッチしているルートのリストを返します。
パンくずリストの実装など、ルート(Root)~現在のロケーションまでのパスの情報が欲しいときに使います。

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 />
    </>
  );
}
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>
    </>
  );
}
useMatches.$child.tsx
// useMatches/$childのレイアウト
import { Outlet } from '@remix-run/react';

export default function UseMatchesChild() {
  return (
    <>
      <Outlet />
    </>
  );
}
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>
    </>
  );
}
useMatched.$child.$grandchild.tsx
// useMatches/$child/$grandchildのindex
export default function UseMatchesGrandChild() {
  return <></>;
}

ポイント

  • サンプルコードには、2つのDynamic Segmentsが入ってます($child$grandchild)
    • これらは、match.paramsから実際の値を取得できます
  • 各ルートモジュールからhandle関数をエクスポートしておくと、match.handleにそれが入ってきます

useNavigate

useNavigateはページ遷移のための関数を返すHookです。

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も送れます
  • 公式ドキュメント曰くIt's often better to use redirect in actions and loaders than this hook, but it still has use cases.

拙訳

このHookを使うよりも、actionloader関数内でredirectを使うほうがよいですが、まだ使いどころはあります

  • 結局、<Form>submitなんかはサーバーに処理が行っちゃうのでredirectすればいいし、クリックで遷移する通常のページ移動なら<Link>でよいので、公式ドキュメントの通り、正直あんまり使いどころがない印象です

useNavigation

useNavigationは、ページ移動の状況を取得できるHookです。
useNavigateと名前が似てますが、役割は全然違います。
割とユーザー体験を左右する、重要なHookだと思います。

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.stateuseFetcherのそれと同じで、idlesubmittingloadingの3種類です
    • loaderを待ってるときはloadingactionを待っているときはsubmitting、それ以外の状況ではidleになります
    • ロード中のスケルトン表示なんかに使うとよいです

useNavigationType

useNavigationTypeは、ユーザーが前のページからどんな方法で来たのかがわかります。

  • navigation(navigate)と名の付くのフックがいろいろある中、全然役割が違うのが面白いですね
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

useOutletは、子ルートのコンポーネントを取得するためのHookです。
<Outlet>コンポーネントはこのHookを使って子コンポーネントを取得しています。

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

useOutletContextは親ルートが<Outlet context={*}>の形で渡してきたデータを子ルートから取得するためのHookです。
親がloaderで取得したデータを(ときに加工して)子に渡す、というシチュエーションは結構あるので、最頻出というほどではないですが、常に道具箱には入れておきたいHook、という印象1

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} />
    </>
  );
}
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を置いておきます。

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

宣伝

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

  1. 落葉樹 - MTG Wiki

7
3
1

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?