Skip to content

Full-Stack Reference

Complete reference for jac-client, the full-stack web development plugin for Jac.


pip install jac-client

jac create myapp --use client
cd myapp
myapp/
├── jac.toml # Project configuration
├── main.jac # Entry point with app() function
├── components/ # Reusable components
│ └── Button.tsx # TypeScript components supported
└── styles/ # CSS files
└── main.css

Use cl { } to define client-side (React) code:

cl {
def:pub app() -> JsxElement {
return <div>
<h1>Hello, World!</h1>
</div>;
}
}

The entry app() function must be exported with :pub:

cl {
def:pub app() -> JsxElement { # :pub required
return <App />;
}
}

cl {
def:pub Button(props: dict) -> JsxElement {
return <button
className={props.get("className", "")}
onClick={props.get("onClick")}
>
{props.children}
</button>;
}
}
cl {
def:pub Card(props: dict) -> JsxElement {
return <div className="card">
<h2>{props["title"]}</h2>
<p>{props["description"]}</p>
{props.children}
</div>;
}
}
cl {
def:pub app() -> JsxElement {
return <div>
<Card title="Welcome" description="Hello!">
<Button onClick={lambda -> None { print("clicked"); }}>
Click Me
</Button>
</Card>
</div>;
}
}

Inside cl { } blocks, has creates reactive state:

cl {
def:pub Counter() -> JsxElement {
has count: int = 0; # Compiles to useState(0)
return <div>
<p>Count: {count}</p>
<button onClick={lambda -> None { count = count + 1; }}>
Increment
</button>
</div>;
}
}
Jac SyntaxReact Equivalent
has count: int = 0const [count, setCount] = useState(0)
count = count + 1setCount(count + 1)
cl {
def:pub Form() -> JsxElement {
has name: str = "";
has items: list = [];
has data: dict = {"key": "value"};
# Create new references for lists/objects
def add_item(item: str) -> None {
items = items + [item]; # Concatenate to new list
}
return <div>Form</div>;
}
}

Similar to how has variables automatically generate useState, the can with entry and can with exit syntax automatically generates useEffect hooks:

Jac SyntaxReact Equivalent
can with entry { ... }useEffect(() => { ... }, [])
async can with entry { ... }useEffect(() => { (async () => { ... })(); }, [])
can with exit { ... }useEffect(() => { return () => { ... }; }, [])
can with [dep] entry { ... }useEffect(() => { ... }, [dep])
can with (a, b) entry { ... }useEffect(() => { ... }, [a, b])
cl {
def:pub DataLoader() -> JsxElement {
has data: list = [];
has loading: bool = True;
# Run once on mount (async with IIFE wrapping)
async can with entry {
data = await fetch_data();
loading = False;
}
# Cleanup on unmount
can with exit {
cleanup_subscriptions();
}
return <div>...</div>;
}
def:pub UserProfile(userId: str) -> JsxElement {
has user: dict = {};
# Re-run when userId changes (dependency array)
async can with [userId] entry {
user = await fetch_user(userId);
}
# Multiple dependencies using tuple syntax
async can with (userId, refresh) entry {
user = await fetch_user(userId);
}
return <div>{user.name}</div>;
}
}

You can also use useEffect manually by importing it from React:

cl {
import from react { useEffect }
def:pub DataLoader() -> JsxElement {
has data: list = [];
has loading: bool = True;
# Run once on mount
useEffect(lambda -> None {
fetch_data();
}, []);
# Run when dependency changes
useEffect(lambda -> None {
refresh_data();
}, [some_dep]);
return <div>...</div>;
}
}
cl {
import from react { createContext, useContext }
glob AppContext = createContext(None);
def:pub AppProvider(props: dict) -> JsxElement {
has theme: str = "light";
return <AppContext.Provider value={{"theme": theme}}>
{props.children}
</AppContext.Provider>;
}
def:pub ThemedComponent() -> JsxElement {
ctx = useContext(AppContext);
return <div className={ctx.theme}>Content</div>;
}
}

Use native Jac spawn syntax to call walkers from client code. First, import your walkers with sv import, then spawn them:

