ルートの各種の値を定義するための設定
過去2回にわたって、ルートモジュールの設定項目を解説してきましたが、残りは4つとなりました。今回解説するのは、ルートの各種の値を定義するための設定です。表2にまとめました。
| 名称 | 種別 | 概要 |
|---|---|---|
| headers | 関数 | HTTPヘッダーを定義してCache-Controlなどを制御する |
| links | 関数 | <link>要素を定義してCSSなどを制御する |
| meta | 関数 | <title>要素や<meta>要素を定義する |
| handle | オブジェクト | パンくずリストなどに載せたい自ページの情報を登録する |
基本的に、HTMLの <head> 要素や、HTTPヘッダーに働きかけるような設定を行うためのものです。それぞれの使い方を見ていきましょう。
headers
headers はブラウザへのHTTPレスポンスのヘッダーを定義するための関数です。リスト1のように定義します。
export const headers = ({
// (2)
actionHeaders,
errorHeaders,
loaderHeaders,
parentHeaders,
}) => ({
"Cache-Control": "max-age=300", // (1)
});
headers の戻り値は、(1)のようなHTTPヘッダーのオブジェクトです。何も参考にせずに直接記載しても構いませんし、(2)のように引数で受け取れる各種ヘッダーを参考に組み立てることもできます。引数から受け取れる値は次の通りです。
-
actionHeaders:
actionやclientActionの戻り値に設定されたヘッダー -
errorHeaders:
loaderやactionの実行中に発生したエラーレスポンスに設定されたヘッダー -
loaderHeaders:
loaderやclientLoaderの戻り値に設定されたヘッダー -
parentHeaders:親ルートモジュールの
headers関数の戻り値
Nested Routesを組んでいるときに着目したいのは、 parentHeaders です。これは、親ルートモジュールの headers 関数の戻り値を参照するためのプロパティです。まず、前提条件として、Remix v2では、より深い階層のルートモジュールの設定が優先されます。
例えば、親ルートモジュールの headers 関数の戻り値に(1)と同様に Cache-Control: max-age=300 というヘッダーが設定されていた場合に、子ルートモジュールの headers 関数の戻り値に Cache-Control: s-maxage=3600 というヘッダーを設定するとしましょう。
すると、最終的にブラウザへ渡るレスポンスヘッダーは Cache-Control: s-maxage=3600 のみとなります。max-age はクライアント側のキャッシュ時間、 s-maxage はCDNやプロキシなどのキャッシュ時間で、少し役割が違うので、どちらも尊重して残したいところです。そのような場合には、 parentHeaders を使って、親ルートモジュールの設定を参照しながら子ルートモジュールのヘッダーを組み立てることになります。リスト2のように定義します。
import { json } from "@remix-run/node";
import parseCacheControl from "parse-cache-control";
export const headers = ({
loaderHeaders,
parentHeaders,
}) => {
// (2)
const loaderCache = parseCacheControl(
loaderHeaders.get("Cache-Control")
);
const parentCache = parseCacheControl(
parentHeaders.get("Cache-Control")
);
// (3)
const maxAge = Math.min(
loaderCache["max-age"],
parentCache["max-age"]
);
// (4)
const sMaxAge = loaderCache["s-maxage"]
return {
// (5)
"Cache-Control": `s-maxage=${sMaxAge}, max-age=${maxAge}`,
};
};
export const loader = () => {
return json({
message: "Hello, I'm loader!",
}, {
headers: {
// (1)
"Cache-Control": "max-age=500, s-maxage=3600",
},
});
};
リスト2である /headers/child の loader で、(1)のように Cache-Control: max-age=500, s-maxage=3600 というヘッダーを設定しています。もし headers を使わない場合には、このヘッダーがそのままブラウザに渡されることになりますが、今回は親ルートモジュールの headers と組み合わせて、より適切なヘッダーを設定することを考えてみましょう。
まずは(2)のように、 loaderHeaders と parentHeaders から Cache-Control ヘッダーを取得します。ヘッダーの情報をパースしてオブジェクトとして扱うために parse-cache-control というライブラリを使用しましたが、他のものでも構いません。
次に、(3)のように、 loader と parent の両方に設定されている max-age の値を取り出し、より小さい方を採用します。
そして、(4)のように、 s-maxage は loader に設定されている値をそのまま採用します。
最後に、(5)のように、 Cache-Control ヘッダーを組み立てて返すことになります。最終的には図2のように、 Cache-Control: max-age=300, s-maxage=3600 というレスポンスヘッダーが設定されることになります。
このように、 parentHeaders や loaderHeaders の間で折り合いをつけることで、より適切なヘッダーを設定することができます。
links
links は、 <link> 要素を定義するための関数です。リスト3のように定義します。
export const links = () => [
// <link rel="stylesheet" href="/styles.css"> を定義する
{ rel: "stylesheet", href: "/styles.css" },
// <link rel="icon" href="/favicon.ico"> を定義する
{ rel: "icon", href: "/favicon.ico" },
];
このルートで使用したいCSSファイルやfaviconを定義するために使います。戻り値は、 <link> 要素の属性を定義したオブジェクトの配列です。この配列の要素がそのまま <link> 要素として出力されます。href のパスを / から始めているので、 public フォルダ直下のファイルを参照する形になりますね。
href にはファイルの絶対パスを書くこともできますが、相対パスでファイルを参照することもできます。たとえば、図2のように、 app/styles フォルダにCSSファイルを、 app/images フォルダにアイコンを配置した場合のことを考えてみましょう。
このとき、リスト4のように styles.css や favicon.ico を指定できます。
// CSSファイルやfaviconをインポートする
import styles from '../styles/styles.css';
import favicon from '../images/favicon.ico';
export const links = () => [
// hrefでCSSファイルやfaviconを指定する
{ rel: "stylesheet", href: styles },
{ rel: "icon", href: favicon },
];
このとき、CSSファイルやアイコンは、ビルド時に適切なフォルダ(/build/_assets など)に移動され、そのパスが <link> 要素に埋め込まれます。実際に開発者ツールで確認してみると、図3のように、CSSファイルやアイコンが正しく読み込まれていることが確認できます。
このように、links を使うことで、 <link> 要素を柔軟に構築できます。詳細は公式ドキュメントを参照してください。
meta
meta は、 <title> 要素や <meta> 要素を定義するための関数です。リスト5のように定義します。
export const meta = ({
// (3)
location,
matches,
data,
params,
error,
}) => [
// <title>要素を定義する
{ title: "Remix meta function!" }, // (1)
// <meta name="description" content="Remix is ..." /> を定義する
{ name: "description", content: "Remix is ..." }, // (2)
];
このルートで使用したい <title> 要素や <meta> 要素を定義するために使います。<title> 要素だけは(1)のように特別なデータ構造を持っていますが、それ以外は基本的に(2)のように <meta> 要素のための name 属性と content 属性を定義したオブジェクトを記述することになります。リスト5の設定で実際に画面を表示すると、図4のようになります。
<title> 要素や <meta> 要素が構築されていますね。また、(3)のように <meta> 要素を動的に構築するための引数を受け取ることもできます。受け取れる値は次の通りです。
- location:現在のルートのLocationを表すデータ
- matches:親ルートモジュールの情報などが格納されている
-
data:
loaderの戻り値 - params:URLパスのパラメーター
-
error:
ErrorBoundaryでキャッチしたエラー
location や params を使えばURLに付与された各種パラメータを利用できますし、 data を使えば loader で取得したデータを利用できるので、ページの内容に合わせた <meta> 要素を動的に構築することができます。また、Nested Routesを組んである場合は matches を使用することで親ルートモジュールの情報を参照することもできます。詳細は公式ドキュメントを参照してください。
handle
最後に、 handle について解説します。これは、少し特殊な項目で、Nested Routesの各階層の情報を useMatches フックで集約するためのものです。リスト6のように定義します。
export const handle = {
breadcrumb: () =>
<Link to="/handle/child/grandchild">Handle Grand Child</Link>,
};
このような情報をNested Routesの各階層に設置しておくと、 useMatches フックの戻り値として、そのとき表示している(=マッチしている)ルートモジュールの、すべてのhandleを取得することができます。この仕組みを活用すると、公式ドキュメントのBreadcrumbs Guideのように、パンくずリストを容易に作成することができます。
簡単な例を示してみましょう。日本の都道府県を、地方区分ごとに表示するページを作成するとします。図5〜7のように、地方区分のリンクからドリルダウンしていく形で、各都道府県のページに遷移するイメージです。
これらを実現するために、次の5つのルートファイルを作成しました。ファイル名とURLパスの関係については、第5回を参照してください。
- app/routes/handle.jsx:パンくずリストを表示するためのレイアウトを定義する
- app/routes/handle._index.jsx:トップページで地方区分一覧を表示する
- app/routes/handle.$region.jsx:地方区分ひとつ分のページのためのレイアウトを定義する
- app/routes/handle.$region._index.jsx:地方区分ひとつ分の都道府県一覧を表示する
- app/routes/handle.$region.$pref.jsx:都道府県ひとつ分のページ
これらのうち、リスト6のような形式の handle を定義してあるファイルは次の3つです。
- app/routes/handle.jsx
- app/routes/handle.$region.jsx
- app/routes/handle.$region.$pref.jsx
それぞれ、どんな定義をしているか見てみましょう。まずは、 handle.jsx です(リスト7)。
export const handle = {
breadcrumb: () => <Link to="/handle">トップページ</Link>,
};
このファイルは、自分自身(のインデックスページ)であるパンくずリストのトップページへのリンクを定義しています。次に、 handle.$region.jsx です(リスト8)。
export const loader = async ({ params }) => {
// $region で渡された地方区分のキーを取得
const regionKey = params.region;
// 地方区分の詳細データを取得
const region = japan.find((item) => item.key === regionKey);
if (!region) {
return json({
status: 404,
message: 'Not Found',
}, {
status: 404,
});
}
return json({
region,
});
};
export const handle = {
// (1) loaderの戻り値を参照してパンくずリストを表示する
breadcrumb: ({ data }) =>
<Link to={`/handle/${data.region.key}`}>{data.region.name}</Link>,
};
こちらも、 /handle/tohoku のような形でアクセスされたときに、その地方区分のページへのリンクを生成するために handle を定義しています。
注目すべきは(1)で引数に data を受け取っていることです。これは、 loader の戻り値を参照するためのもので、 loader で取得したデータを使ってパンくずリストを生成することができます。これによって、URLのパスからはキーだけを受け取りつつ、パンくずリストにはデータの名前を表示する、といった処理が可能です。これはフレームワークのデフォルトの挙動ではなく、上位の階層でパンくずリストを構成するときの工夫によって実現されています。リスト10で解説するので、今は「こういうこともできる」ということだけ覚えておいてください。
最後に、 handle.$region.$pref.jsx です(リスト9)。
export const loader = async ({ params }) => {
const regionKey = params.region;
const prefKey = params.pref;
const region = japan.find((item) => item.key === regionKey);
const pref = region?.prefs.find((item) => item.key === prefKey);
if (!region || !pref) {
return json({
status: 404,
message: 'Not Found',
}, {
status: 404,
});
}
return json({
region,
pref,
});
};
export const handle = {
breadcrumb: ({ data }) =>
<Link to={`/handle/${data.region.key}/${data.pref.key}`}>
{data.pref.name}
</Link>,
};
リスト8とほぼ同等の内容になっていますね。
これらのデータを取りまとめて、パンくずリストを作成しているのが、最上位の handle.jsx です。パンくずリストを作成している部分をリスト10で見てみましょう。
import { Link, Outlet, useMatches } from "@remix-run/react";
// (省略)
export default function HandleLayout() {
// (1) マッチしたルートモジュールの情報を取得
const matches = useMatches();
// (2) パンくずリスト用のデータがあるルート情報のみを抽出
const breadcrumbMatches = matches.filter((match) =>
match.handle && match.handle.breadcrumb);
return (
<div>
<h1>日本の都道府県</h1>
<nav className="breadcrumb">
<ul>
{/* (3) */}
{breadcrumbMatches.map((match, index) => (
<li key={index}>
{match.handle.breadcrumb(match)}{/* (4) */}
</li>
))}
</ul>
</nav>
<hr />
<Outlet />
</div>
)
}
まず、(1)で useMatches フックを使って、現在表示中のURLパスにマッチしたルート(route)モジュールの情報を配列を取得しています。ここに含まれる情報については公式ドキュメントを参照してください。
(2)では、パンくずリスト用のデータがあるルート情報のみを抽出しています。これは、 breadcrumb プロパティを含む handle オブジェクトがルートモジュールに定義されている情報、つまりパンくずリストの表示対象だけを取り出していることになります。
(3)で実際にパンくずリストを構築します。(4)の match.handle.breadcrumb() 関数を実行することで、各ルートモジュールの handle に定義した <Link> 要素が展開されることになり、画面に各階層のリンクが表示されるのです。
ここで注目してほしいのが、 breadcrumb(match) のように、引数に match を渡しているところです。この match には、各ルートモジュールで loader 関数が実行された結果が格納されています。ここで引数にデータを渡していることによって、リスト8のように loader で取得したデータを使ってパンくずリストを構築することができるわけです。
Nested Routesを活かした、強力な仕組みだとは思うのですが、まだ公式側もパンくずリストくらいしか活用方法を例示できていないので、今後の展開に期待したいところです。
まとめ
3回にわたって、ルートモジュールの設定項目を解説してきました。フレームワークのAPIとしてはすっきりした形になりつつも、古き良きブラウザとサーバーの関係を踏襲して、HTTPやHTMLの設定項目を柔軟に扱えるようになっているRemixの特徴が出ていたと思います。これらの設定項目を活用して、より高度なWebページを構築していきましょう。
次回は、実際のインフラにデプロイしながら、Remixで作ったアプリケーションを公開する方法について解説します。
