# Ripple UI: Building Reactive Frontends for Python Backends

JavaScript frameworks have become essential for modern web development, with React dominating the landscape for years. Its component-based architecture and virtual DOM power countless applications, but this power comes with complexity, boilerplate code, and a steep learning curve. 

**Ripple UI offers a different approach by providing fine-grained reactivity**, a clean syntax without JSX, and impressive performance, particularly for developers working with Python backends like FastAPI, Flask, and Django.

This guide explores Ripple's core concepts through a complete demo application, examining how it manages state with the `track()` function, **builds dynamic components without JSX**, and creates seamless communication between a Ripple frontend and a Python FastAPI backend.

<iframe width="100%" height="315" src="https://www.youtube.com/embed/ANPbRu5cWFQ" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>


## What is Ripple UI and why should you care?

Many developers, especially those from backend-heavy languages, find the modern JavaScript ecosystem overwhelming with its build tools, state management libraries, and abstract concepts like the virtual DOM.

### The problem with traditional frameworks

React introduces several layers of abstraction. The virtual DOM efficiently updates the user interface by re-evaluating the entire component tree to find differences, but this can lead to "re-render bloat." Developers must master complex hooks like `useEffect`, `useCallback`, and `useMemo` with their tricky dependency arrays to optimize performance. JSX, while popular, mixes HTML-like syntax directly into JavaScript, which isn't always the cleanest approach.

### Ripple's core principles

Ripple builds on simplicity and performance, drawing inspiration from frameworks like Svelte and SolidJS while maintaining a familiar component structure.

