Skip to content

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


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:

  1. File-Based Routing (Recommended) - Convention over configuration
  2. Manual Routing - React Router-style explicit routes

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>;
}
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  # Matches /users/:id
# 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>;
}
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#

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>;
}

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#