ファイルベースルーティング
ファイルを置くだけでルートが生まれる。ページ・レイアウト・動的パラメータ・404/error の規約。
Vantage のルーティングは ファイルの配置がそのまま URL になります。ルーターの設定は書きません。
ページを置く場所
置き場所は 2 通りあり、src/ があるかどうかだけで決まります。設定ではなく検出です。
| プロジェクト | ページ走査のルート | / になるファイル |
|---|---|---|
src/ がある |
src/ の中だけ |
src/index.tsx |
src/ が無い |
プロジェクトルート直下 | index.tsx |
src/ 自体は URL に現れません(src/sales/index.tsx → /sales)。1 ファイルのアプリはそのまま
直下に置け、育ったら src/ を作ってまとめて移せます。
ページの規約
以下の「ファイル」列は、上のページ走査ルートからの相対パスです。
| ファイル | ルート |
|---|---|
index.tsx |
/ |
monthly-analysis.tsx |
/monthly-analysis |
sales/index.tsx |
/sales |
sales/[customerId].tsx |
/sales/:customerId |
sales/[...path].tsx |
/sales/*(catchall) |
_layout.tsx |
そのディレクトリ配下のレイアウト(入れ子可) |
_404.tsx |
not-found の UI(ルートのみ) |
_error.tsx |
error の UI(ルートのみ) |
[name]→ 動的パラメータ、[...name]→ catchall。indexは自身のディレクトリにマップ(名前は URL から落ちる)。_で始まるファイルは予約(_layout/_404/_error)またはプライベート(ルーティングされない)。
ルーティングされないディレクトリ
次のディレクトリは(どの階層でも)決して走査されません。ここには自由にコードを置けます。
components/ 再利用する React コンポーネント
hooks/ カスタムフック
lib/ ユーティリティ
server/ API(存在するとサーバーが有効になる)
public/ 静的アセット
動的パラメータの 3 つの綴り
同じパラメータが、場所によって 3 つの綴りで現れます。必ず一致させます。
| 場所 | 綴り | 例 |
|---|---|---|
| ファイル名 | [id] |
sales/[customerId].tsx |
| 表示ルート | :id |
/sales/:customerId |
| リンク | $id + params |
to="/sales/$customerId" params={{ customerId }} |
import { Link, useParams } from "@squadbase/vantage/router";
function Row({ customerId }: { customerId: string }) {
return (
<Link to="/sales/$customerId" params={{ customerId }}>
顧客を開く
</Link>
);
}
// sales/[customerId].tsx
export default function Customer() {
const { customerId } = useParams({ strict: false });
return <div>顧客 {customerId}</div>;
}
レイアウトと入れ子
_layout.tsx は <Outlet /> を描画して子ルートを囲みます。ディレクトリごとに置けば入れ子になります。
// _layout.tsx
import { Outlet } from "@squadbase/vantage/router";
export default function RootLayout() {
return (
<div className="min-h-screen">
<nav className="border-b p-4">My Dashboard</nav>
<Outlet />
</div>
);
}
_404.tsx と _error.tsx は ルート(プロジェクト直下)でのみ認識されます。
ルート一覧からナビを作る
useRoutes() はアプリの全ページルートを返します。ファイルシステムが正本なので、
ページファイルを 1 つ足せばナビに 1 行増えます。手で管理するリンク配列は不要です。
// _layout.tsx
import { Link, Outlet, useCurrentRoute, useRoutes } from "@squadbase/vantage/router";
export default function RootLayout() {
// 動的ルート(/sales/:customerId)は URL が 1 つに定まらないので外す
const routes = useRoutes().filter((route) => !route.dynamic);
const current = useCurrentRoute();
return (
<div>
<nav>
{routes.map((route) => (
<Link key={route.path} to={route.to} aria-current={route.path === current?.path}>
{route.label}
</Link>
))}
</nav>
<Outlet />
</div>
);
}
並び順は Vantage のスキャン順(浅い順 → 静的が動的より先 → アルファベット順)で、 そのままナビに使える順序です。
各要素は RouteInfo です。
| フィールド | 型 | 内容 |
|---|---|---|
path |
string |
表示パス。/sales/:customerId |
to |
string |
Link の to に渡す形。/sales/$customerId |
params |
string[] |
動的パラメータ名。catch-all は _splat |
dynamic |
boolean |
動的パラメータを持つか |
index |
boolean |
ディレクトリの index ルートか |
label |
string |
navLabel → title → path の順で決まる表示名 |
title / description / navLabel |
string | undefined |
definePage の値 |
useCurrentRoute() は今表示中のルートを返します(404 のときは undefined)。
上の例のような「現在地」判定のほか、パンくずやページ見出しに使えます。
URL にフィルタ状態を置く
ダッシュボードの絞り込みは URL に置くと、リロードで消えず、そのまま同僚に共有できます。
useSearchParam は useState と同じ形で search params を読み書きします。
import { useSearchParam } from "@squadbase/vantage/router";
import { SegmentedControl } from "@squadbase/vantage/components";
export default function Sales() {
const [region, setRegion] = useSearchParam("region", "all");
return <SegmentedControl options={REGIONS} value={region} onChange={setRegion} />;
}
- 値は常に文字列です(
?year=2024は数値としてパースされますが、この hook が文字列に戻します)。 - デフォルト値(上の例では
"all")やnullを書き込むと、キーは URL から取り除かれます。 - 履歴は既定で
replace(戻るボタンが埋まらない)。{ replace: false }で push に変えられます。
配列やオブジェクトなど JSON になる値は useSearchState を使います。関数更新も渡せます。
import { useSearchState } from "@squadbase/vantage/router";
const [segments, setSegments] = useSearchState<string[]>("segments", []);
setSegments((prev) => [...prev, "enterprise"]);
HMR とルート再生成
.tsx ページや server/api ファイルを追加・削除すると、開発サーバーがルートを再生成して
フルリロードします。ページ本体を編集した場合は React Fast Refresh がその場で反映します。
次に読む
- ページと definePage — ページの契約とメタデータ