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
執筆時点のバージョン情報は以下の通りです
続けて、必要なページを追加していきます。
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は以下のようにオブジェクトの形で定義します。
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
を追加する箇所は、パンくずリストを追加したいページの、一番親のコンポーネントになります。今回の場合は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
が返るように作っています。
そのため表示するには
-
useMatches
で取得できた情報のうち、handle
にbreadcrumb
が定義されているもののみを絞り込む -
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
の組み合わせによって、各ページへのリンクを取得できるようにする仕組みがとてもわかりやすく、使いやすいです!