Python · Django · FastAPI · Litestar · HTMX
Build reactive UIs entirely in Python. No React, no Redux, no build step — HTMX handles the browser side; your components own the state.
capabilities
A complete server-component system with zero mandatory JavaScript and a pure-Python test story.
permission_classes = [IsAuthenticated] on the component class. Enforced by every view automatically. JSON 401/403, no redirects.get_optimistic_patch() to return an instant state delta. The client rolls back automatically on error.StreamingComponent.SlotComponent and CompositeComponent let you build complex pages from named, reusable pieces.ComponentTestCase lets you mount, dispatch events, and assert state — no HTTP, no browser, no running server.RateLimitMixin on any view. Sliding-window, per-user. Returns HTTP 429 with Retry-After header.CacheMixin caches render output per component + params. Event requests bypass the cache automatically.DjangoModelComponent binds state fields to ORM instances. save_instance() handles validation and transactions.why not React?
Building a real-time cart in React requires Redux, async thunks, a store provider, serialisation, and a bundler. In component-framework it's one Python class.
// 1. async thunk
const addToCart = createAsyncThunk(
'cart/addItem',
async (item, { dispatch }) => {
const r = await fetch('/api/cart/', {
method: 'POST',
body: JSON.stringify(item),
headers: { 'X-CSRFToken': getCsrf() },
});
return r.json();
}
);
// 2. reducer
const cartSlice = createSlice({
name: 'cart',
initialState: { items: [], total: '0.00', status: 'idle' },
extraReducers: (builder) => {
builder
.addCase(addToCart.pending, s => { s.status = 'loading' })
.addCase(addToCart.fulfilled, (s, a) => {
s.items = a.payload.items;
s.total = a.payload.total;
s.status = 'idle';
})
.addCase(addToCart.rejected, s => { s.status = 'error' });
},
});
// 3. optimistic hook (React 19)
function CartButton({ item }) {
const [optimisticItems, addOptimistic] =
useOptimistic(items, (state, newItem) =>
[...state, { ...newItem, pending: true }]
);
/* …plus store provider, types, bundler config */
}
@registry.register("cart")
class CartComponent(Component):
def on_add_item(self,
product_id: int,
size: str):
"""Add item or increment qty."""
for item in self.state["items"]:
if (item["product_id"] == product_id
and item["size"] == size):
item["qty"] += 1
self._recalculate_total()
return
self.state["items"].append({
"product_id": product_id,
"size": size,
"qty": 1,
})
self._recalculate_total()
def get_optimistic_patch(self,
event: str,
payload: dict) -> dict | None:
if event == "add_item":
return {"items":
self.state["items"]
+ [{**payload, "qty": 1}]}
return None
# That's it. pytest covers the rest.
| Concern | React + Redux | Component Framework |
|---|---|---|
| State location | useState / Redux store | self.state dict on server |
| Sync strategy | REST polling / WS events | Built-in server state |
| Optimistic UI | useOptimistic() + rollback | get_optimistic_patch() |
| Auth gating | Middleware + useSession() | permission_classes = [IsAuthenticated] |
| Build step | webpack / Vite required | None |
| JS payload | 150–400 KB gzipped | ~5 KB (htmx + client) |
| Test tooling | Jest, Storybook, RTL | pytest — pure Python |
quick start
uv pip install -e ".[django,websockets]"
Component, register it, implement lifecycle methods.@registry.register("counter") class Counter(Component): def mount(self): self.state["count"] = 0 def on_increment(self): self.state["count"] += 1
# Django path("components/<str:name>/", component_view) # FastAPI create_component_routes(app) # Litestar app = Litestar([component_endpoint])
<!-- counter.html -->
<div id="counter">
{{ state.count }}
<button
hx-post="/components/counter/"
hx-vals='{"event":"increment"}'
hx-target="#counter"
hx-swap="outerHTML">
+1
</button>
</div>
documentation