# Import walkers from backend
sv import from ...main { get_tasks, create_task }
cl {
def:pub TaskList() -> JsxElement {
has tasks: list = [];
has loading: bool = True;
# Fetch data on component mount
async can with entry {
result = root spawn get_tasks();
if result.reports and result.reports.length > 0 {
tasks = result.reports[0];
}
loading = False;
}
if loading {
return <p>Loading...</p>;
}
return <ul>
{[<li key={task["id"]}>{task["title"]}</li> for task in tasks]}
</ul>;
}
}

The spawn call returns a result object:

PropertyTypeDescription
result.reportslistData reported by walker via report
result.statusintHTTP status code
sv import from ...main { create_task, toggle_task, delete_task }
cl {
def:pub TaskForm() -> JsxElement {
has title: str = "";
has tasks: list = [];
async def handleSubmit() -> None {
if not title.trim() {
return;
}
result = root spawn create_task(title=title.trim());
if result.reports {
tasks = tasks.concat([result.reports[0]]);
}
title = "";
}
async def handleToggle(taskId: str) -> None {
taskId spawn toggle_task();
# Update local state
}
return <form onSubmit={lambda e: any -> None { e.preventDefault(); handleSubmit(); }}>
<input value={title} onChange={lambda e: any -> None { title = e.target.value; }} />
<button type="submit">Add Task</button>
</form>;
}
}

jac-client supports file-based routing using a pages/ directory:

myapp/
├── main.jac
└── pages/
├── index.jac # /
├── about.jac # /about
├── users/
│ ├── index.jac # /users
│ └── [id].jac # /users/:id (dynamic route)
└── (auth)/ # Route group (parentheses)
├── layout.jac # Shared layout for auth routes
├── login.jac # /login
└── signup.jac # /signup

Each page file exports a page function:

# pages/about.jac
cl {
def:pub page() -> any {
return <div>
<h1>About Us</h1>
</div>;
}
}

For manual routing, import components from @jac/runtime:

cl import from "@jac/runtime" { Router, Routes, Route, Link }
cl {
def:pub app() -> JsxElement {
return <Router>
<nav>
<Link to="/">Home</Link>
<Link to="/about">About</Link>
</nav>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
</Routes>
</Router>;
}
}
cl import from "@jac/runtime" { useParams }
cl {
def:pub UserProfile() -> JsxElement {
params = useParams();
user_id = params["id"];
return <div>User: {user_id}</div>;
}
# Route: /user/:id
}
cl import from "@jac/runtime" { useNavigate }
cl {
def:pub LoginForm() -> JsxElement {
navigate = useNavigate();
async def handle_login() -> None {
success = await do_login();
if success {
navigate("/dashboard");
}
}
return <button onClick={lambda -> None { handle_login(); }}>
Login
</button>;
}
}
cl import from "@jac/runtime" { Outlet }
cl {
def:pub DashboardLayout() -> JsxElement {
# Child routes render where Outlet is placed
return <div>
<Sidebar />
<main>
<Outlet />
</main>
</div>;
}
}

jac-client provides built-in authentication functions via @jac/runtime.

FunctionReturnsDescription
jacLogin(username, password)boolLogin user, returns True on success
jacSignup(username, password)dictRegister user, returns {success: bool, error?: str}
jacLogout()voidClear auth token
jacIsLoggedIn()boolCheck if user is authenticated
cl import from "@jac/runtime" { jacLogin, useNavigate }
cl {
def:pub LoginForm() -> any {
has username: str = "";
has password: str = "";
has error: str = "";
navigate = useNavigate();
async def handleLogin(e: any) -> None {
e.preventDefault();
# jacLogin returns bool (True = success, False = failure)
success = await jacLogin(username, password);
if success {
navigate("/dashboard");
} else {
error = "Invalid credentials";
}
}
return <form onSubmit={handleLogin}>...</form>;
}
}
cl import from "@jac/runtime" { jacSignup }
cl {
async def handleSignup() -> None {
# jacSignup returns dict with success key
result = await jacSignup(username, password);
if result["success"] {
# User registered and logged in
navigate("/dashboard");
} else {
error = result["error"] or "Signup failed";
}
}
}
cl import from "@jac/runtime" { jacLogout, jacIsLoggedIn }
cl {
def:pub NavBar() -> any {
isLoggedIn = jacIsLoggedIn();
def handleLogout() -> None {
jacLogout();
# Redirect to login
}
return <nav>
{isLoggedIn and (
<button onClick={lambda -> None { handleLogout(); }}>Logout</button>
) or (
<a href="/login">Login</a>
)}
</nav>;
}
}

