Before any explanation, let's click something. Press the button below and the number goes up by 1. By the end of this lesson, you'll be able to explain exactly how these dozen or so lines of code work.
import reflex as rx
class CounterState(rx.State):
count: int = 0
@rx.event
def increment(self):
self.count += 1
def counter_increment():
return rx.hstack(
rx.heading(CounterState.count, as_="h2"),
rx.button("Increment", on_click=CounterState.increment),
)localhost:3000
Try itThe code has three parts.
| Part | Code | What it does |
|---|---|---|
| State | class CounterState(rx.State) | A box that holds the data that changes (count) |
| Event handler | def increment(self) | A function that changes the data |
| Event trigger | on_click=CounterState.increment | The link that says "call this function on click" |
When you click the button, increment runs on the server. Once count changes, Reflex automatically redraws just the parts of the page that use it. You never write a single line of code that updates the page directly.
Apps have data that changes over time: the number in a counter, whether you're logged in, the items in a shopping cart. In Reflex, you keep this kind of data in a State class.
rx.State and declare variables inside it. These variables are called Vars (state vars).count: int), and you can give it a default value (= 0).To use a Var in your UI, refer to it through the class name, like MyState.count. You can pass it as a child or as a prop value.
class MyState(rx.State):
count: int = 0
color: str = "red"
def counter():
return rx.hstack(
# The heading `color` prop is set to the `color` var in MyState.
rx.heading("Count: ", as_="h2", color=MyState.color),
# The `count` var in `MyState` is passed as a child to the heading component.
rx.heading(MyState.count, as_="h2"),
)localhost:3000
Live previewYou can use the same Var in as many components as you like. When its value changes, every place that uses it updates together.
Good to know — Only event handlers can change State. You can't assign a value directly in UI code, as in MyState.count = 5.
Methods on a State that change its data are called event handlers. Special props on components, such as on_click, are called event triggers. Connect a trigger to a handler, and your page responds to what the user does.
We recommend always putting @rx.event above your handlers. With this decorator, your editor and type checker can verify how many arguments a handler takes and of what types, so you catch mistakes early.
Like regular functions, handlers can take arguments. Let's build an "add 1" and an "add 5" button with a single handler.
class CounterState2(rx.State):
count: int = 0
@rx.event
def increment(self, amount: int):
self.count += amount
def counter_variable():
return rx.hstack(
rx.heading(CounterState2.count, as_="h2"),
rx.button("Increment by 1", on_click=lambda: CounterState2.increment(1)),
rx.button("Increment by 5", on_click=lambda: CounterState2.increment(5)),
)localhost:3000
Try itlambda, as in lambda: CounterState2.increment(1), to make the intent clear: "on click, call it with 1."on_click=CounterState2.increment(1) directly works exactly the same way. It doesn't run the handler right away; it only creates the event information "on click, run this with amount=1." The handler code runs on the server when the button is clicked.on_click doesn't pass any value to its handler, but some triggers do. on_blur fires when the input loses focus (when you click somewhere else or press Tab), and it passes the text you typed to the handler.
class TextState(rx.State):
text: str = ""
@rx.event
def update_text(self, new_text: str):
self.text = new_text
def text_input():
return rx.vstack(
rx.heading(TextState.text, as_="h2"),
rx.input(default_value=TextState.text, on_blur=TextState.update_text),
)localhost:3000
Try itGood to know — A handler must take as many arguments as its trigger passes. on_blur passes one value, so update_text takes one argument, new_text. If the numbers don't match, you get an error.
decrement handler to CounterState and make a button that lowers the number by 1.counter_variable that sets count back to 0. Could you do it with a handler that takes an argument?on_change instead of on_blur. Now the heading updates with every keystroke.ClassName.var_name.on_click doesn't run the handler right away.Get news about new lessons
We'll email you when this course gets new lessons or a follow-up course opens.
R
ReflexKR
Learn Python and Reflex by doing. Every example was run and verified on Reflex 0.10.0.
ReflexKR is free, with no banner ads. If it helped you, you can support it with a coffee. ☕ Buy us a coffee
Affiliate disclosure© 2026 ReflexKR · Since 2026 · CC BY-NC 4.0
Questions & answers
Ask anything about this lesson. Questions from every language version of this page appear together.