![Ripple in Action demo application showcasing various reactive components](https://imagedelivery.net/xZXo0QFi-1_4Zimer-T0XQ/f7639f5a-82d6-4f58-a982-66024b798e00/orig =2560x1440)

**Fine-grained reactivity** is Ripple's distinguishing feature. Instead of re-rendering entire components, Ripple tracks exactly which parts of the DOM depend on specific pieces of state. When a state variable changes, only the text nodes or attributes that use it are updated. This surgical precision delivers incredible speed and efficiency compared to the broader virtual DOM approach.

Ripple is a compiled framework that generates highly optimized vanilla JavaScript that directly manipulates the DOM. Its templates use standard string literals, allowing you to write what feels like HTML with embedded logic without special syntax. Control flow uses simple JavaScript `if` statements and `for` loops, making code more readable.

The combination of a compiler and fine-grained reactivity means Ripple applications are lightweight and fast. The framework has a minimal footprint, and the resulting code is stripped of unnecessary overhead.

### Integration with Python backends

Python developers building modern UIs have traditionally faced a choice between learning heavy JavaScript frameworks or settling for less dynamic server-side templating engines like Jinja2. Ripple bridges this gap as a simple, self-contained frontend library that any Python backend can easily serve. You can build API routes and business logic in FastAPI or Django while Ripple handles the user interface, communicating through standard `fetch` calls. This creates a clean, powerful, and maintainable full-stack architecture.

## Project structure and setup

A typical Ripple and Python project consists of a FastAPI backend serving as the API and a Ripple UI frontend that consumes it.

### The Python backend with FastAPI

The backend is a standard Python application built with FastAPI. The primary file defines the server and its API endpoints:

![The /api/message endpoint definition in main.py](https://imagedelivery.net/xZXo0QFi-1_4Zimer-T0XQ/c5ed634d-b590-4248-96f3-3c4f93f21500/lg1x =2560x1440)

```python
[label main.py]
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
import logging

app = FastAPI(title="Ripple - Logging Demo API")

logger = logging.getLogger("uvicorn.error")

@app.get("/api/message")
async def api_message(request: Request) -> JSONResponse:
    return JSONResponse({
        "message": "Blending Python w/ Ripple!",
        "backend": "FastAPI",
        "frontend": "Ripple UI"
    })

@app.post("/api/signup")
async def process_user_signup(request: Request) -> JSONResponse:
    user_id = "user_183da42b"
    logger.info(f"User signup successful: {user_id}")
    return JSONResponse({"status": "Signup successful!", "user": user_id})

@app.get("/api/logs")
async def get_logs(request: Request, limit: int = 50) -> JSONResponse:
    logs = [
        {"level": "INFO", "event": "request_started", "path": "/api/logs"},
        {"level": "INFO", "event": "request_completed", "path": "/api/message"}
    ]
    return JSONResponse({"logs": logs})
```

These routes allow the Ripple frontend to fetch data, trigger actions, and retrieve logs.

### The Ripple UI frontend

The frontend code resides in a separate `frontend` directory, using Vite as a development server for features like hot module reloading and API request proxying to the Python backend.

The main application logic lives in `frontend/src/App.ripple.ts`, which defines a class-based component containing all state, methods, and rendering logic for the user interface.

## Understanding reactivity with `track()`

Ripple's reactivity system centers on the `track()` function, which replaces the entire suite of state and effect hooks found in React.

### How `track()` works

To make data reactive, you wrap its initial value in the `track()` function, creating a "signal" that holds a value and notifies subscribers when that value changes:

![Multiple state variables declared using track() in App.ripple.ts](https://imagedelivery.net/xZXo0QFi-1_4Zimer-T0XQ/6e0f5d05-7814-4c97-286f-c048bfc61700/md2x =2560x1440)

```typescript
[label App.ripple.ts]
import { Component, track } from "./ripple";

interface Todo {
  id: number;
  text: string;
}

export class App implements Component {
  private countState = track(0);
  private todosState = track<Todo[]>([]);
  private todoInputState = track("");
  private messageState = track("");
  private logsState = track<any[]>([]);
  private loadingState = track(false);
}
```

When you call `track()`, it returns a tuple containing a getter function and a setter function. To read the value, you call the getter (e.g., `this.countState[0]()`). When Ripple's renderer executes template logic and calls this getter, it automatically creates a subscription, recording that this specific DOM part depends on this specific state.

To update the value, you call the setter with the new value (e.g., `this.countState[1](newValue)`). The setter notifies all subscribers, triggering them to update with the new value.

This mechanism enables fine-grained reactivity without a virtual DOM diff or manual dependency tracking. The connection between state and DOM is direct and automatic, eliminating the need for React's `useEffect` hook.

### Building a reactive counter

Here's how a counter component demonstrates this reactivity:

```typescript
[label App.ripple.ts]
private getCount() { return this.countState[0](); }
private setCount(value: number) { this.countState[1](value); }

private increment = () => {
    this.setCount(this.getCount() + 1);
}

private reset = () => {
    this.setCount(0);
}

private renderCounterCard(): string {
[highlight]    const count = this.getCount();
    const double = count * 2;[/highlight]

    return `
        <div class="card">
            <h2>Reactive Counter</h2>
            <p>track() replaces the whole useEffect from React</p>
[highlight]            <div class="counter-display">${count}</div>
            <div class="derived-value">Double: ${double}</div>[/highlight]
            <div style="text-align: center">
                <button id="increment-btn">Increment</button>
                <button id="reset-btn">Reset</button>
            </div>
        </div>
    `;
}
```

When the "Increment" button is clicked, the `increment` method updates `countState`. Because the `counter-display` and `derived-value` divs were rendered using `getCount()`, Ripple knows they're subscribed to `countState` and immediately updates only those two text pieces in the DOM without touching anything else.

## Building dynamic UIs without JSX

Ripple deliberately moves away from JSX, resulting in a templating system that's both powerful and simple.

### Control flow with plain JavaScript

![Visual emphasizing Ripple's approach without .map() and JSX](https://imagedelivery.net/xZXo0QFi-1_4Zimer-T0XQ/483ce3a0-cd46-403c-3a16-44b21e9cc200/md1x =2560x1440)

Instead of learning special syntax for loops and conditionals, you use JavaScript directly. Conditionals use `if` statements, and lists use `for...of` loops. This keeps rendering logic clean and easy to follow.

### The to-do list component

Here's how a to-do list component handles dynamic rendering:

```typescript
[label App.ripple.ts]
private todosState = track<Todo[]>([]);
private todoInputState = track('');

private renderTodosCard(): string {
    const todos = this.getTodos();
    const todoInputValue = this.getTodoInput();

    let todosHtml = '';
[highlight]    if (todos.length === 0) {
        todosHtml = '<p>No todos yet. Add one above!</p>';
    } else {
        for (const todo of todos) {
            todosHtml += `
                <div class="todo-item">
                    <span>${this.escapeHtml(todo.text)}</span>
                    <button class="remove-todo-btn" data-id="${todo.id}">Remove</button>
                </div>
            `;
        }
    }[/highlight]

    return `
        <div class="card">
            <h2>Todo List</h2>
            <p>Say goodbye to all that JSX Syntax</p>
            <input type="text" id="todo-input" value="${this.escapeHtml(todoInputValue)}" placeholder="Add a todo..." />
            <button id="add-todo-btn">Add Todo</button>
            <button id="clear-todos-btn">Clear All</button>
            <div class="todo-list">${todosHtml}</div>
        </div>
    `;
}
```

Adding, removing, and clearing to-dos simply involves updating the `todosState` array using its setter. Ripple's reactive system automatically and efficiently re-renders just the `todo-list` section whenever the array changes.

## Connecting Ripple UI to Python

Ripple makes frontend and backend communication straightforward through standard API calls.

### API calls and development proxy

The Ripple app runs in the browser while the Python app runs as a server. They communicate using the `fetch` API. During development, they run on different ports (Ripple on `5173`, FastAPI on `8000`). Vite's development server acts as a proxy, forwarding any request starting with `/api` to the Python server at `http://localhost:8000` to avoid CORS issues.

### Fetching data from FastAPI

When a user clicks the "Fetch from Python API" button, the following method executes:

```typescript
[label App.ripple.ts]
private fetchFromApi = async () => {
    this.setLoading(true);
    try {
[highlight]        const response = await fetch('/api/message');
        const data = await response.json();
        this.setMessage(data.message);[/highlight]
    } catch (error) {
        console.error('Failed to fetch from API:', error);
    } finally {
        this.setLoading(false);
    }
}
```

The fetched message is stored in `messageState` using its setter, and the UI, which subscribes to `messageState`, instantly updates to display the message from the Python backend.

![UI showing successful response from the backend: "Response: Blending Python w/ Ripple!"](https://imagedelivery.net/xZXo0QFi-1_4Zimer-T0XQ/2a01ec22-175a-450c-8dcd-33f31bb63500/md2x =2560x1440)

### Live log viewer implementation

The live log viewer demonstrates a complete round-trip interaction. When a user clicks "Create User Signup," Ripple sends a `POST` request to the `/api/signup` endpoint. The FastAPI backend executes the corresponding function, creates a log entry, and returns a success response.

When the user clicks "Refresh Logs," Ripple sends a `GET` request to `/api/logs`:

```typescript
[label App.ripple.ts]
private refreshLogs = async () => {
    this.setLoading(true);
    try {
[highlight]        const response = await fetch('/api/logs?limit=50');
        const data = await response.json();
        this.setLogs(data.logs);[/highlight]
    } catch (error) {
        console.error('Failed to fetch logs:', error);
    } finally {
        this.setLoading(false);
    }
}
```

FastAPI reads the latest logs and sends them back as a JSON array. Ripple updates its `logsState` tracked variable with this array, and the UI automatically re-renders the "Live Logs" panel to display the new entries.

![Live Logs component displaying structured log entries from the Python backend](https://imagedelivery.net/xZXo0QFi-1_4Zimer-T0XQ/a23ac398-168e-41bf-07f9-70bf39d62900/lg2x =2560x1440)

This entire interaction is managed with simple functions, tracked state, and standard API calls, showcasing clean separation of concerns.

## Ripple UI vs. the competition

![Framework comparison chart showing how Ripple UI compares to React, Tailwind, Bootstrap, and Shadcn](https://imagedelivery.net/xZXo0QFi-1_4Zimer-T0XQ/1077cd01-88b0-4e27-8fcc-07c3117f3b00/orig =2560x1440)

Ripple doesn't aim to replace React for massive, enterprise-scale applications but offers a compelling alternative with different priorities:

| FRAMEWORK        | NEED JS?   | CURVE  | SPEED           | DESIGN         |
| :--------------- | :--------- | :----- | :-------------- | :------------- |
| **RIPPLE UI**    | Minimal    | Easy   | ⚡️⚡️⚡️⚡️⚡️ | Clean + Modern |
| **REACT**        | Heavy      | Medium | ⚡️⚡️⚡️⚡️    | No Default     |
| **TAILWIND CSS** | None       | Medium | ⚡️⚡️⚡️⚡️    | No Components  |
| **BOOTSTRAP**    | Low        | Easy   | ⚡️⚡️⚡️       | Generic Look   |
| **SHADCN**       | React Only | Hard   | ⚡️⚡️⚡️       | Setup Heavy    |

Unlike React which requires a large runtime, Ripple compiles away, leaving minimal JavaScript needed to manage reactivity. The API is small and the concepts are intuitive, especially for those who prefer plain HTML and JavaScript over JSX and hooks. Its compiled, fine-grained reactivity model is one of the fastest approaches possible for web UIs. Ripple works with any CSS framework but encourages a clean, component-based design philosophy.

## Final thoughts

Ripple UI is a new way to build frontends that focuses on being simple, fast, and easy for developers to use. Its `track()` function gives you a clear and efficient way to watch for changes in data and update the UI.

For Python developers, **Ripple makes it possible to build fast, interactive, and good-looking user interfaces without having to fully learn React**. Python can stay in charge of the backend, while Ripple takes care of the frontend, which makes full-stack development feel clean and organized.

Ripple UI won’t replace React in every project, but it has found an important place in the frontend world. It shows that building for the web can be quick, enjoyable, and not overly complicated. If you want a framework that stays out of your way and helps you move fast with clear code, Ripple is worth a look.
