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.

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#