Skip to content

Routing#

Build multi-page applications with client-side routing.

Prerequisites


Overview#

Jac-client provides React Router-style routing:

cl {
    import from jac_client { Router, Route, Link }

    def:pub app() -> any {
        return <Router>
            <Route path="/" element={<Home />} />
            <Route path="/about" element={<About />} />
            <Route path="/contact" element={<Contact />} />
        </Router>;
    }
}

Basic Routing#

Setting Up Routes#

cl {
    import from jac_client { Router, Route, Link }

    def:pub Home() -> any {
        return <div>
            <h1>Home Page</h1>
            <p>Welcome to our site!</p>
        </div>;
    }

    def:pub About() -> any {
        return <div>
            <h1>About Us</h1>
            <p>Learn more about our company.</p>
        </div>;
    }

    def:pub Contact() -> any {
        return <div>
            <h1>Contact</h1>
            <p>Get in touch with us.</p>
        </div>;
    }

    def:pub app() -> any {
        return <Router>
            <nav>
                <Link to="/">Home</Link>
                <Link to="/about">About</Link>
                <Link to="/contact">Contact</Link>
            </nav>

            <main>
                <Route path="/" element={<Home />} />
                <Route path="/about" element={<About />} />
                <Route path="/contact" element={<Contact />} />
            </main>
        </Router>;
    }
}
cl {
    # Navigation example showing Link vs anchor
    def:pub NavExample() -> any {
        return <div>
            <Link to="/about">About</Link>
            <a href="https://example.com">External Site</a>
        </div>;
    }
}

Dynamic Routes#

URL Parameters#

cl {
    import from jac_client { Router, Route, useParams }

    def:pub UserProfile() -> any {
        # Get URL parameters
        params = useParams();
        user_id = params["id"];

        return <div>
            <h1>User Profile</h1>
            <p>Viewing user: {user_id}</p>
        </div>;
    }

    def:pub app() -> any {
        return <Router>
            <Route path="/user/:id" element={<UserProfile />} />
        </Router>;
    }
}

Multiple Parameters#

