Remixでデモアプリを作成(パンくずリスト編)

Remixフレームワークを使ったパンクズリストの作り方をご紹介しました。

Remixでパンくずリストの実装方法を徹底解説
パンくずリスト(breadcrumb)とは、Webサイトの階層構造を表すリストのことです。 設置することで、ユーザーがWebサイトのどの位置にいるのかを把握するのに役立ちます。 パンくずナビゲーション – CSS: カスケーディングスタイルシート | MDN  今回は、Remixでパンくずリストを作る方法について解説し...

 

今回は、実際にパンクズリストを実装したアプリを開発してみたいと思います。

ページの追加

今回作成するアプリの完成形は以下の通りです。

ページの構成としては親ページがあり、その下に子供一覧ページがあり、さらにその下に子供の詳細ページがあるという作りにしています。

当記事はパンくずリストの実現方法を学ぶことが目的なので、必要なページの追加などはパッと終わらせてしまいましょう。

まずは任意のディレクトリで、remixのプロジェクトを立ち上げます。

npx create-remix@latest breadclumnb-test
? What type of app do you want to create? Just the basics
? Where do you want to deploy? Choose Remix App Server if you're unsure; it's easy to change deployment targets. Fly.io
? TypeScript or JavaScript? TypeScript
? Do you want me to run `npm install`? Yes

執筆時点のバージョン情報は以下の通りです

  • node.js: 16.18.1
  • remix-run/css-bundle: 1.19.3
  • remix-run/node: 1.19.3
  • remix-run/react: 1.19.3
  • remix-run/: 1.19.3

続けて、必要なページを追加していきます。

touch app/routes/parent.tsx
touch app/routes/parent._index.tsx
touch app/routes/parent.children.tsx
touch app/routes/parent.children._index.tsx
touch app/routes/parent.children.\\$childId.tsx

app/routes/parent.tsx

import { Link, Outlet, useMatches } from "@remix-run/react";

export default function Parent() {
  return (
    <div>
      <h1>パンくずリストテスト</h1>

      <header>
        ここにパンくずリストを表示します
      </header>

      <Outlet />
    </div>
  )
}

app/routes/parent._index.tsx

import { Link } from "@remix-run/react";

export default function ParentIndex() {
  return (
    <div>
      <h2>親ページ</h2>

      <Link to="children">子供一覧ページ</Link>
    </div>
  )
}

app/routes/parent.children.tsx

import { Link, Outlet } from "@remix-run/react";

export default function Children() {

  return (
    <Outlet />
  )
}

app/routes/parent.children._index.tsx

import { Link } from "@remix-run/react";

export default function ChildrenIndex() {
  return (
    <div>
      <h2>子供の一覧ページ</h2>

      <ul>
        <li>
          <Link to="1">子供1</Link>
        </li>
        <li>
          <Link to="2">子供2</Link>
        </li>
        <li>
          <Link to="3">子供3</Link>
        </li>
      </ul>
    </div>
  )
}

app/routes/parent.children.$childId.tsx

import { Link, useParams } from "@remix-run/react";

export default function ChildDetail() {
  const { childId } = useParams()

  return (
    <div>
      <h2>子供の詳細ページ</h2>

      <p>私は子供{childId}です</p>
    </div>
  )
}

また、トップページを以下のように編集します。

app/routes/_index.tsx

import type { V2_MetaFunction } from "@remix-run/node";
import { Link } from "@remix-run/react";

export const meta: V2_MetaFunction = () => {
  return [
    { title: "Breadcrumb" },
    { name: "description", content: "Welcome!" },
  ];
};

export default function Index() {
  return (
    <div style={{ fontFamily: "system-ui, sans-serif", lineHeight: "1.8" }}>
      <Link to="/parent">パンくずリストの親ページ</Link>
    </div>
  );
}

これでベースは完成です。

定数handleの実装

まずは、routesにあるファイルに対して、定数handleを実装してエクスポートします。

handleの主な役割は、 useMatches で取得することのできる情報を追加することです。

handle

handleは以下のようにオブジェクトの形で定義します。

export const handle = {
  its: "all yours",
};

今回はパンくずリストを作るため、自身へのリンクを持つ Linkコンポーネント を返す関数を breadcrumbという名前で追加します。

app/routes/parent.tsx

// ...

export const handle = {
  breadcrumb: () => <Link to="/parent">親ページ</Link>
}

app/routes/parent.children.tsx

// ...

export const handle = {
  breadcrumb: () => <Link to="/parent/children">子供一覧ページ</Link>
}

app/routes/parent.children.$childId.tsx

// ...

export const handle = {
  breadcrumb: (match: RouteMatch) => {
    return (
      <Link to={`/parent/children/${match.params.childId}`}>{`子供${match.params.childId}`}</Link>
    )
  }
}

子供の詳細ページ( app/routes/parent.children.$childId.tsx )については動的セグメントを使用しているため、引数として match を受け取り、そこに含まれる params から childId を取り出してリンクに含めています。

