LazyFast is deeply integrated with FastAPI. A LazyFast application is built as a router that inherits from fastapi.APIRouter
. This allows you to easily integrate LazyFast into an existing FastAPI application, add a URL prefix, and configure dependencies at the router level, among other features. You can also distribute logic across multiple LazyFast routers, with each router managing its own session and state independently.
from fastapi import FastAPI
from lazyfast import LazyFastRouter
app = FastAPI()
root_router = LazyFastRouter(prefix="/")
login_router = LazyFastRouter(prefix="/login")
project_router = LazyFastRouter(prefix="/project")
app.include_router(root_router)
app.include_router(login_router)
app.include_router(project_router)
Every LazyFast tag and component operates within the context of a page. You can define a page using the @router.page
decorator. This decorator creates an endpoint that returns an HTML response along with LazyFast's JavaScript dependencies. The decorated function behaves like a regular FastAPI endpoint and supports all dependency injection features. However, you don’t need to specify a return value — LazyFast automatically builds and returns the final HTMLResponse
. Additionally, the page injects a hidden input
tag containing a csrf token.
from fastapi import FastAPI
from lazyfast import LazyFastRouter, tags
router = LazyFastRouter()
@router.page("/")
def index(query: str | None = None):
with tags.div():
tags.h1("Hello, World!")
tags.span(query or "No query provided")
app = FastAPI()
app.include_router(router)
This code means, that HTTP GET /?query=example
request will return:
<div>
<h1>Hello, World!</h1>
<span>example</span>
</div>
LazyFast key feature component interactivity and lazy loading work only within page:
from fastapi import FastAPI
from lazyfast import LazyFastRouter, Component, tags
router = LazyFastRouter()
@router.component()
class MyComponent(Component):
def view(self):
tags.div("My lazy loaded component")
@router.page("/")
def index():
MyComponent()
app = FastAPI()
app.include_router(router)
To pass your custom javascript or css in the page, you can use head_renderer
parameter in @router.page
decorator. This argument accepts function, wich will be called inside head
tag.
...
def cdn_libs_and_meta():
tags.meta(charset="utf-8")
tags.meta(name="viewport", content="width=device-width, initial-scale=1")
tags.link(
rel="stylesheet",
href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css"
)
tags.script(
src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"
)
@router.page("/", head_renderer=cdn_libs_and_meta)
def index():
with tags.div(class_="container"):
tags.span(f"This page is use bootstrap v5.1.3")
...
This example will add bootstrap
css and js to the page head.
You also can meta
tag inside the head_renderer
function.
In LazyFast tag is a simple wrapper for HTML tags:
from lazyfast import tags
with tags.div(class_="box", id="box"):
with tags.div(class_="content"):
tags.h1("Hello, World!")
with tags.div(class_="control"):
tags.button("Click me!", id="btn")
The code above is equivalent to:
<div class="box" id="box">
<div class="content">
<h1>Hello, World!</h1>
</div>
<div class="control">
<button id="btn">Click me!</button>
</div>
</div>
You also can import tags separately:
from lazyfast.tags import div, span, h1
The library supports nearly all standard HTML attributes, along with LazyFast-specific attributes for enhancing component interactivity. Under the hood, LazyFast uses Python dataclasses
to manage the logic behind tags and attributes.
Almost all tag attributes correspond to standard HTML attributes. However, some end with an underscore because their original form conflicts with Python reserved words or built-in methods. This is a map between original and lazyfast attributes:
{
"class_": "class",
"dir_": "dir",
"async_": "async",
"type_": "type",
"for_": "for",
"content_": "content",
}
Specific attributes are used in the many ways to make componentns interactive and responsive.
In order for a component to be reloaded by an HTML event in a tag, you can set the reload_on
tag parameter with a list of events:
tags.input(type_="text", reload_on=["change", "keydown"])
It is equivalent to:
<div
type="text"
onchange="reloadComponent(this, event)"
onkeydown="reloadComponent(this, event)"
></div>
The reloadComponent
function is an internal JavaScript function in the library, built on HTMX. It reloads a component and sends all input values to the server. You don't need to use this function directly in your code. Instead, we recommend using the reload_on
parameter with a list of events to trigger component reloads.
This attribute is used to show or hide a tag when component is in reloading process. Is is equivalent to htmx-indicator
class. The most frequent use case is to show a loader or spinner during reloading.
# Button tag already has onclick reloading by default,
# but we set this attribute explicitly to show the mechanism
tags.button("Run reloading", reload_on=["click"])
tags.span("Loading...", is_indicator=True)
Direct way to set the htmx-indicator
class:
tags.button("Run reloading", reload_on=["click"])
tags.span("Loading...", class_="htmx-indicator")
The span
tag is hidden when the component is not in the process of reloading. The reloading process occurs between the trigger of the reloadComponent
function and the server's response.
For the purpose of security, we use allow_unsafe_html
to allow use of unsafe HTML in the component. This attribute is False
by default. Use case example:
tags.div("Hello, <b>World!</b>", allow_unsafe_html=True)
is equivalent to:
<div>Hello, <b>World!</b></div>
The LazyFast library uses the htmx
library to handle client-server interactions, represented by the HTMX class, which implements a limited set of htmx features. In most common use cases, you won't need to interact directly with the HTMX class, as it is used internally by the library. However, for custom scenarios, you can manually use it by setting the hx
tag parameter with an instance of HTMX.
hx = HTMX(url="/some/endpoint", method="post", trigger="reveal")
tags.div(hx=hx)
Is equivalent to:
<div hx-post="/some/endpoint" hx-trigger="reveal"></div>
LaztFast implements data-*
attributes in tags via dataset
attribute with dictionary type:
dataset = {
"custom-attribute": "value",
"another-custom-attribute": "another-value",
}
tags.div(dataset=dataset)
Is equivalent to:
<div data-custom-attribute="value" data-another-custom-attribute="another-value"></div>
The LazyFast library lets you nest tags and components just like you would in HTML. This is achieved using Python's with
statement.
# field div inside box div
with tags.div(class_="box", id="box"):
with tags.div(class_="field"):
There are no limitations on the nesting level — you can nest as many tags and components as you like, similar to HTML.
If you want to nest plain text, use the content
tag parameter, which is the first optional positional argument.
⚠️ You can't usewith
andcontent
nesting at the same timewith tags.div("Hello world", class_="box", id="box"): tags.span()This code raises an error
If you want to use custom tags or if the LazyFast library doesn't support certain existing HTML tags, you can create your own by inheriting from the Tag
Tag class and using the @dataclass(slots=True)
decorator:
from dataclasses import dataclass
from lazyfast.tags import Tag
@dataclass(slots=True)
class MyCustomTag(Tag):
my_attribute: str | None = None
A component is a class that helps you create complex, interactive web interfaces with lazy loading. It enables code reuse by organizing your interface into logical blocks. Components can be nested within pages or even inside other components, allowing for flexible and scalable design.
Creating a component consists of declaring a class inherited from Component
and registering the component in the router with the appropriate decorator.
from fastapi import FastAPI
from lazyfast import LazyFastRouter, Component, tags
router = LazyFastRouter()
@router.component()
class MyComponent(Component):
def view(self):
tags.div("My lazy loaded component")
@router.page("/")
def index():
MyComponent()
app = FastAPI()
app.include_router(router)
When, you go to the /
page, you will see the following:
<div
class="__componentLoader__"
id="MyComponent"
hx-post="/"
hx-include="#csrf, #MyComponent"
trigger="load, MyComponent"
></div>
And after the component is loaded, you will see:
<div
class="__componentLoader__"
id="MyComponent"
hx-post="/"
hx-include="#csrf, #MyComponent"
hx-trigger="load, MyComponent"
>
<div>My lazy loaded component</div>
</div>
From an HTML perspective, creating a component involves generating a div
tag with specific HTMX attributes.
The view
endpoint, registered using the component decorator, is called from the client side. This endpoint fully corresponds to a FastAPI endpoint, supporting all dependency injection features and asynchronous functionality.
...
async def my_dependency() -> str:
return "My dependency"
@router.component()
class MyComponent(Component):
async def view(self, dep_result: str = Depends(my_dependency)):
tags.div(f"My lazy loaded component and dependency result: {dep_result}")
...
Paramters are pydantic model fields, which can be used to parameterize view logic or local state of the component.
...
@router.component()
class MyComponent(Component):
edit: bool = False
def view(self):
if self.edit:
tags.div("Edit mode")
else:
tags.div("Read mode")
@router.page("/")
def index():
MyComponent(edit=True)
...
Component reloading is a key feature for enabling interactive components in LazyFast. The concept is inspired by Streamlit, where the entire page is reloaded (or "rerun") after interactions with inputs, buttons, and other elements. Starting from Streamlit 1.33.0, fragments allow for partial page reruns. LazyFast’s component interactivity is similar to Streamlit's fragments but offers more flexibility with support for multiple nested components.
Reloading is triggered by various sources, such as tag interactions, changes in state fields, and self-reloading mechanisms. This process involves the client sending a POST
request to the component’s view endpoint on the server, receiving the newly rendered component in response. Each reload sends the current field values from the client to the server, enabling dynamic changes to the component’s appearance based on user input.
In LazyFast, the following tags are endowed with interactivity:
Tag | Default Event (on*) |
---|---|
input |
change |
button |
click |
select |
change |
radio |
click |
checkbox |
change |
textarea |
input |
Each interactive tag has a trigger
property that indicates whether it caused the reload. If the tag was not the source, trigger will be None
. If it was the source, trigger will contain the name of the JavaScript event that triggered the reload:
...
@router.component()
class MyComponent(Component):
def view(self):
with tags.div():
if event := tags.input(type="text").trigger:
tags.div(f"Text was changed, event: {event}")
btn = tags.button(type="button")
if trigger := btn.trigger:
tags.div(f"Button was clicked, event: {trigger}")
...
You can prevent reloading by these tags by wrapping them with form
tag:
@router.component()
class MyComponent(Component):
def view(self):
with tags.form():
tags.input(type="text")
btn = tags.button(type="button")
if trigger := btn.trigger:
tags.div(f"Button was clicked, event: {trigger}")
Form prevents all reloads except button
tags.
The component can subscribe to changes in specific state fields. When any of these fields are updated, the component automatically reloads. This is done via Server-Sent Events (SSE), eliminating the need for manual browser refreshes.
@router.component(id="MyComponent", reload_in=[State.my_field])
class MyComponent(Component):
def view(self, state: State = Depends(State.load)):
tags.span(state.my_field)
To enable state change listening, you need to specify the id
property in the component decorator.
The component can automatically reload itself via SSE (Server-Sent Events) without requiring a full page reload:
@router.component(id="MyComponent")
class MyComponent(Component):
async def view(self):
random_number = random.randint(0, 100)
tags.span(random_number)
await asyncio.sleep(1)
await self.reload()
This example reloads the component every second, displaying a random number. You must also specify the id
property in the component decorator. This sets the id
of the div
element that contains the component on the HTML page.
When a component sends a reload request to the server but hasn't yet received a response, it's important to show the user that the system is processing. To indicate this waiting state, you can use any tag with the is_indicator
attribute. This tag will remain hidden until the reload process begins, at which point it becomes visible.
If you prefer not to hide the tag but want to modify its appearance during the reload, you can use the htmx-indicator-class
field inside the tag's dataset attribute. This allows you to assign a CSS class to the tag during the reload process, changing its appearance without hiding it.
@router.component()
class MyComponent(Component):
def view(self):
dataset = {"htmx-indicator-class": "is-loading"}
tags.button("Run reloading", dataset=dataset)
tags.span("Loading...", is_indicator=True)
This example hides the span
element by default and shows it after the button
is clicked. Additionally, it adds the is-loading
class to the button
without hiding it.
In the past, we discussed handling reloads using the trigger
property, which requires waiting for the current tag structure to finish rendering. However, there are situations where we need to rebuild the entire component based on new data from the user. For these cases, LazyFast provides a ReloadRequest
object to facilitate this.
@router.component()
class MyComponent(Component):
def view(self, reload_request: ReloadRequest = Depends(ReloadRequest)):
if reload_request.trigger_id == "my-id":
tags.span("Reloaded by my-id")
tags.button("Run reloading", id="my-id")
ReloadRequest
object has following properties:
method
:GET
orPOST
trigger_id
:None
or trigger tag idtrigger_event
:None
or trigger javascript event (e.g.click
,change
, orinput
)data
: request form inputs. Is deprecated, useinputs
insteadinputs
:None
or request form inputs (values from all input tags within the component)session_id
: current unique session id
You can use TypedDict for the inputs
property:
from typing import TypedDict
class Inputs(TypedDict):
title: str
description: str
@router.component()
class MyComponent(Component):
def view(self, reload_request: ReloadReques[Inputs] = Depends(ReloadRequest)):
text = reload_request.inputs["title"]
description = reload_request.inputs["description"]
tags.input(type="text", name="title", value=title)
tags.input(name="description", value=description)
Using a
ReloadRequest
allows you to separate the display from the logic, which is especially important in the context of large components.
By default, the component replace old content with new content. We can change this behavior by using the swapping_method
parameter:
@router.component(swapping_method="append") # or "prepend"
class MyComponent(Component):
async def view(self):
tags.div("LazyFast")
await self.reload()
Every reloading will append new content after the last component innerHTML child. For prepend
method, it will prepend new content before the first component innerHTML child.
This is a result of the triple reload:
<div class="__componentLoaded__" hx-... >
<div>LazyFast</div>
<div>LazyFast</div>
<div>LazyFast</div>
</div>
The component register decorator lets you customize the div
container class and pass a preload_renderer
function. This function will be called before the component is rendered, which is helpful for scenarios like rendering skeletons.
@router.component(class_"my-class", preload_renderer=lambda: tags.div("Loading..."))
class MyComponent(Component):
def view(self):
tags.div("My component")
It equivalent to:
<div class="my-class __componentLoader__" hx-...>
<div>Loading...</div>
</div>
And after the component is rendered:
<div class="my-class __componentLoaded__" hx-...>
<div>My component</div>
</div>
State management in LazyFast enables components to interact with each other through a unified interface. The State
class, which is based on Pydantic, can have any number of fields. Components can subscribe to updates to these fields. Within LazyFastRouter
, only one state model can be used, and this state is stored in the user's session, ensuring isolation from other user sessions. Behind the scenes, the state interacts with components using an asynchronous queue and Server-Sent Events (SSE).
To define state model you need to inherit BaseState
class:
from lazyfast import LazyFastRouter, BaseState
class State(BaseState):
my_field: int = 0
router = LazyFastRouter(state_schema=State)
To work with the state you can use dependency State.load
:
...
@router.component()
class MyComponent(Component):
async def view(self, state: State = Depends(State.load)):
tags.span(state.my_field)
...
The component will be rendered with the current state value.
You also can use State.load
within @router.page
decorator and other FastAPI endpoints:
...
@router.page()
async def index(state: State = Depends(State.load)):
tags.span(state.my_field)
@app.get("/")
async def root(state: State = Depends(State.load)):
return {"message": state.my_field}
...
To ensure that updating a field triggers the reload of all dependent components, the commit mechanism must be used. This mechanism can be applied in several ways:
...
@router.component()
class MyComponent(Component):
async def view(self, state: State = Depends(State.load)):
tags.span(state.my_field)
# `with` operator
async with state:
state.my_field = 1
# or
# directly `open` and `commit`
state.open()
state.my_field = 1
await state.commit()
# or
# directly `open` and `commit` with try/finally
try:
state.open()
state.my_field = 1
finally:
await state.commit()
...
After committing the state using any of the three methods, the component will reload with the updated state value.
⚠️ LazyFast currently lacks a concurrent commit system, so simultaneous state updates from multiple parts of the code within a session (i.e., within a single client) at high frequency may lead to unpredictable behavior. I'm actively working on addressing this issue.
Coming soon...