cl {
    import from jac_client { useParams }

    def:pub BlogPost() -> any {
        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#

cl {
    import from jac_client { Router, Route, Outlet }

    def:pub DashboardLayout() -> any {
        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() -> any {
        return <h2>Dashboard Overview</h2>;
    }

    def:pub DashboardSettings() -> any {
        return <h2>Settings</h2>;
    }

    def:pub DashboardProfile() -> any {
        return <h2>Profile</h2>;
    }

    def:pub app() -> any {
        return <Router>
            <Route path="/dashboard" element={<DashboardLayout />}>
                <Route index element={<DashboardHome />} />
                <Route path="settings" element={<DashboardSettings />} />
                <Route path="profile" element={<DashboardProfile />} />
            </Route>
        </Router>;
    }
}

Programmatic Navigation#

useNavigate Hook#

cl {
    import from jac_client { useNavigate }

    def:pub LoginForm() -> any {
        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: any -> None { email = e.target.value; }}
            />
            <button onClick={lambda -> None { handle_login(); }}>
                Login
            </button>
        </form>;
    }
}
cl {
    import from jac_client { useNavigate }

    def:pub NavExample() -> any {
        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#

Protected Routes#

cl {
    import from jac_client { useNavigate }

    def:pub ProtectedRoute(props: dict) -> any {
        auth = use_auth();
        navigate = useNavigate();

        if auth.loading {
            return <div>Loading...</div>;
        }

        if not auth.isAuthenticated {
            # Redirect to login
            navigate("/login", {"replace": True});
            return None;
        }

        return <div>{props.children}</div>;
    }

    def:pub app() -> any {
        return <Router>
            <Route path="/login" element={<Login />} />

            <Route path="/dashboard" element={
                <ProtectedRoute>
                    <Dashboard />
                </ProtectedRoute>
            } />
        </Router>;
    }
}

Role-Based Access#

cl {
    def:pub AdminRoute(props: dict) -> any {
        auth = use_auth();

        if not auth.isAuthenticated {
            return <Navigate to="/login" />;
        }

        if auth.user.role != "admin" {
            return <div className="error">
                <h2>Access Denied</h2>
                <p>You need admin privileges to view this page.</p>
            </div>;
        }

        return <>{props.children}</>;
    }
}

Query Parameters#

useSearchParams Hook#

cl {
    import from jac_client { useSearchParams }

    def:pub SearchResults() -> any {
        (searchParams, setSearchParams) = useSearchParams();

        query = searchParams.get("q") or "";
        page = int(searchParams.get("page") or "1");

        def update_page(new_page: int) -> None {
            setSearchParams({"q": query, "page": str(new_page)});
        }

        return <div>
            <h2>Results for: {query}</h2>
            <p>Page: {page}</p>

            <button
                onClick={lambda -> None { update_page(page - 1); }}
                disabled={page <= 1}
            >
                Previous
            </button>

            <button onClick={lambda -> None { update_page(page + 1); }}>
                Next
            </button>
        </div>;
    }

    # URL: /search?q=hello&page=2
}

404 Not Found#

cl {
    import from jac_client { Router, Route }

    def:pub NotFound() -> any {
        return <div className="error-page">
            <h1>404</h1>
            <p>Page not found</p>
            <Link to="/">Go Home</Link>
        </div>;
    }

    def:pub app() -> any {
        return <Router>
            <Route path="/" element={<Home />} />
            <Route path="/about" element={<About />} />

            <Route path="*" element={<NotFound />} />
        </Router>;
    }
}

cl {
    import from jac_client { NavLink }

    def:pub Navigation() -> any {
        return <nav>
            <NavLink
                to="/"
                className={lambda info: any -> str {
                    return "nav-link " + ("active" if info.isActive else "");
                }}
            >
                Home
            </NavLink>

            <NavLink
                to="/about"
                className={lambda info: any -> str {
                    return "nav-link " + ("active" if info.isActive else "");
                }}
            >
                About
            </NavLink>
        </nav>;
    }
}
/* styles.css */
.nav-link {
    color: gray;
    text-decoration: none;
}

.nav-link.active {
    color: blue;
    font-weight: bold;
}

Complete Example#

cl {
    import from jac_client { Router, Route, Link, Outlet, useParams, useNavigate }

    # Layout
    def:pub Layout() -> any {
        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() -> any {
        return <div>
            <h1>Welcome!</h1>
            <Link to="/products">Browse Products</Link>
        </div>;
    }

    def:pub Products() -> any {
        products = [
            {"id": 1, "name": "Widget A"},
            {"id": 2, "name": "Widget B"},
            {"id": 3, "name": "Widget C"}
        ];

        return <div>
            <h1>Products</h1>
            <ul>
                {products.map(lambda p: any -> any {
                    return <li key={p["id"]}>
                        <Link to={f"/products/{p['id']}"}>
                            {p["name"]}
                        </Link>
                    </li>;
                })}
            </ul>
        </div>;
    }

    def:pub ProductDetail() -> any {
        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() -> any {
        return <div>
            <h1>About Us</h1>
            <p>We make great products.</p>
        </div>;
    }

    def:pub NotFound() -> any {
        return <div>
            <h1>404 - Not Found</h1>
            <Link to="/">Go Home</Link>
        </div>;
    }

    # App
    def:pub app() -> any {
        return <Router>
            <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>
        </Router>;
    }
}

Key Takeaways#

Concept Usage
Define routes <Route path="/..." element={<Comp />} />
Navigation links <Link to="/path">Text</Link>
URL parameters useParams() returns {param: value}
Programmatic nav navigate("/path") or navigate(-1)
Query strings useSearchParams()
Nested routes <Outlet /> renders child routes
404 handling <Route path="*" element={<NotFound />} />

Next Steps#