Keep State between calls
State holds the small values that a later server call needs. Citry restores those values for the next handler and keeps the browser's public State in sync.
Start with Server events if you have not called a Python handler from a component yet.
Add live search with one State binding
Use :c-query to connect a control to a public State field. Giving the attribute a handler name makes it two-way: the edit and the named call travel together, so the handler receives the latest State.
class LiveSearch(Component):
citry = citry_app
class Kwargs:
query: str = ""
class State(Kwargs):
pass
class Events:
def refresh(self, state):
return LiveSearch(query=state.query)
def template_data(self, kwargs, slots):
results = find_products(kwargs.query) if kwargs.query else []
return {"results": results}
template = """
<div>
<input
type="search"
placeholder="Search..."
:c-query.debounce.300ms="refresh"
>
<ul :class="{ searching: $loading() }">
<c-for each="item in results">
<li>{{ item.name }}</li>
</c-for>
</ul>
</div>
"""
The data flow is explicit:
querystarts as a component input.class State(Kwargs)makes that field part of the round trip.:c-querydisplays the State value in the input..debounce.300ms="refresh"waits for 300 ms of quiet, then sends the pending value and callsrefreshonce.refreshbuilds a newLiveSearchfrom the updated State.template_dataperforms the search for that new render.- The morph updates the result list while preserving the focused input and its caret.
Choose what survives in State
Kwargs describes one render. State describes only the small, JSON-serializable values needed by later calls. Keep database ids, short filters, page numbers, and editing flags there. Reload records and permission facts in the handler.
For a leaf component whose inputs are already the right State, inherit the schema:
class State(Kwargs):
pass
For richer inputs, declare a smaller State and derive it at render time:
class ProjectPanel(Component):
citry = citry_app
class Kwargs:
project: Project
show_archived: bool = False
class State:
project_id: int
show_archived: bool = False
def state_data(self, kwargs, slots):
return {
"project_id": kwargs.project.id,
"show_archived": kwargs.show_archived,
}
All State fields are public and writable by default. Narrow the client surface only when needed:
class State:
project_id: int
page: int = 1
can_delete: bool = False
_public = ("page", "can_delete")
_model = ("page",)
_public selects which fields client expressions may read. _model selects which public fields they may write through $state or a two-way binding. These lists are capability controls, not secrecy controls. Signed State travels through the browser and must be treated as client input. Server-held State keeps its values out of the token, but client-writable fields are still client input. See Security.
Build every event render from explicit inputs
A handler's render is a fresh component tree. It does not retain the original component's kwargs or slot fills.
The natural first attempt fails because self is the per-call Events object, not the rendered component:
class Events:
def refresh(self):
# Wrong: the original component inputs do not exist here.
return ProjectPanel(project=self.kwargs.project)
Carry the durable id in State, reload the authorized object, and pass every input explicitly:
class Events:
def refresh(self, state, request):
project = load_project_for_user(
project_id=state.project_id,
user=current_user(request),
)
return ProjectPanel(
project=project,
show_archived=state.show_archived,
)
This is the golden rule for Events rendering: if a later render needs a value, the handler must obtain it and pass it to the new tree.
Put data in State, js_data, or x-data deliberately
| Data kind | Put it in | Lifetime |
|---|---|---|
Small values a later server call needs, or values used by :c-* | State | Survives calls and self-renders; signed storage travels through the browser. |
| Large or derived read-only values for Component.js | js_data() | Recomputed for each render; may be shared by identical instances. |
| Client-only UI state such as an open accordion | x-data | Owned by Alpine in the current client region. |
js_data() is not reactive State. Do not mutate its object to publish local variables because identical instances may share the deduplicated value. In Component.js, write instance-local names to the callback's reactive scope object. See Client interactivity for the complete component-boundary and Component.js contract.