Keep values between calls
An event handler often needs to remember something from the last call: which page of results it showed, which filter is on, how many items it loaded. The handler does not keep anything between calls on its own. In this step, a counter survives from one call to the next, so Python can load a different batch of choices on each click.
Citry keeps such values in the component's State. Citry sends State to the browser with the rendered component, and the browser sends it back with the next call.
Replace the components
Replace components.py with:
from citry_setup import citry_app
from citry import Component
from citry.ext.events import actions
# New in this step: simulate two database query results.
# Based on value from State, we return one of these two
# batches of choices.
CHOICE_BATCHES = (
("Ocean", "Forest"),
("History", "Science"),
)
def load_choices_from_database(batch: int) -> list[str]:
choices = CHOICE_BATCHES[batch % len(CHOICE_BATCHES)]
return list(choices)
class ChoiceButton(Component):
citry = citry_app
class Kwargs:
pass
class Slots:
pass
template = """
<button
class="choice-button"
type="button"
@click="$emit('select')"
>
Choose
<span
class="choice-button__label"
v-text="label"
></span>
</button>
"""
js = """
$component({
props: {
label: { type: String, required: true },
},
emits: ['select'],
});
"""
class ChoicePicker(Component):
citry = citry_app
# New in this step: set the counter for the first render.
class Kwargs:
batches_loaded: int = 0
class Slots:
pass
# New in this step: carry the counter between Python calls.
class State:
batches_loaded: int = 0
class Events:
def load_choices(self, state):
# New in this step: read and advance the signed State.
# NOTE: State is passed to the event handler.
# You can mutate the State, and it will be
# signed and sent back to the client.
choices = load_choices_from_database(state.batches_loaded)
state.batches_loaded += 1
return actions.Dispatch(
"ChoicePicker:loaded",
{
"choices": choices,
},
)
template = """
<section class="choice-picker">
<button
type="button"
:disabled="$loading('load_choices')"
@c-click="load_choices"
>
Load choices
</button>
<span v-show="$loading('load_choices')">Loading...</span>
{# New in this step: show the counter from Python. #}
{# Python renders the starting count inside <output>. #}
{# `$state` is State in the browser; it updates after #}
{# each successful Python call, and Vue shows the new #}
{# value. #}
<p>
Sets loaded:
<output v-text="$state.batches_loaded">
{{ batches_loaded }}
</output>
</p>
<p v-show="choices.length === 0">
No choices loaded yet.
</p>
<div v-show="choices.length > 0">
<p>
Current choice:
<output
class="choice-picker__value"
v-text="choice"
></output>
</p>
<c-ChoiceButton
:label="choice"
@select="setNextChoice"
/>
</div>
</section>
"""
js = """
$component({
data() {
return { choices: [], choice: '' };
},
methods: {
loadChoices(newChoices) {
this.choices = newChoices;
this.choice = newChoices[0];
},
setNextChoice() {
const oldChoiceIndex = this.choices.indexOf(this.choice);
this.choice = this.choices[
(oldChoiceIndex + 1) % this.choices.length
];
},
},
onServerRender({ component, onEvent }) {
// Citry removes this listener before onServerRender
// runs again and when the component unmounts.
onEvent('ChoicePicker:loaded', (detail) => {
component.loadChoices(detail.choices);
});
},
});
"""
class TutorialPage(Component):
citry = citry_app
class Kwargs:
pass
class Slots:
pass
template = """
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Reading room</title>
</head>
<body>
<main>
<h1>Reading room</h1>
<c-ChoicePicker />
</main>
</body>
</html>
"""
Click βLoad choicesβ twice. The first call loads βOceanβ and βForestβ; the second loads βHistoryβ and βScience.β βSets loadedβ goes from one to two. Reloading the page starts over.
Declare State
class Kwargs:
batches_loaded: int = 0
class State:
batches_loaded: int = 0
Kwargs are the inputs for one render. State holds the values that event handlers receive on each call. On the first render, Citry fills each State field from the input with the same name. A field with no matching input uses its own default.
When every input should be kept, make State inherit from Kwargs instead:
class State(Kwargs):
pass
Keep the two classes separate when some inputs should not go to the browser. When names differ or a value must be computed, fill State with state_data(), as Event state shows.
Read and change State
class Events:
def load_choices(self, state):
choices = load_choices_from_database(state.batches_loaded)
state.batches_loaded += 1
return actions.Dispatch(
"ChoicePicker:loaded",
{"choices": choices},
)
The handler receives State through its state parameter. The first call gets zero, loads batch zero, and sets the value to one. Citry sends the changed State back with the response, so the next call gets one.
Show State on the page
The template reads State through $state:
<output v-text="$state.batches_loaded">{{ batches_loaded }}</output>
Python renders the starting value inside the tag. After each call that changes State, $state.batches_loaded updates, and Vue shows the new value.
The list of choices stays in ordinary Vue data, because Python does not need the browser's current choice on its next call.
Changing State from the browser
Browser code can also assign a whole field, as in $state.batches_loaded = 0. The page shows the new value at once, but the assignment does not send a request: Citry sends the value with the component's next event call (calls sent as GET requests are the exception). You cannot assign a nested value, or a field that State does not let the browser change. See $state for the full rules.
Keep secrets out of State
State is signed, not secret. Citry signs it with CITRY_SECRET so it can tell when someone has changed it, but anyone who opens the page can read the values. Signing also does not identify the user or grant permission.
Never put passwords or API keys in State, and check access inside each event handler. In production, every worker needs the same CITRY_SECRET.
Next steps
Next, handle and validate forms.