Reactive Components#
Keywords: live components, reactive ui, state management, websocket components, realtime ui, component class
Reactive (Live) Components allow you to build interactive UI elements that update in real-time without manual JavaScript. They are isomorphic: rendered on the server as HTML, but reactive via WebSockets.
Overview#
- No Manual JavaScript: Write your logic in Python; Asok handles the WebSocket synchronization.
- Stateful: Components maintain their internal properties (
self.count, etc.) across interactions. - Persistent Sessions: Components can leverage
self.sessionto persist state across page reloads. - Isomorphic: Initial render is SEO-friendly HTML; subsequent updates send only necessary HTML fragments.
- Alive Engine: Powered by the "Alive" JS runtime, providing focus preservation and automatic CSRF synchronization.
Basic Example#
1. The Component (src/components/Counter.py)#
# src/components/Counter.py
from asok import Component
from asok.component import exposed
class Counter(Component):
count = 0
@exposed
def increment(self):
self.count += 1
def render(self):
return self.html("counter.html")
2. The Template (src/components/counter.html)#
<div>
<h3>Count: {{ count }}</h3>
<button ws-click="increment">Add 1</button>
</div>
3. Usage in a Page#
{% extends "html/base.html" %}
{% block main %}
<h1>Welcome</h1>
{{ component('Counter', count=10) }}
{% endblock %}
Exposing Methods#
For security reasons, component methods must be explicitly marked with the @exposed decorator to be callable from the frontend via WebSocket.
from asok import Component
from asok.component import exposed
class Counter(Component):
count = 0
@exposed
def increment(self):
self.count += 1
@exposed
def decrement(self):
self.count -= 1
# This method is NOT exposed and cannot be called from the frontend
def _internal_calculation(self):
return self.count * 2
Only methods decorated with
@exposedcan be triggered viaws-click,ws-input, orws-submitdirectives. This prevents unauthorized access to internal component methods.
How it Works#
- Initial Render: The
{{ component(...) }}helper renders the component on the server and embeds a signed version of its state in adata-asok-stateattribute. - Connection: The browser's reactive engine connects to the WebSocket server (
/asok/live). - Synchronization: When a
ws-clickor other trigger is activated:- The browser sends the component's signed state and the method name to the server.
- The server reconstructs the component, validates the state hash, and executes the method.
- The component is re-rendered on the server.
- The server sends the new HTML back to the browser.
- The browser performs an efficient DOM swap and preserves focus/cursor position automatically.
The "Alive" Reactive Engine#
Asok includes the Alive engine, a lightweight (< 2KB) JavaScript runtime that handles the bridge between your DOM and the server. It handles:
- Automatic Connectivity: Reconnects WebSockets automatically if the connection is lost.
- Security: Synchronizes signed state and CSRF tokens for every interaction.
- UX Polish: Restores input focus and text selection after a component update, preventing "jumping" inputs during fast typing.
Islands Architecture & Selective Hydration#
Instead of scanning and hydrating the entire webpage, Asok supports Islands Architecture. This allows you to serve pure, lightweight HTML for static parts of your page and selectively hydrate interactive elements (islands) when specific conditions are met.
Hydration Attributes#
You can control when and how a component is hydrated by using client: attributes:
| Attribute | Trigger Strategy | Description |
|---|---|---|
client:load | Immediate | Hydrates the component as soon as the page loads. |
client:visible | Intersection | Hydrates the component only when it enters the viewport (using IntersectionObserver). Excellent for comments, interactive widgets in the page body, etc. |
client:idle | Browser Idle | Hydrates when the browser main thread is idle (using requestIdleCallback). Ideal for lower-priority or secondary widgets. |
Usage Example#
<!-- Hydrates immediately -->
{{ component('HeaderCart', client='load') }}
<!-- Remains static until scrolled into view -->
{{ component('CommentsSection', client='visible') }}
<!-- Hydrates during browser idle time -->
{{ component('NewsletterSignup', client='idle') }}
When using Islands, pages without any active directives or interactive components will send zero JavaScript payload to the client. If components are present, they are only hydrated when their respective strategy is triggered, leaving the surrounding static HTML untouched and fast.
Real-time Model Subscriptions (data-subscribe)#
Beyond stateful components, Asok allows any HTML element to become "real-time" by subscribing to database changes.
When a model is created, updated, or deleted, Asok automatically broadcasts an event over WebSockets. Any element with a matching data-subscribe attribute will automatically refresh itself.
<!-- Automatically refresh this block when Order #123 is updated -->
<div data-block="order-status" data-subscribe="model:Order:123">
Status: {{ order.status }}
</div>
<!-- Refresh when ANY product is updated -->
<div data-block="product-list" data-subscribe="model:Product">
...
</div>
How it works: 1. The client joins a WebSocket "room" based on the data-subscribe value. 2. When model.save() or model.delete() is called on the server, an event is emitted. 3. The WebSocket server relays this to all subscribed clients. 4. The client-side runtime automatically triggers a background fetch for that specific data-block and swaps the HTML.
Automatic State Persistence#
Asok components are designed to feel like SPA components. On every interaction (e.g., clicking a ws-click button), the framework automatically persists the component's state to the user's session store.
Benefits#
- Refresh Protection: If the user reloads the page, the component restores its exact state from the session.
- Navigation Stability: Navigating between pages or using the browser's back/forward buttons preserves the state of components.
- Development Stability: In development mode (
DEBUG=true), Asok uses a deterministic development key to ensure state survives server restarts and hot-reloads.
Persistent Sessions#
Components have a self.session property that behaves like request.session. This is useful for explicit data that must be shared across the entire application.
def increment(self):
self.session["pcount"] = self.session.get("pcount", 0) + 1
# Required to persist changes back to the store
self.session.modified = True
Always set
self.session.modified = Truewhen updating session data within a component method to ensure the changes are saved to the persistent store.
Accessing the current request inside a component#
Live components run server-side during WebSocket re-renders. You can access the active request (path, user, session, language…) via current_request:
from asok import Component, current_request
from asok.component import exposed
class UserCard(Component):
name = ""
def render(self):
# current_request is available during every server-side render
is_admin = current_request.user and current_request.user.is_admin
lang = current_request.lang
greeting = "Hello" if lang == "en" else "Bonjour"
return (
f"<div>"
f" <p>{greeting}, {self.name}!</p>"
f" {'<span class=\"badge\">Admin</span>' if is_admin else ''}"
f"</div>"
)
current_requestinside a component'srender()method reflects the original HTTP request that loaded the page — specifically the path, user, and session stored in the WebSocket connection object. It behaves identically to therequestparameter you would have in a normal view.
Was this page helpful?