Use AuthGuard to protect routes in file-based routing:

cl import from "@jac/runtime" { AuthGuard, Outlet }
# pages/(auth)/layout.jac
cl {
def:pub layout() -> any {
return <AuthGuard redirect="/login">
<Outlet />
</AuthGuard>;
}
}

cl {
def:pub StyledComponent() -> JsxElement {
return <div style={{"color": "blue", "padding": "10px"}}>
Styled content
</div>;
}
}
cl {
def:pub Card() -> JsxElement {
return <div className="card card-primary">
Content
</div>;
}
}
/* styles/main.css */
.card {
padding: 1rem;
border-radius: 8px;
}
cl {
import "./styles/main.css";
}

TypeScript/TSX files are automatically supported:

// components/Button.tsx
import React from 'react';
interface ButtonProps {
label: string;
onClick: () => void;
}
export const Button: React.FC<ButtonProps> = ({ label, onClick }) => {
return <button onClick={onClick}>{label}</button>;
};
cl {
import from "./components/Button" { Button }
def:pub app() -> JsxElement {
return <Button label="Click" onClick={lambda -> None { }} />;
}
}

[project]
name = "myapp"
version = "0.1.0"
[serve]
base_route_app = "app" # Serve at /
cl_route_prefix = "/cl" # Client route prefix
[plugins.client]
enabled = true
[plugins.client.configs.tailwind]
# Generates tailwind.config.js
content = ["./src/**/*.{jac,tsx,jsx}"]

CommandDescription
jac create myapp --use clientCreate new full-stack project
jac startStart dev server
jac start --devDev server with HMR
jac start --client pwaStart PWA (builds then serves)
jac start --client desktopStart desktop app in dev mode
jac buildBuild for production (web)
jac build --client desktopBuild desktop app
jac build --client pwaBuild PWA with offline support
jac setup desktopOne-time desktop target setup (Tauri)
jac setup pwaOne-time PWA setup (icons directory)
jac add --npm <pkg>Add npm package
jac remove --npm <pkg>Remove npm package

Build a Jac application for a specific target.

jac build [filename] [--client TARGET] [-p PLATFORM]
OptionDescriptionDefault
filenamePath to .jac filemain.jac
--clientBuild target (web, desktop, pwa)web
-p, --platformDesktop platform (windows, macos, linux, all)Current platform

Examples:

# Build web target (default)
jac build
# Build specific file
jac build main.jac
# Build PWA with offline support
jac build --client pwa
# Build desktop app for current platform
jac build --client desktop
# Build for a specific platform
jac build --client desktop --platform windows
# Build for all platforms
jac build --client desktop --platform all

One-time initialization for a build target.

jac setup <target>
OptionDescription
targetTarget to setup (desktop, pwa)

Examples:

# Setup desktop target (creates src-tauri/ directory)
jac setup desktop
# Setup PWA target (creates pwa_icons/ directory)
jac setup pwa

jac-client extends several core commands:

CommandAdded OptionDescription
jac create--use clientCreate full-stack project template
jac create--skipSkip npm package installation
jac start--client <target>Client build target for dev server
jac add--npmAdd npm (client-side) dependency
jac remove--npmRemove npm (client-side) dependency

jac-client supports building for multiple deployment targets from a single codebase.

TargetCommandOutputSetup Required
Web (default)jac build.jac/client/dist/No
Desktop (Tauri)jac build --client desktopNative installersYes
PWAjac build --client pwaInstallable web appNo

Standard browser deployment using Vite:

jac build # Build for web
jac start --dev # Dev server with HMR

Output: .jac/client/dist/ with index.html, bundled JS, and CSS.

Native desktop applications using Tauri. Creates installers for Windows, macOS, and Linux.

Prerequisites:

  • Rust/Cargo: rustup.rs
  • Build tools (platform-specific)

Setup & Build:

# 1. One-time setup (creates src-tauri/ directory)
jac setup desktop
# 2. Development with hot reload
jac start main.jac --client desktop --dev
# 3. Build installer for current platform
jac build --client desktop
# 4. Build for specific platform
jac build --client desktop --platform windows
jac build --client desktop --platform macos
jac build --client desktop --platform linux

