Routing#
When your application grows beyond a single view, you need routing -- the ability to map URLs to different pages or views within your app. Jac-client supports client-side routing (the browser URL changes but the page doesn't fully reload) with two approaches: file-based routing that uses directory conventions (like Next.js) and manual routing using explicit route declarations (like React Router).
Prerequisites
- Completed: Authentication
- Time: ~20 minutes
Overview#
Single-page vs multi-page
If your app is a single-page application (like the AI Day Planner tutorial), you don't need routing -- a single def:pub app -> JsxElement entry point is sufficient. Add routing when your app needs multiple distinct pages (e.g., dashboard, settings, profile).
Browser APIs in client code
Inside cl { } blocks, standard JavaScript browser APIs like URLSearchParams, parseInt, setInterval, clearInterval, localStorage, and JSON are available since client code compiles to JavaScript.
Route params and jac check
useParams() and useSearchParams() return JS-flavored objects whose dynamic property access (params.id, params.slug) works at runtime but is not yet typed in the static checker. Snippets that read those props show E1030 warnings under isolated jac check, even though they run cleanly under jac start.
Jac-client supports two routing approaches:
- File-Based Routing (Recommended) - Convention over configuration
- Manual Routing - React Router-style explicit routes
File-Based Routing (Recommended)#
Create a pages/ directory with .jac files that automatically become routes.
Project Structure#
myapp/
├── main.jac
└── pages/
├── layout.jac # Root layout (wraps all pages)
├── index.jac # / (home page)
├── about.jac # /about
├── users/
│ ├── index.jac # /users
│ └── [id].jac # /users/:id (dynamic)
├── posts/
│ ├── index.jac # /posts
│ └── [slug].jac # /posts/:slug (dynamic)
├── (public)/ # Route group (no auth required)
│ ├── login.jac # /login
│ └── signup.jac # /signup
├── (auth)/ # Route group (auth required)
│ ├── index.jac # / (protected home)
│ └── dashboard.jac # /dashboard
└── [...notFound].jac # Catch-all 404 page
Route Mapping Reference#
| File | Route | Description |
|---|---|---|
pages/index.jac |
/ |
Home page |
pages/about.jac |
/about |
Static page |
pages/users/index.jac |
/users |
Users list |
pages/users/[id].jac |
/users/:id |
Dynamic user profile |
pages/posts/[slug].jac |
/posts/:slug |
Dynamic blog post |
pages/[...notFound].jac |
* |
Catch-all 404 |
Basic Page#
Each page file exports a page function:
# pages/about.jac
to cl:
def:pub page() -> JsxElement {
return <div>
<h1>About Us</h1>
<p>Learn more about our company.</p>
</div>;
}
Dynamic Routes with [param]#
Use square brackets for dynamic URL segments:
# pages/users/[id].jac
cl import from "@jac/runtime" { Link, useParams }
to cl:
def:pub page() -> JsxElement {
params = useParams();
userId = params.id;
# Mock data lookup
users = {
"1": {"name": "Alice", "role": "Admin"},
"2": {"name": "Bob", "role": "Developer"}
};
user = users[userId];
if not user {
return <div>
<h1>User Not Found</h1>
<Link to="/users">Back to Users</Link>
</div>;
}
return <div>
<Link to="/users">← Back</Link>
<h1>User: {user["name"]}</h1>
<p>Role: {user["role"]}</p>
</div>;
}
Slug-Based Routes#
# pages/posts/[slug].jac
cl import from "@jac/runtime" { Link, useParams }
to cl:
def:pub page() -> JsxElement {
params = useParams();
slug = params.slug; # e.g., "getting-started-with-jac"
return <article>
<Link to="/posts">← All Posts</Link>
<h1>Blog Post</h1>
<p>Slug: {slug}</p>
</article>;
}
Catch-All Routes with [...param]#
Use [...param] for catch-all routes (404 pages, docs, etc.):
# pages/[...notFound].jac
cl import from "@jac/runtime" { Link }
to cl:
def:pub page() -> JsxElement {
return <div style={{"textAlign": "center", "padding": "2rem"}}>
<h1>404 - Page Not Found</h1>
<p>The page you are looking for does not exist.</p>
<Link to="/">Back to Home</Link>
</div>;
}
Route Groups with (groupName)#
Route groups organize pages without affecting the URL:
| Directory | Effect |
|---|---|
(public)/ |
Groups public pages, no URL segment added |
(auth)/ |
Groups protected pages, auto-requires login |
pages/
├── (public)/
│ ├── login.jac # Route: /login
│ └── signup.jac # Route: /signup
├── (auth)/
│ ├── index.jac # Route: / (protected)
│ └── settings.jac # Route: /settings (protected)
The (auth) group automatically wraps pages with authentication checks.
Layout Files#
Create layout.jac to wrap pages with shared UI:
# pages/layout.jac
cl import from "@jac/runtime" { Outlet }
cl import from ..components.navigation { Navigation }
to cl:
def:pub layout() -> JsxElement {
return <>
<Navigation />
<main style={{"maxWidth": "960px", "margin": "0 auto"}}>
<Outlet /> # Child routes render here
</main>
<footer>Footer content</footer>
</>;
}
Index Files#
index.jac represents the default page for a directory:
| File | Route |
|---|---|
pages/index.jac |
/ |
pages/users/index.jac |
/users |
pages/posts/index.jac |
/posts |
Manual Routing#
For explicit route configuration, import from @jac/runtime:
cl import from "@jac/runtime" { Router, Routes, Route, Link }
to cl:
def:pub app() -> JsxElement {
return <Router>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/contact" element={<Contact />} />
</Routes>
</Router>;
}
Basic Routing#
Setting Up Routes#
cl import from "@jac/runtime" { Router, Routes, Route, Link }
to cl:
def:pub Home() -> JsxElement {
return <div>
<h1>Home Page</h1>
<p>Welcome to our site!</p>
</div>;
}
def:pub About() -> JsxElement {
return <div>
<h1>About Us</h1>
<p>Learn more about our company.</p>
</div>;
}
def:pub Contact() -> JsxElement {
return <div>
<h1>Contact</h1>
<p>Get in touch with us.</p>
</div>;
}
def:pub app() -> JsxElement {
return <Router>
<nav>
<Link to="/">Home</Link>
<Link to="/about">About</Link>
<Link to="/contact">Contact</Link>
</nav>
<main>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/contact" element={<Contact />} />
</Routes>
</main>
</Router>;
}
Link vs Anchor#
to cl:
# Use Link for internal navigation, anchor for external
def:pub NavExample() -> JsxElement {
return <div>
<Link to="/about">About</Link>
<a href="https://example.com">External Site</a>
</div>;
}
Dynamic Routes#
URL Parameters#
File-Based Approach:
Create a file with brackets for dynamic segments:
# pages/users/[id].jac
cl import from "@jac/runtime" { useParams }
to cl:
def:pub page() -> JsxElement {
params = useParams();
user_id = params["id"];
return <div>
<h1>User Profile</h1>
<p>Viewing user: {user_id}</p>
</div>;
}
Manual Route Approach:
cl import from "@jac/runtime" { Router, Routes, Route, useParams }
to cl:
def:pub UserProfile() -> JsxElement {
params = useParams();
user_id = params["id"];
return <div>
<h1>User Profile</h1>
<p>Viewing user: {user_id}</p>
</div>;
}
def:pub app() -> JsxElement {
return <Router>
<Routes>
<Route path="/user/:id" element={<UserProfile />} />
</Routes>
</Router>;
}
Multiple Parameters#
cl import from "@jac/runtime" { useParams }
to cl:
def:pub BlogPost() -> JsxElement {
params = useParams();
return <div>
<p>Category: {params["category"]}</p>
<p>Post ID: {params["postId"]}</p>
</div>;
}
# Route: /blog/:category/:postId
# URL: /blog/tech/123
# params = {"category": "tech", "postId": "123"}
Nested Routes#
Layout Pattern (File-Based)#
Create a layout.jac file in a route group:
pages/
└── dashboard/ # URL segment: /dashboard
├── layout.jac # Shared layout
├── index.jac # /dashboard
├── settings.jac # /dashboard/settings
└── profile.jac # /dashboard/profile
# pages/dashboard/layout.jac
cl import from "@jac/runtime" { Outlet, Link }
to cl:
def:pub layout() -> JsxElement {
return <div className="dashboard">
<aside>
<Link to="/dashboard">Overview</Link>
<Link to="/dashboard/settings">Settings</Link>
<Link to="/dashboard/profile">Profile</Link>
</aside>
<main>
<Outlet />
</main>
</div>;
}
Layout Pattern (Manual)#
cl import from "@jac/runtime" { Router, Routes, Route, Outlet, Link }
to cl:
def:pub DashboardLayout() -> JsxElement {
return <div className="dashboard">
<aside>
<Link to="/dashboard">Overview</Link>
<Link to="/dashboard/settings">Settings</Link>
<Link to="/dashboard/profile">Profile</Link>
</aside>
<main>
<Outlet />
</main>
</div>;
}
def:pub DashboardHome() -> JsxElement {
return <h2>Dashboard Overview</h2>;
}
def:pub DashboardSettings() -> JsxElement {
return <h2>Settings</h2>;
}
def:pub DashboardProfile() -> JsxElement {
return <h2>Profile</h2>;
}
def:pub app() -> JsxElement {
return <Router>
<Routes>
<Route path="/dashboard" element={<DashboardLayout />}>
<Route index element={<DashboardHome />} />
<Route path="settings" element={<DashboardSettings />} />
<Route path="profile" element={<DashboardProfile />} />
</Route>
</Routes>
</Router>;
}
Programmatic Navigation#
useNavigate Hook#
cl import from "@jac/runtime" { useNavigate }
to cl:
def:pub LoginForm() -> JsxElement {
has email: str = "";
has password: str = "";
navigate = useNavigate();
async def handle_login() -> None {
success = await do_login(email, password);
if success {
# Redirect to dashboard
navigate("/dashboard");
}
}
return <form>
<input
value={email}
onChange={lambda e: ChangeEvent { email = e.target.value; }}
/>
<button onClick={lambda -> None { handle_login(); }}>
Login
</button>
</form>;
}
Navigation Options#
cl import from "@jac/runtime" { useNavigate }
to cl:
def:pub NavExample() -> JsxElement {
navigate = useNavigate();
return <div>
<button onClick={lambda -> None { navigate("/home"); }}>
Go Home
</button>
<button onClick={lambda -> None { navigate("/login", {"replace": True}); }}>
Login (replace)
</button>
<button onClick={lambda -> None { navigate(-1); }}>
Back
</button>
<button onClick={lambda -> None { navigate(1); }}>
Forward
</button>
</div>;
}
Route Guards#
Using AuthGuard (Recommended)#
For file-based routing, use the built-in AuthGuard component in a layout file:
# pages/(protected)/layout.jac
cl import from "@jac/runtime" { AuthGuard, Outlet }
to cl:
def:pub layout() -> JsxElement {
return <AuthGuard redirect="/login">
<Outlet />
</AuthGuard>;
}
Any pages in the (protected) group will require authentication.
Custom Protected Routes#
cl import from "@jac/runtime" { useNavigate, jacIsLoggedIn }
to cl:
def:pub ProtectedRoute(props: dict) -> JsxElement {
navigate = useNavigate();
isAuthenticated = jacIsLoggedIn();
can with entry {
if not isAuthenticated {
navigate("/login", {"replace": True});
}
}
if not isAuthenticated {
return <div>Redirecting...</div>;
}
return <div>{props.children}</div>;
}
Query Parameters#
Using useLocation#
Access query parameters using useLocation and standard URL parsing:
cl import from "@jac/runtime" { useLocation, useNavigate }
to cl:
def:pub SearchResults() -> JsxElement {
location = useLocation();
navigate = useNavigate();
# Parse query parameters from location.search
searchParams = URLSearchParams(location.search);
query = searchParams.get("q") or "";
page = int(searchParams.get("page") or "1");
def updatePage(newPage: int) -> None {
navigate(f"/search?q={query}&page={newPage}");
}
return <div>
<h2>Results for: {query}</h2>
<p>Page: {page}</p>
<button
onClick={lambda -> None { updatePage(page - 1); }}
disabled={page <= 1}
>
Previous
</button>
<button onClick={lambda -> None { updatePage(page + 1); }}>
Next
</button>
</div>;
}
# URL: /search?q=hello&page=2
404 Not Found#
cl import from "@jac/runtime" { Router, Routes, Route, Link }
to cl:
def:pub NotFound() -> JsxElement {
return <div className="error-page">
<h1>404</h1>
<p>Page not found</p>
<Link to="/">Go Home</Link>
</div>;
}
def:pub app() -> JsxElement {
return <Router>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="*" element={<NotFound />} />
</Routes>
</Router>;
}
Active Link Styling#
Use useLocation with Link to create active link styling:
cl import from "@jac/runtime" { Link, useLocation }
to cl:
def:pub Navigation() -> JsxElement {
location = useLocation();
def isActive(path: str) -> bool {
return location.pathname == path;
}
return <nav>
<Link
to="/"
className={"nav-link " + ("active" if isActive("/") else "")}
>
Home
</Link>
<Link
to="/about"
className={"nav-link " + ("active" if isActive("/about") else "")}
>
About
</Link>
</nav>;
}
/* styles.css */
.nav-link {
color: gray;
text-decoration: none;
}
.nav-link.active {
color: blue;
font-weight: bold;
}
Complete Example#
cl import from "@jac/runtime" { Router, Routes, Route, Link, Outlet, useParams, useNavigate }
to cl:
# Layout
def:pub Layout() -> JsxElement {
return <div className="app">
<header>
<nav>
<Link to="/">Home</Link>
<Link to="/products">Products</Link>
<Link to="/about">About</Link>
</nav>
</header>
<main>
<Outlet />
</main>
<footer>
<p>© 2024 My App</p>
</footer>
</div>;
}
# Pages
def:pub Home() -> JsxElement {
return <div>
<h1>Welcome!</h1>
<Link to="/products">Browse Products</Link>
</div>;
}
def:pub Products() -> JsxElement {
products = [
{"id": 1, "name": "Widget A"},
{"id": 2, "name": "Widget B"},
{"id": 3, "name": "Widget C"}
];
return <div>
<h1>Products</h1>
<ul>
{[
<li key={p["id"]}>
<Link to={f"/products/{p['id']}"}>
{p["name"]}
</Link>
</li>
for p in products
]}
</ul>
</div>;
}
def:pub ProductDetail() -> JsxElement {
params = useParams();
navigate = useNavigate();
product_id = params["id"];
return <div>
<button onClick={lambda -> None { navigate(-1); }}>
← Back
</button>
<h1>Product {product_id}</h1>
<p>Details about product {product_id}</p>
</div>;
}
def:pub About() -> JsxElement {
return <div>
<h1>About Us</h1>
<p>We make great products.</p>
</div>;
}
def:pub NotFound() -> JsxElement {
return <div>
<h1>404 - Not Found</h1>
<Link to="/">Go Home</Link>
</div>;
}
# App
def:pub app() -> JsxElement {
return <Router>
<Routes>
<Route path="/" element={<Layout />}>
<Route index element={<Home />} />
<Route path="products" element={<Products />} />
<Route path="products/:id" element={<ProductDetail />} />
<Route path="about" element={<About />} />
<Route path="*" element={<NotFound />} />
</Route>
</Routes>
</Router>;
}
Routing Hooks Reference#
Import from @jac/runtime:
cl import from "@jac/runtime" {
Link, # Navigation link component
useNavigate, # Programmatic navigation
useParams, # Access URL parameters
useLocation, # Get current location info
Navigate, # Redirect component
Outlet # Render child routes (for layouts)
}
| Hook | Returns | Usage |
|---|---|---|
useParams() |
dict |
params.id, params.slug |
useNavigate() |
function | navigate("/path"), navigate(-1) |
useLocation() |
object | location.pathname, location.search |
Key Takeaways#
File-Based Routing Patterns#
| Pattern | File | Route |
|---|---|---|
| Static page | about.jac |
/about |
| Index page | users/index.jac |
/users |
| Dynamic param | users/[id].jac |
/users/:id |
| Slug param | posts/[slug].jac |
/posts/:slug |
| Catch-all | [...notFound].jac |
* (404) |
| Route group | (auth)/dashboard.jac |
/dashboard |
| Layout | layout.jac |
Wraps child routes |
Quick Reference#
| Concept | Usage |
|---|---|
| Navigation links | <Link to="/path">Text</Link> |
| URL parameters | params = useParams(); params.id |
| Programmatic nav | navigate("/path") or navigate(-1) |
| Query strings | useLocation().search + URLSearchParams |
| Nested routes | <Outlet /> renders child routes |
| Protected routes | Use (auth)/ group or AuthGuard |
| 404 handling | [...notFound].jac or path="*" |
Next Steps#
- Backend Integration - Connect to walker APIs
- Authentication - Add protected routes