コンテンツにスキップ
Vantage
English
Esc
navigateopen⌘Jpreview
このページの内容

File-based routing

Dropping a file creates a route. Conventions for pages, layouts, dynamic params, and 404/error.

In Vantage, where you place a file is its URL. You never configure a router.

Where pages live

There are two layouts, and which one applies is decided by whether src/ exists — detected, never configured.

Project Page-scan root The file that becomes /
Has src/ Inside src/ only src/index.tsx
No src/ The project root index.tsx

src/ itself never appears in a URL (src/sales/index.tsx/sales). A one-file app can stay at the root; once it grows, create src/ and move the pages in.

Page conventions

The “File” column below is relative to the page-scan root above.

File Route
index.tsx /
monthly-analysis.tsx /monthly-analysis
sales/index.tsx /sales
sales/[customerId].tsx /sales/:customerId
sales/[...path].tsx /sales/* (catchall)
_layout.tsx Layout for that directory (nestable)
_404.tsx Not-found UI (root only)
_error.tsx Error UI (root only)
  • [name] → dynamic param, [...name] → catchall.
  • index maps to its own directory (the name drops from the URL).
  • Files starting with _ are reserved (_layout/_404/_error) or private (not routed).

Directories that are never routed

These directories are never scanned, at any depth. Put whatever code you like here.

components/   Reusable React components
hooks/        Custom hooks
lib/          Utilities
server/       API (its presence enables the server)
public/       Static assets

The three spellings of a dynamic param

The same param appears in three spellings depending on where it is. They must match.

Where Spelling Example
Filename [id] sales/[customerId].tsx
Display route :id /sales/:customerId
Link $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 }}>
      Open customer
    </Link>
  );
}

// sales/[customerId].tsx
export default function Customer() {
  const { customerId } = useParams({ strict: false });
  return <div>Customer {customerId}</div>;
}

Layouts and nesting

_layout.tsx renders an <Outlet /> that wraps child routes. Place one per directory to nest them.

// _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 and _error.tsx are recognized at the root only.

Build a nav from the route list

useRoutes() returns every page route in the app. The file system is the source of truth, so adding one page file adds one nav entry — there is no link array to maintain.

// _layout.tsx
import { Link, Outlet, useCurrentRoute, useRoutes } from "@squadbase/vantage/router";

export default function RootLayout() {
  // Dynamic routes (/sales/:customerId) have no single URL, so leave them out.
  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>
  );
}

The order is Vantage’s scan order — shallow before deep, static before dynamic, alphabetical — which is the order a nav wants.

Each entry is a RouteInfo:

Field Type Contents
path string Display path, /sales/:customerId
to string The form Link’s to expects, /sales/$customerId
params string[] Dynamic parameter names; a catch-all is _splat
dynamic boolean Whether the route has dynamic parameters
index boolean Whether it is a directory’s index route
label string Display name: navLabel, else title, else path
title / description / navLabel string | undefined Values from definePage

useCurrentRoute() returns the route being rendered, or undefined on the 404 route. Beyond the “you are here” check above, it fits breadcrumbs and page headings.

Keep filter state in the URL

A dashboard’s filters belong in the URL: the view survives a reload and can be pasted to a colleague. useSearchParam reads and writes one search-param key with a useState shape.

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} />;
}
  • The value is always a string (?year=2024 parses as a number; this hook normalizes it back).
  • Writing the default value ("all" above) or null removes the key from the URL.
  • History defaults to replace, so filtering doesn’t fill the back button. Pass { replace: false } to push instead.

For anything JSON-serializable — an array, an object — use useSearchState, which also accepts a functional update.

import { useSearchState } from "@squadbase/vantage/router";

const [segments, setSegments] = useSearchState<string[]>("segments", []);
setSegments((prev) => [...prev, "enterprise"]);

HMR and route regeneration

Adding or removing a .tsx page or a server/api file regenerates routes and triggers a full reload. Editing a page body applies in place via React Fast Refresh.

このページは役に立ちましたか?