-
Notifications
You must be signed in to change notification settings - Fork 1
LocalState
LocalState represents client-side reactive state β values that live in the page until it is reloaded, with no server round-trip involved.
When a LocalState value changes, every part of the DOM that references it updates automatically. This includes text content, attributes, and inline styles.
Use use_state() inside a route handler to declare a local state value:
from htealeaf import use_state
async def Home():
counter = use_state(0)use_state(initial_value) accepts any value as the initial state. It returns a JSCode object with a stable generated ID (derived from the call site, not the Python variable name), and automatically injects the corresponding LocalState initializer into the page head:
const localstate_a1b2c3d4e5f6 = new LocalState(0, "localstate_a1b2c3d4e5f6");No manual registration is needed β use_state() registers itself in the active render context, and the HTML renderer emits the initializer <script> into <head>.
use_state()must be called inside a route handler (sync or async) β calling it at module level will not inject the initializer.
Pass the LocalState object directly as the content of any element. HTeaLeaf renders a {{state_id}} placeholder into the HTML, which the frontend hydration step replaces with the current value on load:
div(counter) # renders as <div>{{localstate_a1b2c3d4e5f6}}</div> β hydrated to <div>0</div>Any subsequent call to .set() on the state will update all nodes that contain that placeholder automatically.
The same placeholder mechanism works inside attributes:
textInput().attr(value=counter)
# renders as <input value="{{localstate_a1b2c3d4e5f6}}"> β hydrated to <input value="0">This also works inside f-strings, since the placeholder is emitted by __format__:
div(f"{counter} clicks so far")div("bar").style(width=counter)
# renders as <div style="width: {{localstate_a1b2c3d4e5f6}}"> β hydrated to <div style="width: 0">When the page loads, the LocalState JS object scans the DOM for {{state_id}} tokens β both in text nodes and in element attributes β and replaces them with the current value. From that point on it tracks every node it touched, so future .set() calls know exactly what to update without a full re-render.
This is handled entirely client-side by the LocalState class in helper.js. The server only emits the placeholders and the initializer script.
Since LocalState lives entirely on the frontend, you interact with it through its JS interface β .get() and .set() β both from .attr() inline handlers and from @js functions.
button("+").attr(onclick=counter.set(counter.get() + 1))async def Home():
counter = use_state(0)
@js
def increment():
counter.set(counter.get() + 1)
button("+").attr(onclick=increment())Both patterns are equivalent. Use @js when the update logic is non-trivial.
| Behavior | |
|---|---|
| Declared with |
use_state(initial_value) inside a route handler |
| Type |
JSCode (backed by a LocalState JS object) |
| Scope | Client-side only, lives until page reload |
| DOM binding | Automatic via {{state_id}} placeholder hydration |
| Read | state.get() |
| Write | state.set(new_value) |
| Usable in | Element content, attributes, inline styles, @js functions |
π HTeaLeaf
State
Client-side