Skip to content

w3cj/ruta

Repository files navigation

ruta

A tiny type-safe client-side router for hono/jsx client components.

Zero dependencies, only 2.49 KB with brotli compression.

Inspired by wouter and @tanstack/react-router

Install

pnpm install @w3cj/ruta

Quick 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 Register is not augmented, all APIs accept plain strings and params are 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>
  );
};

About

A tiny type-safe client-side router for hono/jsx

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors