Skip to content

Todo App - Beginner's Guide to Building with Jac#

Welcome to the Todo App example! This guide will walk you through building a full-stack web application with Jac, from setup to deployment. Jac simplifies web development by eliminating the need for separate frontend and backend technologies, HTTP clients, and complex build configurations.


1. Creating the Application#

Prerequisites#

Before installing Jac client, you need to have Node.js installed on your system.

Installing Node.js#

For Linux users:

Visit https://nodejs.org/en/download and follow the instructions to install Node.js using nvm (Node Version Manager) with npm.

Select: - Platform: Linux - Package Manager: nvm - Package: npm

Then follow the installation commands provided on that page.

For macOS users:

Download and install Node.js from https://nodejs.org/en/download by selecting your operating system.

Verify Installation:

After installation, verify Node.js and npm are installed correctly:

node -v
npm -v

Installation#

Once Node.js is installed, install the Jac client package:

pip install jac-client

Create a New Jac App#

Use the jac create_jac_app command to scaffold a new application: (* we can name our app however we want, here we are using `todo-app)

jac create_jac_app todo-app

This command will: - Create a new directory with your project name - Set up the basic project structure - Initialize npm and install Vite (for development server) - Create a starter app.jac file with a sample component

What gets created:

my-app/
├── app.jac          # Your main application file
├── package.json      # Node.js dependencies
└── node_modules/    # Dependencies (after npm install)

Running Your App#

Navigate to your project directory and start the development server:

cd todo-app
jac serve app.jac

This starts both: - Backend server: Handles your Jac graph operations and walkers - Frontend development server: Serves your React components

You can access your app at http://localhost:8000


2. Entry Point of the App#

Every Jac client application needs an entry point function. This is where your app starts rendering.

The app() Function#

Inside your cl block, define a function called app():

cl import from react {useState}

cl {
    def app() -> any {
        let [count, setCount] = useState(0);
        return <div>
            <h1>Hello, World!</h1>
            <p>Count: {count}</p>
            <button onClick={lambda e: any -> None { setCount(count + 1); }}>
                Increment
            </button>
        </div>;
    }
}

Key Points: - app() is the required entry point that Jac looks for - It must be defined inside a cl { } block (client-side code) - The cl (client) block indicates this code runs in the browser - This function gets called automatically when the page loads - You can define other components and helper functions in the same cl block

Example with Multiple Components:

cl import from react {useState, useEffect}

cl {
    def TodoList(todos: list) -> any {
        return <ul>
            {todos.map(lambda todo: any -> any {
                return <li key={todo._jac_id}>{todo.text}</li>;
            })}
        </ul>;
    }

    def app() -> any {
        let [todos, setTodos] = useState([]);

        useEffect(lambda -> None {
            async def loadTodos() -> None {
                result = root spawn read_todos();
                setTodos(result.reports if result.reports else []);
            }
            loadTodos();
        }, []);

        return <div>
            <h1>My Todos</h1>
            <TodoList todos={todos} />
        </div>;
    }
}

3. Creating Components#

Components in Jac are functions that return JSX (JavaScript XML). They're similar to React components but written in pure Jac syntax.

Basic Component Structure#

cl {
    def MyComponent() -> any {
        return <div>
            <h1>Hello from Jac!</h1>
        </div>;
    }
}

Component with Props#

Components can accept parameters (props):

def TodoItem(item: dict) -> any {
    return <li key={item.id}>
        <span>{item.text}</span>
        <button onClick={lambda -> None { removeTodo(item.id); }}>
            Remove
        </button>
    </li>;
}

Component Features: - JSX Syntax: Write HTML-like syntax directly in Jac - Inline Styles: Use JavaScript objects for styling - Event Handlers: Attach functions to user interactions - Reusability: Components can call other components

Example: TodoItem Component#

def TodoItem(item: dict) -> any {
    return <li key={item.id} style={{
        "display": "flex",
        "gap": "12px",
        "alignItems": "center",
        "padding": "12px",
        "background": "#FFFFFF",
        "borderRadius": "10px"
    }}>
        <input
            type="checkbox"
            checked={item.done}
            onChange={lambda -> None { toggleTodo(item.id); }}
        />
        <span style={{
            "textDecoration": ("line-through" if item.done else "none")
        }}>
            {item.text}
        </span>
        <button onClick={lambda -> None { removeTodo(item.id); }}>
            Remove
        </button>
    </li>;
}

Breaking it down: - item: dict - Component receives a dictionary (todo item) as a prop - style={{...}} - Inline styles using JavaScript objects - checked={item.done} - Dynamic attribute binding - onChange={lambda -> None {...}} - Event handler using lambda - {item.text} - Interpolation of JavaScript expressions in JSX


4. Adding State with React Hooks#

Jac uses React hooks for state management. You can use all standard React hooks by importing them:

cl import from react { useState, useEffect }

cl {
    def Counter() -> any {
        let [count, setCount] = useState(0);

        useEffect(lambda -> None {
            console.log("Count changed:", count);
        }, [count]);

        return <div>
            <h1>Count: {count}</h1>
            <button onClick={lambda e: any -> None {
                setCount(count + 1);
            }}>
                Increment
            </button>
        </div>;
    }
}

Available React Hooks: - useState - For state management - useEffect - For side effects - useRef - For refs - useContext - For context - useCallback, useMemo - For performance optimization - And more...

State Management Example#

Here's a complete example showing state management in a todo app:

cl import from react {useState, useEffect}

cl {
    def app() -> any {
        let [todos, setTodos] = useState([]);
        let [input, setInput] = useState("");
        let [filter, setFilter] = useState("all");

        # Load todos on mount
        useEffect(lambda -> None {
            async def loadTodos() -> None {
                result = root spawn read_todos();
                setTodos(result.reports if result.reports else []);
            }
            loadTodos();
        }, []);

        # Add new todo
        async def addTodo() -> None {
            if not input.trim() { return; }
            response = root spawn create_todo(text=input.trim());
            new_todo = response.reports[0][0];
            setTodos(todos.concat([new_todo]));
            setInput("");
        }

        # Filter todos
        def getFilteredTodos() -> list {
            if filter == "active" {
                return todos.filter(lambda todo: any -> bool { return not todo.done; });
            } elif filter == "completed" {
                return todos.filter(lambda todo: any -> bool { return todo.done; });
            }
            return todos;
        }

        filteredTodos = getFilteredTodos();

        return <div>
            <h1>My Todos</h1>
            <input
                value={input}
                onChange={lambda e: any -> None { setInput(e.target.value); }}
                onKeyPress={lambda e: any -> None {
                    if e.key == "Enter" { addTodo(); }
                }}
            />
            <button onClick={addTodo}>Add</button>

            <div>
                {filteredTodos.map(lambda todo: any -> any {
                    return <div key={todo._jac_id}>
                        <span>{todo.text}</span>
                    </div>;
                })}
            </div>
        </div>;
    }
}

5. Event Handling#

Event handling in Jac works just like React, but with Jac's lambda syntax.

Basic Event Handlers#

def Button() -> any {
    return <button onClick={lambda -> None {
        console.log("Button clicked!");
    }}>
        Click Me
    </button>;
}

Event Handlers with Event Object#

def InputField() -> any {
    let [value, setValue] = useState("");

    return <input
        type="text"
        value={value}
        onChange={lambda e: any -> None {
            console.log("Input value:", e.target.value);
            setValue(e.target.value);
        }}
    />;
}

Form Submission#

def TodoForm() -> any {
    return <form onSubmit={onAddTodo}>
        <input id="todo-input" type="text" />
        <button type="submit">Add Todo</button>
    </form>;
}

async def onAddTodo(e: any) -> None {
    e.preventDefault();  # Prevent page refresh
    inputEl = document.getElementById("todo-input");
    text = inputEl.value.trim();

    if not text { return; }

    # Handle form submission
    response = root spawn create_todo(text=text);
    new_todo = response.reports[0][0];
    # ... update state
}

Common Event Types#

Event Syntax Use Case
onClick onClick={lambda -> None {...}} Button clicks
onChange onChange={lambda e: any -> None {...}} Input changes
onSubmit onSubmit={lambda e: any -> None {...}} Form submission
onKeyPress onKeyPress={lambda e: any -> None {...}} Keyboard input

Advanced: Conditional Event Handlers#

def FilterButton(filterType: str, currentFilter: str, onFilterChange: any) -> any {
    isActive = currentFilter == filterType;

    return <button
        onClick={lambda -> None {
            onFilterChange(filterType);
        }}
        style={{
            "background": ("#7C3AED" if isActive else "#FFFFFF"),
            "color": ("#FFFFFF" if isActive else "#7C3AED")
        }}
    >
        {filterType}
    </button>;
}

6. Magic: No More Axios/Fetch!#

One of Jac's most powerful features is seamless backend communication without writing HTTP requests, fetch calls, or axios code.

The spawn Syntax#

Instead of writing:

// Traditional approach
const response = await fetch('/api/todos', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ text: 'New todo' })
});
const data = await response.json();

You simply write:

response = root spawn create_todo(text="New todo");

How spawn Works#

# Call a walker on a node
result = node_reference spawn walker_name(parameters);

Syntax: - node_reference - The node to spawn the walker on (commonly root for the root node, or a node ID) - spawn - The spawn keyword - walker_name - Name of the walker to execute - parameters - Parameters to pass to the walker (as function arguments)

Example from Todo App:

# Create a new todo (spawn on root node)
response = root spawn create_todo(text=text);

# Toggle todo status (spawn on specific todo node)
id spawn toggle_todo();

# Read all todos
todos = root spawn read_todos();

Backend Walkers#

Walkers are defined in the same app.jac file (outside the cl block):

# Node definition (data model)
node Todo {
    has text: str;
    has done: bool = False;
}

# Walker: Create a new todo
walker create_todo {
    has text: str;
    can create with `root entry {
        new_todo = here ++> Todo(text=self.text);
        report new_todo;
    }
}

# Walker: Toggle todo status
walker toggle_todo {
    can toggle with Todo entry {
        here.done = not here.done;
        report here;
    }
}

# Walker: Read all todos
walker read_todos {
    can read with `root entry {
        visit [-->(`?Todo)];
    }

    can report_todos with exit {
        report here;
    }
}

Complete Example: Creating a Todo#

Frontend (in cl block):

async def onAddTodo(e: any) -> None {
    e.preventDefault();
    text = document.getElementById("todo-input").value.trim();
    if not text { return; }

    # Call backend walker - no fetch/axios needed!
    response = root spawn create_todo(text=text);
    new_todo = response.reports[0][0];

    # Update frontend state
    s = todoState();
    newItem = {
        "id": new_todo._jac_id,
        "text": new_todo.text,
        "done": new_todo.done
    };
    setTodoState({"items": s.items.concat([newItem])});
}

Backend (outside cl block):

walker create_todo {
    has text: str;
    can create with `root entry {
        # 'text' comes from the walker parameter
        new_todo = here ++> Todo(text=self.text);
        report new_todo;
    }
}

Benefits of spawn#

No HTTP Configuration: No need to set up API endpoints, CORS, or request/response formats Type Safety: Jac handles serialization automatically Authentication: Built-in token management via jacLogin() / jacLogout() Error Handling: Exceptions are properly propagated Graph Operations: Direct access to graph-based data operations Less Code: Eliminates boilerplate HTTP client code Natural Syntax: Call walkers on nodes using intuitive syntax

Authentication Helpers#

Jac also provides built-in auth functions:

# Sign up
result = await jacSignup(username, password);
if result["success"] {
    navigate("/todos");
}

# Log in
success = await jacLogin(username, password);
if success {
    navigate("/todos");
}

# Log out
jacLogout();
navigate("/login");

# Check if logged in
if jacIsLoggedIn() {
    return <div>Welcome!</div>;
}

Complete Example: Todo App Structure#

Here's how all the pieces fit together:

# ============================================================================
# BACKEND: Data Models and Walkers
# ============================================================================
node Todo {
    has text: str;
    has done: bool = False;
}

walker create_todo {
    has text: str;
    can create with `root entry {
        new_todo = here ++> Todo(text=self.text);
        report new_todo;
    }
}