match というのは useMatches を使えば取得できる情報の塊で、その中には「それぞれのページを開いた時のパラメータ」も含まれているため、そちらを引数として受け取る想定です。

useMatchesの使用

パンくずリストを実装する前に、 useMatches を使用して追加した handle が取得できるかを確かめてみます。

useMatches

useMatches を追加する箇所は、パンくずリストを追加したいページの、一番親のコンポーネントになります。今回の場合はParentコンポーネント( app/routes/parent.tsx )に追加します。

app/routes/parent.tsx

// useMatches を追加
import { Link, Outlet, useMatches } from "@remix-run/react";

// ...

export default function Parent() {
  const matches = useMatches();
  console.log(matches);

  return (

// ...

サーバーを確認すると、以下のような出力が確認できました。2つ目の要素を見ると、想定通りhandleの中に breadbrumb を持つオブジェクトが入っていますね。

[
  {
    id: 'root',
    pathname: '/',
    params: {},
    data: null,
    handle: undefined
  },
  {
    id: 'routes/parent',
    pathname: '/parent',
    params: {},
    data: null,
    handle: { breadcrumb: [Function: breadcrumb] }
  },
  {
    id: 'routes/parent._index',
    pathname: '/parent/',
    params: {},
    data: null,
    handle: undefined
  }
]

一番最下層である子供の詳細ページ( /parent/children/1 )にアクセスした場合は以下のような内容となります。

[
  {
    id: 'root',
    pathname: '/',
    params: { childId: '1' },
    data: null,
    handle: undefined
  },
  {
    id: 'routes/parent',
    pathname: '/parent',
    params: { childId: '1' },
    data: null,
    handle: { breadcrumb: [Function: breadcrumb] }
  },
  {
    id: 'routes/parent.children',
    pathname: '/parent/children',
    params: { childId: '1' },
    data: null,
    handle: { breadcrumb: [Function: breadcrumb] }
  },
  {
    id: 'routes/parent.children.$childId',
    pathname: '/parent/children/1',
    params: { childId: '1' },
    data: null,
    handle: { breadcrumb: [Function: breadcrumb] }
  }
]

handleを追加したものについては追加した内容が入っており、何もしていないものについては undefined となっています。

こちらをもとに、実際の表示を作ってみましょう。

パンくずリスト用コンポーネントの追加

確認した内容をもとに、表示を作っていきます。

注意すべき点は「 handle が定義されていない routes については、値が undedined になっている」という点です。

今回の場合で言えば、 app/routes/parent._index.ts  app/root.tsx(ページを表示する際の最上位のコンポーネント)はパンくずリストに追加したくないため、あえて定義をせず undefined が返るように作っています。

そのため表示するには

  1. useMatches で取得できた情報のうち、 handle  breadcrumb が定義されているもののみを絞り込む
  2. breadcrumb の情報(Linkコンポーネント)をもとにして、パンくずリストを作成する

という手順が必要となります。

以上を踏まえた上で追加したプログラムは以下の通りです。

app/components/breadcrumb.tsx

import { useMatches } from "@remix-run/react"

export const Breadcrumb = () => {
  const matches = useMatches()

  return (
    <ol style={{ listStyle: "none" }}>
      {
        matches
          .filter((match) => match.handle?.breadcrumb)
          .map((match, index) => {
            const separator = index === 0 ? null : ">"
            return (
              <li key={index} style={{ display: "inline-block" }}>
                {separator && <span style={{ padding: "0 10px" }}>{separator}</span>}
                {match.handle.breadcrumb(match)}
              </li>
            )
          })
      }
    </ol>
  )
}

app/routes/parent.tsx

import { Breadcrumb } from "~/components/breadcrumb";

// ...

export default function Parent() {
  // 先ほど試しに追加した useMatches は削除します。

return (
    <div>
      <h1>パンくずリストテスト</h1>

      <header>
				{/* 追加したコンポーネントを表示 */}
        <Breadcrumb />
      </header>

      <Outlet />
    </div>
  )
}

特出すべき点は以下の2つです

  • .filter((match) => match.handle?.breadcrumb) で handleにbreadcrumbが定義されているものに絞りんでいる
  • const separator = index === 0 ? null : ">" で、リストの項目間を区切る文字を定義し、それを表示している

前者については前述の通りです。 useMatches を使うと、handleが定義されていないものまで取得してしまうので、そういったものを取り除いています。

後者はレイアウトを整えるために追加しています。このほかにも、CSSの擬似要素を使うなどの方法もありますが、手軽なのでjsx内に直接記述する形を採用しました。

画面を確認すると、最初に載せた画像のようになっていると思います。これにて完成です!

まとめ

remixを使ってパンくずリストを実装してみましたが、正直なところかなり楽に実現ができて驚いています。

Outlet を使った部分的なルーティングのおかげで表示したい最上位の routes に表示用のコンポーネントを追加するだけで済みますし、何より handle  useMatches の組み合わせによって、各ページへのリンクを取得できるようにする仕組みがとてもわかりやすく、使いやすいです!

ブログに戻る

コメントを残す

コメントは公開前に承認される必要があることにご注意ください。