複雑なルートを定義する
前項で1階層分のパスを定義する方法は分かりました。しかし、実際のWebサイト制作では、もう少し複雑なURLの構築を求められます。2つ以上の階層を持つパスを定義したり、表示するデータのIDをパスとして使用したいこともあるでしょう。
Remixはそういったケースにも対応しているので、やり方を見ていきましょう。
パスの階層を定義する
まずは、深い階層のパスを作る方法です。Remixでは、深い階層のパスを定義したい場合、ファイル名をドットで区切ることで表現します。公式ドキュメントではDot Delimitersと呼ばれる表記方法です。直訳すると「ドット区切り」なので、本記事でもドット区切りと表記します。
例えば、/about/me というパスを定義したい場合は、app/routes/about.me.jsx というファイル名にすれば実現できます。また、階層はいくつ区切っても大丈夫なので、app/routes/foo.bar.buz.jsx を定義して /foo/bar/buz というパスを作ってもOKです。
なお、このドット区切りのパス表現でも _index という命名は有効なので、前述した /top20 というパスは app/routes/top20._index.jsx という名前でも表現できます。
パスでパラメータを表現する
次は、URLのパスにパラメータを持つ方法について解説します。/top20/12345 の 12345 の部分のように、データのIDをパスに含めたいことがありますよね。そういった場合は、パス名の先頭にドル記号 $ を付けて、パラメータ名を定義できます。
具体的には、app/routes/top20.$id.jsx のようなファイル名を定義すると、/top20/12345 のパスでアクセスできるようになります。$id の部分は、リスト2のように loader の引数で取得できます。
export const loader = async ({ params }) => {
const { id } = params; // (1)
// データを取得して画面へ送る処理
};
今回はファイル名が $id だったので、(1)で params から取り出したプロパティも id になっていますが、パラメータ名は自由なので、$item_id や $key にしてもOKです。app/routes/user.$user_id.$item_id のように、複数のパラメータを指定することもできます。
また、loader だけではなく action の引数でも同様にパラメータを利用できるので、編集画面での送信処理にもパラメータを活用できます。
レイアウトとしてのルートを定義する
Remixの特徴的な機能として、Nested Routes(ネステッドルーツ)というルーティング方法があります。Webサイトのよくあるページ構成として、ヘッダー部とメニュー部とコンテンツ部が分かれていることがありますよね。ページ遷移したときに大きく切り替わるのはコンテンツ部だけしかない、ということはあります(図3)。
そういったケースのために、コンテンツ部とそれ以外を別々のルートとして定義して、ページ遷移するときはシングルページアプリケーションとして、本当にコンテンツ部だけ切り替えられるようにしよう、というコンセプトがNested Routesです。
Nested Routesが成立するための条件は2つあります。
- ドット区切りによるパスを定義した状態で、上位のパス名と名前が一致するファイル名を定義して、「外側」のコンポーネントを定義する
-
コンポーネント内の、「内側」のコンテンツを表示したい場所に
<Outlet />を配置する
具体例を挙げてみましょう。第3回で作成したHacker News Viewerを再現する形で、次の3つのファイルを使ってNested Routesを構成してみましょう。
-
app/routes/top20.jsx:ヘッダーとメニューが定義された外側のコンポーネント -
app/routes/top20._index.jsx:初期ページ -
app/routes/top20.$id.jsx:個別の記事ページ
まずは、外側のコンポーネントである app/routes/top20.jsx を作成します(リスト3)。
// (略)
export default function Top20Route() {
// loaderで取得済みのデータを取り出す
const data = useLoaderData();
return (
<div>
<header>
<h1>Hacker News Viewer</h1>
</header>
<div id="container">
<div id="sidebar">
<h2>Top 20</h2>
<nav>
<ul>
{data.map((item) => (
<li key={item.id}>
<Link to={/top20/${item.id}}>{item.title}</Link>
</li>
))}
</ul>
</nav>
</div>
<main>
<Outlet />{/ (1) /}
</main>
</div>
</div>
);
}
省略部分についてはサンプルコードを参照してください。
<main> 要素の中がコンテンツ部になるようにスタイルを組んであるので、(1)で <main> 要素の中に <Outlet /> を配置して、コンテンツが表示できるようにしました。
これだけではページとして成立しないので、<Outlet /> に表示できるものも作らないといけません。簡単に app/routes/top20._index.jsx を作成してみましょう(リスト4)。
import stylesUrl from '~/style/article.css';
export const links = () => {
return [{ rel: "stylesheet", href: stylesUrl }];
};
export default function Top20IndexRoute() {
return (
<article>
<p>ここに記事が表示されます。</p>
<p>左のメニューから記事を選択してください。</p>
</article>
)
}
当たり障りなくテキストが書いてあるだけのコンポーネントです。では、このリスト4に該当するページである http://localhost:3000/top20 を開いてみましょう(図4)。
外側にリスト3のヘッダーとメニュー、内側にリスト4のテキストが表示され、両者が <Outlet /> を通じて、上手く組み合わさっています。
それでは、個別の記事ページである app/routes/top20.$id.jsx も作りましょう(リスト5)。
// (略)
export default function Top20IdRoute() {
const { item, kids } = useLoaderData();
return (
<article>
<h1>{item.title}</h1>
<p>by {item.by} on {new Date(item.time * 1000).toLocaleString()}</p>
<p>{item.text}</p>
<p><a href={item.url}>{item.url}</a></p>
<h2>Comments</h2>
{kids.map((kidsItem) => (
<div key={kidsItem.id}>
<h3>by: {kidsItem.by}</h3>
<p>{kidsItem.text}</p>
<p>{new Date(kidsItem.time * 1000).toLocaleString()}</p>
<hr />
</div>
))}
</article>
);
}
こちらも省略部分についてはサンプルコードを参照してください。
タイトルやコメントが書いてあるページです。作成したら、メニューの記事をどれか選んでみてください(図5)。
記事が表示されました。メニューの記事名をクリックしていくと、内側のコンポーネントだけが切り替わっていきます。
外側のコンポーネントとコンテンツを別々に実装することができました。外側のコンポーネントのことは、特別に「レイアウト」と呼ぶこともあり、Nested Routesはレイアウト機能とも呼ばれます。
ちなみに、app/root.jsx はすべてのルートの外側に被さるレイアウトの一種です。
URLをネストさせずにレイアウトだけ使いたい
レイアウトには、もう少し柔軟な使い方もあります。ログイン画面やユーザー登録の画面に、専用のレイアウトを用意したいことってありますよね。ここまでの知識で素直に実装すると、次のようなファイルを用意すればいいように思えます。
-
app/routes/auth.login.jsx:ログイン画面 -
app/routes/auth.register.jsx:ユーザー登録画面 -
app/routes/auth.jsx:上記2画面のレイアウト
もちろんこれは /auth/login のように、/auth/ という階層を含むURLでアクセスすることになります。ですが、URLの設計として /login や /register というルートパスの直下に認証系のパスを置きたいこともありますよね。
app/routes/ フォルダの直下に login.jsx と register.jsx を配置して、ルートレイアウトである app/root.jsx を編集する手もないわけではありませんが、認証と関係の無いルートにまで影響が出かねないのは、いいことではなさそうです。
そんなときは、パス名の先頭にアンダースコアをつけてみましょう。そのパスはレイアウトだけで利用され、URLのパスとしては利用されなくなります。つまり、今回の例で言うと、ファイル名を次のようにすると上手くいきます。
-
app/routes/_auth.login.jsx:ログイン画面 -
app/routes/_auth.register.jsx:ユーザー登録画面 -
app/routes/_auth.jsx:上記2画面のレイアウト
これで /login や /register だけに _auth.jsx で定義したレイアウトが適用されるようになります。
まとめ
今回はルーティングについて解説しました。公式ドキュメントには、他にもルーティングの小技が紹介されているので、困った時には目を通してみると良いかもしれません。
次回は、ルートに設定できる内容について詳細に解説します。
