Skip to main content

State

State is reactive data that lives in the browser. When state changes, the component re-renders automatically — no full page reload.

Two ways to declare state


Style 1 — dict (simple)

The most common approach. Return a "state" dict with field names and their initial values:

def client(self):
return {
"state": {
"count": 0,
"name": "lua-spa",
"visible": True,
"price": 9.99,
}
}

Use state.<name> in the template:

<p>Count: {{ state.count }}</p>
<p l-if="state.visible">{{ state.name }}</p>

Style 2 — StateField list (typed, prop-seeded)

Use StateField when a state field should:

  • be initialised from a prop passed by the parent, and/or
  • be type-coerced (HTML attributes are always strings)
from lua_spa.types import StateField

def client(self):
return {
"state": [
StateField(name="count", from_prop="initialCount", default=0, cast="int"),
StateField(name="price", from_prop="startPrice", default=0.0, cast="float"),
StateField(name="label", from_prop="label", default="qty", cast="str"),
StateField(name="enabled", from_prop="enabled", default=True, cast="bool"),
]
}
<!-- Parent passes values as HTML attributes -->
<Counter initialCount="5" startPrice="19.99" enabled="true" />
cast valueConversion
"int"Number(value) → integer
"float"Number(value) → float
"str"stays as string
"bool"Boolean(value)
"raw"no conversion (default)

Style 2b — inner class list

The same as StateField, but using plain Python classes — useful if you prefer a class-oriented style:

def client(self):
class Count:
name = "count"
default = 0
from_prop = "initialCount"
cast = "int"

class Visible:
name = "visible"
default = True
cast = "bool"

return {
"state": [Count, Visible]
}

Comparing the two styles

Style 1 (dict)Style 2 (StateField / class)
Syntaxconciseverbose
Seed from prop
Type coercion
Best forstatic initial valuesdynamic, prop-driven state

Mutating state via actions

State is never set directly. Declare actions that describe mutations:

def client(self):
return {
"state": {"count": 0, "visible": True},
"actions": {
"increment": self.add("count"),
"decrement": self.sub("count"),
"reset": self.set("count", 0),
"flip": self.toggle("visible"),
}
}
MethodJS equivalent
self.add("x")x = x + 1
self.add("x", n)x = x + n
self.sub("x")x = x - 1
self.sub("x", n)x = x - n
self.set("x", v)x = v
self.toggle("x")x = !x

Seeding state from props

Already covered above in Style 2. Quick reference:

from lua_spa.types import StateField

def client(self):
return {
"state": [
StateField(name="count", from_prop="initialCount", default=0, cast="int"),
]
}
<Counter initialCount="5" />

State flow diagram

Working with state using Python methods

Besides the built-in self.add/sub/set/toggle, you can define Python methods on your component class and call them as actions. The tracing system records the operations they perform and compiles them to JavaScript automatically.

Example: multi-step action

class Cart(Component):
def client(self):
return {
"state": {
"quantity": 1,
"total": 0.0,
},
"actions": {
# Each action is a call to a Python method
"addOne": self.increase_quantity(),
"removeOne": self.decrease_quantity(),
"clear": self.reset_cart(),
},
}

def increase_quantity(self):
return self.add("quantity") # quantity += 1

def decrease_quantity(self):
return self.sub("quantity") # quantity -= 1

def reset_cart(self):
return [
self.set("quantity", 0),
self.set("total", 0.0),
]
<template>
<div class="cart">
<p>Quantity: {{ state.quantity }}</p>
<p>Total: {{ state.total }}</p>
<button @click="addOne">+</button>
<button @click="removeOne">-</button>
<button @click="clear">Clear</button>
</div>
</template>

Note: returning a list of operations from an action executes them all in sequence on the client.

Example: conditional state with context() + StateField

context() runs on the server and can use any Python logic — database queries, environment variables, complex calculations — to compute the initial values that seed client state.

import datetime

class Greeting(Component):
def context(self, props):
hour = datetime.datetime.now().hour
if hour < 12:
period = "morning"
elif hour < 18:
period = "afternoon"
else:
period = "evening"
# Expose to template as {{ py.period }}
return {"period": period}

def client(self):
return {
"state": {"liked": False},
"actions": {"like": self.toggle("liked")},
}
<template>
<div class="greeting">
<p>Good {{ py.period }}!</p>
<button @click="like">
{{ state.liked ? "❤️ Liked" : "🤍 Like" }}
</button>
</div>
</template>

context() (server) and client() (browser) are fully independent: