Skip to content

LocalState

Alberto Ruiz edited this page Apr 20, 2026 · 2 revisions

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.


Declaring state

Use use_state() inside a route handler to declare a local state value:

from HTeaLeaf.JS import use_state

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, and automatically injects the corresponding LocalState initializer into the page head:

let counter_a1b2 = new LocalState(0, "counter_a1b2");

No manual registration is needed — HTeaLeaf handles this via the on_render hook when the route executes.

use_state() must be called inside a route handler. Calling it at module level will not inject the initializer correctly.


Using state in the DOM

As content

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>{{counter_a1b2}}</div> → hydrated to <div>0</div>

Any subsequent call to .set() on the state will update all nodes that contain that placeholder automatically.

As an attribute

The same placeholder mechanism works inside attributes:

input_("number").attr(value=counter)
# renders as <input value="{{counter_a1b2}}"> → hydrated to <input value="0">

As an inline style

div("bar").style(width=counter)
# renders as <div style="width: {{counter_a1b2}}"> → hydrated to <div style="width: 0">

How hydration works

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.


Reading and updating state

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.

From an inline handler

button("+").attr(onclick=counter.set(counter.get() + 1))

From a @js function

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.


Summary

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

Clone this wiki locally