Skip to main content

Client Routes

evjs routing is built on TanStack Router. All routing APIs are re-exported from @evjs/client — never import from @tanstack/react-router directly.

important

Route paths must be string literals. The path property only accepts string literal types — passing a string variable or template string will produce a TypeScript compile error. This is enforced by the type system to ensure routes are statically analyzable.

// ✅ Good — string literal
createRoute({ path: "/users/$id", ... });

// ❌ Compile error — broad `string` type
const p: string = "/users";
createRoute({ path: p, ... });

// ❌ Compile error — template string
createRoute({ path: `/users/${segment}`, ... });

Project Structure

src/
├── main.tsx ← Entry: build route tree, createApp, register types
├── api/*.server.ts ← Server functions
└── pages/
├── __root.tsx ← Root layout (nav + <Outlet />)
├── home.tsx ← Static route
├── user.tsx ← Dynamic route (/users/$username)
├── posts/index.tsx ← Nested routes with layout
├── dashboard.tsx ← Pathless layout
├── search.tsx ← Search param validation
└── catch.tsx ← Redirects & 404 catch-all

Entry Point Setup

// src/main.tsx
import { createApp } from "@evjs/client";
import { rootRoute } from "./pages/__root";
import { homeRoute } from "./pages/home";
import { postsRoute, postsIndexRoute, postDetailRoute } from "./pages/posts";

const routeTree = rootRoute.addChildren([
homeRoute,
postsRoute.addChildren([postsIndexRoute, postDetailRoute]),
]);

const app = createApp({ routeTree });

// Required for full type-safety on useParams, useSearch, Link, etc.
declare module "@tanstack/react-router" {
interface Register {
router: typeof app.router;
}
}

app.render("#app");

Root Layout

Every app needs a root route with <Outlet /> to render child routes:

import { createAppRootRoute, Link, Outlet } from "@evjs/client";

function RootLayout() {
return (
<div>
<nav>
<Link to="/" activeProps={{ style: { fontWeight: 600 } }}>Home</Link>
<Link to="/posts" activeProps={{ style: { fontWeight: 600 } }}>Posts</Link>
</nav>
<Outlet />
</div>
);
}

export const rootRoute = createAppRootRoute({ component: RootLayout });

Static Routes

import { createRoute } from "@evjs/client";
import { rootRoute } from "./__root";

export const homeRoute = createRoute({
getParentRoute: () => rootRoute,
path: "/",
component: () => <h1>Home</h1>,
});

Dynamic Routes ($param)

Use $name syntax for path parameters. Access them type-safely via route.useParams():

import { createRoute, useQuery } from "@evjs/client";
import { getUser } from "../api/data.server";
import { rootRoute } from "./__root";

function UserProfile() {
const { username } = userRoute.useParams(); // { username: string }
const { data } = useQuery(getUser, username);
return <h2>{data?.name}</h2>;
}

export const userRoute = createRoute({
getParentRoute: () => rootRoute,
path: "/users/$username",
loader: ({ params, context }) =>
context.queryClient.ensureQueryData(
getFnQueryOptions(getUser, params.username),
),
component: UserProfile,
});

Nested Routes (Layout + Children)

Parent routes render <Outlet /> to display child routes. Wire children via addChildren() in main.tsx:

// pages/posts/index.tsx
import { createRoute, Link, Outlet } from "@evjs/client";
import { rootRoute } from "../__root";

// Layout route: /posts
export const postsRoute = createRoute({
getParentRoute: () => rootRoute,
path: "/posts",
component: () => (
<div style={{ display: "flex" }}>
<nav>{ /* sidebar */ }</nav>
<Outlet />
</div>
),
});

// Index route: /posts/ (shown when no child matches)
export const postsIndexRoute = createRoute({
getParentRoute: () => postsRoute,
path: "/",
component: () => <p>Select a post</p>,
});

// Detail route: /posts/$postId
export const postDetailRoute = createRoute({
getParentRoute: () => postsRoute,
path: "$postId",
loader: ({ params, context }) =>
context.queryClient.ensureQueryData(
getFnQueryOptions(getPost, params.postId),
),
component: PostDetail,
});

Pathless Layouts

Use id instead of path for shared UI that doesn't add a URL segment:

export const dashboardLayout = createRoute({
getParentRoute: () => rootRoute,
id: "dashboard-layout",
component: () => <div className="layout"><Outlet /></div>,
});

export const dashboardRoute = createRoute({
getParentRoute: () => dashboardLayout,
path: "/dashboard",
component: Dashboard,
});

// main.tsx: dashboardLayout.addChildren([dashboardRoute])

Search Parameters

Use validateSearch to define typed query string parameters:

export const searchRoute = createRoute({
getParentRoute: () => rootRoute,
path: "/search",
validateSearch: (search: Record<string, unknown>) => ({
q: (search.q as string) || "",
page: Number(search.page) || 1,
}),
component: SearchPage,
});

function SearchPage() {
const { q, page } = searchRoute.useSearch(); // { q: string, page: number }
}

Navigate with search params:

<Link to="/search" search={{ q: "hello" }}>Search</Link>

Route Loaders (Prefetching)

Use loader to prefetch data before the route renders — eliminates loading spinners:

export const usersRoute = createRoute({
getParentRoute: () => rootRoute,
path: "/users",
loader: ({ context }) =>
context.queryClient.ensureQueryData(getFnQueryOptions(getUsers)),
component: UsersPage,
});

Redirects

Throw redirect() in beforeLoad to redirect before rendering:

import { createRoute, redirect } from "@evjs/client";

export const redirectRoute = createRoute({
getParentRoute: () => rootRoute,
path: "/old-blog",
beforeLoad: () => {
throw redirect({ to: "/posts" });
},
});

404 Catch-All

Use path: "*" to catch all unmatched URLs:

export const notFoundRoute = createRoute({
getParentRoute: () => rootRoute,
path: "*",
component: () => <h1>404 — Page not found</h1>,
});
import { Link, useNavigate, Navigate } from "@evjs/client";

// Declarative
<Link to="/posts/$postId" params={{ postId: "1" }}>View</Link>

// Imperative
const navigate = useNavigate();
navigate({ to: "/posts" });

// Redirect component
<Navigate to="/login" />

Available Re-exports

All imported from @evjs/client:

CategoryAPIs
Route creationcreateAppRootRoute, createRoute, createRouter, createRootRouteWithContext, createRouteMask
ComponentsLink, Outlet, Navigate, RouterProvider, ErrorComponent, CatchBoundary, CatchNotFound
HooksuseParams, useSearch, useNavigate, useLocation, useMatch, useMatchRoute, useRouter, useRouterState, useLoaderData, useLoaderDeps, useRouteContext, useBlocker, useCanGoBack
Utilitiesredirect, notFound, isRedirect, isNotFound, getRouteApi, linkOptions, lazyRouteComponent, createLink