@w3cj/ruta
v1.1.2
Published
A tiny type-safe client-side router for hono/jsx
Maintainers
Readme
ruta
A tiny type-safe client-side router for hono/jsx client components.
Zero dependencies, only 2.51 KB with brotli compression.
Inspired by wouter and @tanstack/react-router
Install
pnpm install @w3cj/rutaQuick Start
Define routes with defineRoutes and register them via module augmentation. All route-aware APIs (Link, navigate, useRoute, useParams, etc.) get typed automatically.
// src/routes.ts
import { defineRoutes, useParams } from "@w3cj/ruta";
import { Home, Post, User } from "./pages";
export const routes = defineRoutes(route => [
route("/", Home),
route("/users/:id", User),
route("/posts/:year/:slug", Post),
]);Register route types globally at the bottom of the same file:
declare module "@w3cj/ruta" {
// eslint-disable-next-line ts/consistent-type-definitions
interface Register {
routes: typeof routes;
}
}When
Registeris not augmented, all APIs accept plain strings andparamsare optional.
<Router routes={routes} />
Pass the route definition to Router
import { Router } from "@w3cj/ruta";
import { routes } from "./routes";
const App = () => <Router routes={routes} />;Page components used in the example
// src/pages.ts
export const Home = () => <h1>Home</h1>;
export const User = () => {
const { id } = useParams<"/users/:id">();
return (
<h1>
User
{id}
</h1>
);
};
export const Post = () => {
const { year, slug } = useParams<"/posts/:year/:slug">();
return (
<h1>
Post
{year}
{" - "}
{slug}
</h1>
);
};<Link> with typed params
The to prop gets autocomplete for registered routes. When a route has parameters, the params prop is required.
import { Link } from "@w3cj/ruta";
<Link to="/about" />;<Link to="/users/:id" params={{ id: "42" }} />;navigate with typed params
import { navigate } from "@w3cj/ruta";
navigate("/about");
navigate("/users/:id", { params: { id: "42" } });
navigate("/users/:id", { params: { id: "42" }, replace: true });<Redirect> with typed params
import { Redirect } from "@w3cj/ruta";
<Redirect to="/about" />;<Redirect to="/users/:id" params={{ id: "42" }} />;useParams
Pass a route pattern as a generic to get typed params.
import { useParams } from "@w3cj/ruta";
const { id } = useParams<"/users/:id">(); // { id: string }useRoute
Returns a typed match result.
import { useRoute } from "@w3cj/ruta";
const { matched, params } = useRoute("/posts/:year/:slug");
// params: { year: string; slug: string }<Route> with typed callbacks
TypeScript infers param types from the path prop when using a render function.
import { Route } from "@w3cj/ruta";
<Route path="/users/:id">
{params => (
<h1>
User
{params.id}
</h1>
)}
</Route>;History Types
Ruta supports three history types. Browser history is the default — pass a different history to Router to switch modes.
Browser (default)
Uses the browser History API. No configuration needed.
<Router>
<App />
</Router>;Hash
Uses hash-based URLs (/#/about). Useful when your server doesn't support rewrites to index.html.
import { createHashHistory } from "@w3cj/ruta";
<Router history={createHashHistory()}>
<App />
</Router>;Memory
In-memory routing for testing or non-browser environments.
import { createMemoryHistory } from "@w3cj/ruta";
const mem = createMemoryHistory({ path: "/initial", record: true });<Router history={mem}>
<App />
</Router>;mem.navigate("/next");
console.log(mem.entries); // ["/initial", "/next"]
mem.reset!();Components
<Route>
Renders when the path matches.
<Route path="/about" component={About} />;<Route path="/about">
<h1>About</h1>
</Route>;<Route path="/users/:id" component={User} />;// Nested routing
<Route path="/app" nest>
<Route path="/dashboard" component={Dashboard} />
<Route path="/settings" component={Settings} />
</Route>;<Switch>
Renders the first matching route.
<Switch>
<Route path="/" component={Home} />
<Route path="/about" component={About} />
<Route path="*" component={NotFound} />
</Switch>;<Link>
Navigates without a full page reload.
<Link href="/about">About</Link>;<Link href="/login" replace>Log in</Link>;<Link href="/about" className={isActive => isActive ? "active" : ""}>
About
</Link>;<Link href="/about" asChild>
<button>About</button>
</Link>;<Redirect>
Navigates immediately on mount.
<Redirect href="/login" />;<Redirect href="/home" replace />;<Router>
Provides routing context. Optional — browser history is used by default.
<Router>
<App />
</Router>;<Router history={createHashHistory()}>
<App />
</Router>;Base Path
Prepend all routes with a base path.
<Router base="/dashboard">
<App />
</Router>;Hooks
useLocation
Returns the current path and a navigate function.
const { location, navigate } = useLocation();
navigate("/about");
navigate("/login", { replace: true });useRoute
Matches a pattern against the current path.
const { matched, params } = useRoute("/users/:id");
if (matched) {
console.log(params.id);
}useParams
Returns route parameters from the nearest <Route>.
const { id } = useParams<"/users/:id">();useSearch
Returns the search string (without the leading ?).
// URL: /page?sort=name&page=2
const search = useSearch(); // "sort=name&page=2"useSearchParams
Returns URLSearchParams and a setter.
const { params, setParams } = useSearchParams();
const sort = params.get("sort");
setParams(new URLSearchParams({ sort: "date" }));
// Functional update
setParams((prev) => {
prev.set("page", "2");
return prev;
});useRouter
Returns the current router context.
const router = useRouter();
console.log(router.base); // "/"Pattern Matching
Match paths directly without hooks or components.
import { matchPath, matchRoute } from "@w3cj/ruta";
const { matched, params } = matchPath("/users/:id", "/users/42");
// matched: true, params: { id: "42" }
const result = matchPath("/files/*", "/files/a/b/c");
// result.matched: true, result.params: { "*": "a/b/c" }
const match = matchRoute(/^\/post-(?<slug>\w+)$/, "/post-hello");
// match.matched: true, match.params: { slug: "hello" }Path Utilities
import { absolutePath, relativePath, sanitizeSearch } from "@w3cj/ruta";
absolutePath("/dashboard", "/app"); // "/app/dashboard"
absolutePath("~/home", "/app"); // "/home" (~ bypasses base)
relativePath("/app", "/app/dashboard"); // "/dashboard"
sanitizeSearch("?foo=bar"); // "foo=bar"Manual Route Registration
You can use Switch and Route to create the route tree manually if needed, but you will not get type-safety.
import { Link, Route, Switch, useParams } from "@w3cj/ruta";
const App = () => (
<>
<nav>
<Link href="/">Home</Link>
<Link href="/about">About</Link>
</nav>
<Switch>
<Route path="/" component={Home} />
<Route path="/about" component={About} />
<Route path="/users/:id" component={User} />
</Switch>
</>
);
const Home = () => <h1>Home</h1>;
const About = () => <h1>About</h1>;
const User = () => {
const { id } = useParams<"/users/:id">();
return (
<h1>
User
{id}
</h1>
);
};