こちらの記事は「MEDLEY Summer Tech Blog Relay」の11日目の記事です。
はじめに
こんにちは。フロントエンドエンジニアのavaiceです。
業務では医療機関向けのシステムを開発していますが、趣味で個人開発にも取り組んでいます。
個人開発ではよくVite+React+TypeScriptの構成で開発を行うのですが、そこで欠かせないのがルーティングライブラリです。
普段はミニマムで使いやすいwouterを愛用していますが、ここ数年でWeb APIやReact周りにルーティング関連の新機能がいくつか追加されたので、標準APIを活用したルーティングライブラリを作ってみました。
この記事では、ライブラリ開発を通して得た近年のクライアントサイドルーティングの知見をまとめます。(完成したルーターは最後に紹介します!)
新入りのWeb APIたちを使う
Navigation API
SPAのクライアントサイドルーティングを扱いやすくするNavigation APIが、Baselineになりました。
これまでのルーティングライブラリでは、<a>要素のクリックイベントを監視してpreventDefault()してから、History APIのpushState()を呼ぶ方法がよく使われていました。
しかし、ページ遷移では修飾キーを押したクリック、外部リンク、ハッシュリンク、ダウンロード、フォーム送信など、さまざまなケースを考慮する必要があります。
Navigation APIでは、これらのナビゲーションをnavigateイベントで受け取れるようになりました。
Chromium系では2022年から利用できましたが、2025年末から2026年初頭にFirefoxとSafariも対応し、2026年1月にBaseline 2026となりました。
実際の使用例は以下のようになります。
const routes = {
"/": "<h1>Home</h1>",
"/about": "<h1>About</h1>",
}
if ("navigation" in window) {
navigation.addEventListener("navigate", (event) => {
if (
!event.canIntercept ||
event.hashChange ||
event.downloadRequest !== null ||
event.formData !== null
) {
return
}
const url = new URL(event.destination.url)
const content = routes[url.pathname]
// 未登録のURLは通常のページ遷移に任せる
if (!content) return
event.intercept({
handler() {
document.querySelector("#app").innerHTML = content
},
})
})
}
また、少しうれしい要素として、handler()内で非同期処理を行いPromiseを返すと、その完了までがナビゲーション処理中として扱われます!
(これによるうれしい例として、Chromeではこの間タブにネイティブのローディング表示が出ます)
URLPattern API
URLPattern APIは、path-to-regexpを基にした構文で、URLまたはその一部をパターンと照合するAPIです。
URLPattern APIは2025年9月にBaselineとなりました。
ルーター用途では、主にpathnameの照合に利用できます。
const pattern = new URLPattern({
pathname: "/users/:id",
})
pattern.test({ pathname: "/users/123" }) // true
pattern.test({ pathname: "/posts/123" }) // false
const result = pattern.exec(
"https://example.com/users/123",
)
if (result) {
console.log(result.pathname.groups)
// { id: "123" }
}
test()はパターンに一致するかを真偽値で返し、exec()は一致した結果とパラメーターを返します。
これまではライブラリや自前の正規表現で行う必要があったパスパターンの照合を、標準APIで扱えるようになりました!
React 19のDocument Metadataを使う
React 19では、<title>、<meta>、<link>などのメタデータをコンポーネント内に記述できるようになりました。Reactがこれらの要素を検出し、ドキュメントの<head>内へ自動的に配置します。
単純なメタデータの設定であれば、useEffectやreact-helmetを使わず、以下のようにページコンポーネント内で宣言できます。詳細はReact 19のDocument Metadataに記載されています。
export const About = () => {
return (
<main>
<title>About</title>
<meta
name="description"
content="このサイトについて"
/>
<h1>About</h1>
</main>
)
}
開発したルーター
- GitHub: avaice/neouter
- デモ: neouter.pages.dev
ここまで紹介したNavigation APIとURLPattern APIを使用して開発したneouterを簡単に紹介します。
npm install neouter
ルートは、パスとコンポーネントの対応をオブジェクトで定義します。
import { useCreateRoutes } from "neouter"
const routes = {
"/": {
component: Home,
},
"/about": {
component: About,
},
}
export const App = () => {
const { Router, RouterProvider } =
useCreateRoutes({ routes })
return (
<RouterProvider>
<Navigation />
<Router />
</RouterProvider>
)
}
ページ間の移動にはLinkを使います。
import { Link } from "neouter"
export const Navigation = () => {
return (
<nav>
<Link href="/">Home</Link>
<Link href="/about">About</Link>
</nav>
)
}
Linkの実体は通常の<a>要素です。Navigation APIでナビゲーションを一括して処理するため、Link自身はクリックイベントを持ちません。neouterのLinkは、通常の<a>要素にhrefの型補完を加えるための薄いラッパーです。(今回の記事とは内容が逸れるので紹介していませんが、neouterではhrefの型補完を行っています)
ページ遷移はNavigation APIのnavigateイベントで検知し、アプリ内で処理できる遷移だけをintercept()します。ルートの照合とパラメーターの取得にはURLPattern APIを使用しています。
ページタイトルについてもneouter独自のAPIを用意せず、React 19のDocument Metadataを利用して、<title>をページコンポーネント内で宣言する形にしました。
おわりに
Navigation APIによってナビゲーション処理を一か所にまとめられるようになり、URLPattern APIによってパスの照合をブラウザに任せられるようになりました。ルーターを自作したことで、これまでライブラリが担っていた処理の一部がWeb標準で実現できるようになりつつあることがわかりました。
どちらもBaselineになったばかりなので、古いブラウザを対象にする場合はフォールバックが必要ですが、現行ブラウザを対象にすれば、以前より少ない実装でシンプルなルーティング処理を作れます。
ページ遷移をブロックしてHTMLを置き換えるという多少ハック的な行為を行うSPAという概念に対して、ブラウザ側が歩み寄っているのは面白いなと感じました。
今後も個人開発で使いながら、必要な機能を追加していく予定です。
MEDLEY Summer Tech Blog Relay 12日目の記事は高橋さんです!お楽しみに!!