# ============================================================================
# FRONTEND: Components, State, and Events
# ============================================================================
cl import from react {useState}

cl {
    def app() -> any {
        let [todos, setTodos] = useState([]);
        let [input, setInput] = useState("");

        # Event Handler
        async def addTodo() -> None {
            if not input.trim() { return; }
            response = root spawn create_todo(text=input.trim());
            new_todo = response.reports[0][0];
            setTodos(todos.concat([new_todo]));
            setInput("");
        }

        return <div>
            <h2>My Todos</h2>
            <input
                value={input}
                onChange={lambda e: any -> None { setInput(e.target.value); }}
                onKeyPress={lambda e: any -> None {
                    if e.key == "Enter" { addTodo(); }
                }}
            />
            <button onClick={addTodo}>Add Todo</button>

            <div>
                {todos.map(lambda todo: any -> any {
                    return <div key={todo._jac_id}>
                        <span>{todo.text}</span>
                    </div>;
                })}
            </div>
        </div>;
    }
}

Running the Todo App#

To run this example:

# From the todo-app directory
jac serve app.jac

Then visit http://localhost:8000 in your browser.


Next Steps#

Ready to dive deeper? Explore these advanced topics:

  • Routing: Build multi-page apps with declarative routing (<Router>, <Routes>, <Route>)
  • Lifecycle Hooks: Use onMount() and React hooks for initialization logic
  • Advanced State: Manage complex state with React hooks and context
  • Imports: Import third-party libraries (React, Ant Design, Lodash), other Jac files, and JavaScript modules
  • Learn JAC: Explore Jac's graph-based data modeling

Examples#

Check out the examples/ directory for working applications:

  • basic: Simple counter app using React hooks
  • with-router: Multi-page app with declarative routing
  • little-x: Social media app with third-party libraries
  • todo-app: Full-featured todo app with authentication
  • basic-full-stack: Full-stack app with backend integration

Key Takeaways#

  1. Single Language: Write frontend and backend in Jac
  2. No HTTP Client: Use spawn syntax instead of fetch/axios
  3. Reactive State: React hooks manage UI updates automatically
  4. Component-Based: Build reusable UI components with JSX
  5. Type Safety: Jac provides type checking across frontend and backend
  6. Graph Database: Built-in graph data model eliminates need for SQL/NoSQL

Happy coding with Jac!