Output: Installers in src-tauri/target/release/bundle/:

  • Windows: .exe installer
  • macOS: .dmg or .app bundle
  • Linux: .AppImage, .deb, or .rpm

Configuration: Edit src-tauri/tauri.conf.json to customize window size, title, and app metadata.

Progressive Web App with offline support, installability, and native-like experience.

Features:

  • Offline support via Service Worker
  • Installable on devices
  • Auto-generated manifest.json
  • Automatic icon generation (with Pillow)

Setup & Build:

# Optional: One-time setup (creates pwa_icons/ directory)
jac setup pwa
# Build PWA (includes manifest + service worker)
jac build --client pwa
# Development (service worker disabled for better DX)
jac start --client pwa --dev
# Production (builds PWA then serves)
jac start --client pwa

Output: Web bundle + manifest.json + sw.js (service worker)

Configuration in jac.toml:

[plugins.client.pwa]
theme_color = "#000000"
background_color = "#ffffff"
cache_name = "my-app-cache-v1"
[plugins.client.pwa.manifest]
name = "My App"
short_name = "App"
description = "My awesome Jac app"

Custom Icons: Add pwa-192x192.png and pwa-512x512.png to pwa_icons/ directory.


# Basic
jac start main.jac
# With hot module replacement
jac start main.jac --dev
# HMR without client bundling (API only)
jac start main.jac --dev --no-client
# Dev server for desktop target
jac start main.jac --client desktop

In dev mode, API routes are automatically proxied:

  • /walker/* → Backend
  • /function/* → Backend
  • /user/* → Backend

cl {
def:pub Form() -> JsxElement {
has value: str = "";
return <div>
<input
value={value}
onChange={lambda e: any -> None { value = e.target.value; }}
onKeyPress={lambda e: any -> None {
if e.key == "Enter" { submit(); }
}}
/>
<button onClick={lambda -> None { submit(); }}>
Submit
</button>
</div>;
}
}

cl {
def:pub ConditionalComponent() -> JsxElement {
has show: bool = False;
has items: list = [];
if show {
content = <p>Visible</p>;
} else {
content = <p>Hidden</p>;
}
return <div>
{content}
{show and <p>Only when true</p>}
{[<li key={item["id"]}>{item["name"]}</li> for item in items]}
</div>;
}
}

JacClientErrorBoundary is a specialized error boundary component that catches rendering errors in your component tree, logs them, and displays a fallback UI, preventing the entire app from crashing when a descendant component fails.

Import and wrap JacClientErrorBoundary around any subtree where you want to catch render-time errors:

cl import from "@jac/runtime" { JacClientErrorBoundary }
cl {
def:pub app() -> any {
return <JacClientErrorBoundary fallback={<div>Oops! Something went wrong.</div>}>
<MainAppComponents />
</JacClientErrorBoundary>;
}
}

By default, jac-client internally wraps your entire application with JacClientErrorBoundary. This means:

  • You don’t need to manually wrap your root app component
  • Errors in any component are caught and handled gracefully
  • The app continues to run and displays a fallback UI instead of crashing
PropTypeDescription
fallbackJsxElementCustom fallback UI to show on error
FallbackComponentComponentShow default fallback UI with error
childrenJsxElementComponents to protect
cl {
def:pub App() -> any {
return <JacClientErrorBoundary fallback={<div className="error">Component failed to load</div>}>
<ExpensiveWidget />
</JacClientErrorBoundary>;
}
}

You can nest multiple error boundaries for fine-grained error isolation:

cl {
def:pub App() -> any {
return <JacClientErrorBoundary fallback={<div>App error</div>}>
<Header />
<JacClientErrorBoundary fallback={<div>Content error</div>}>
<MainContent />
</JacClientErrorBoundary>
<Footer />
</JacClientErrorBoundary>;
}
}

If MainContent throws an error, only that boundary’s fallback is shown, while Header and Footer continue rendering normally.

  1. Isolate Failure-Prone Widgets: Protect sections that fetch data, embed third-party code, or are unstable
  2. Per-Page Protection: Wrap top-level pages/routes to prevent one error from failing the whole app
  3. Micro-Frontend Boundaries: Nest boundaries around embeddables for fault isolation