R

ReflexKR

Reflex Docs

Full-Stack Python Web Development with Reflex

·

Course overview

1

Meet Reflex and set up your environment

2

Drawing the screen with components

3

State and Vars: let data draw the screen

4

Events and forms: adding customers

5

Shaping data and charts

6

Wrap-up, debugging and next steps

·

Appendix

Full-Stack Python Web Development with Reflex
Lesson 4

Time 2:10–2:55

Events and forms: adding customers

By the end of this lesson, the "Add User" button will open a dialog, submitting the form will add a customer to the table, and the dialog will close on its own. This is the first, and most essential, step in making the page react to what the user does.

4-1. Event handlers (10 min)

An event handler is a method defined in State and decorated with @rx.event. Event handlers are the only thing that can change state.

class State(rx.State):
    users: list[User] = [...]

    @rx.event
    def add_user(self, form_data: dict):
        self.users.append(User(**form_data))
  • Handlers run as real Python on the server. Here, self.users is an actual list, not a Var, so you can freely use append, len, if, for and even third-party libraries.
  • When you change self.users, Reflex detects the change and sends it to the browser, and the table and badge re-render automatically. There's no code that updates the page directly.
  • User(**form_data) unpacks the dictionary into keyword arguments: {"name": "Kim", "email": "...", "gender": "Male"} → User(name="Kim", email="...", gender="Male").

Instructor note — Contrast this with the table from Lesson 3: "Inside a UI function it's a Var, so no len; inside a handler it's the real value, so len works." Once that contrast clicks, you understand half of Reflex.

4-2. Build the form (10 min)

rx.form(
    rx.flex(
        rx.input(placeholder="User Name", name="name", required=True),
        rx.input(placeholder="user@reflex.dev", name="email"),
        rx.select(
            ["Male", "Female"],
            placeholder="Select gender",
            default_value="Male",
            name="gender",
            custom_attrs={"aria-label": "Gender"},
        ),
        rx.flex(
            rx.dialog.close(
                rx.button("Cancel", type="button", variant="soft", color_scheme="gray"),
            ),
            rx.button("Submit", type="submit"),
            spacing="3",
            justify="end",
        ),
        direction="column",
        spacing="4",
    ),
    on_submit=State.add_user,
    reset_on_submit=False,
)
  • name= is the shared key. On submit, each input's name becomes a key in the form_data dictionary. They must exactly match the User field names (name, email, gender) for User(**form_data) to work.
  • Pass the handler to on_submit=State.add_user through the class, without calling it. The dictionary the form builds is passed in as form_data automatically.
  • required=True makes the browser block submission while the field is empty.
  • The Cancel button's type="button" matters. Buttons inside a form default to submit, so if you leave it out, clicking Cancel still adds a customer.
  • rx.flex is a box with display="flex". direction="column" stacks children vertically, and spacing sets the gap.
  • custom_attrs attaches HTML attributes that Reflex doesn't know about as-is. Here it's a label for screen readers.

4-3. Wrap it in a dialog (10 min)

def add_customer_button() -> rx.Component:
    return rx.dialog.root(
        rx.dialog.trigger(                       # the element that opens the dialog when clicked
            rx.button(
                rx.icon("plus", size=18),
                rx.text("Add User", size="2"),
            ),
        ),
        rx.dialog.content(                       # the dialog body
            rx.dialog.title("Add New User"),
            rx.dialog.description("Fill the form with the user's info"),
            rx.form(...),                         # the form from 4-2
            max_width="450px",
        ),
    )
  • rx.icon("plus") takes a Lucide icon name as-is.
  • Put the new function on the right side of the header as add_customer_button() (after the title box, inside the same parent).
  • Splitting UI into functions works just like splitting any other Python function. No special syntax needed.

Check your browser — A blue "+ Add User" button appears at the far right of the header.

localhost:3000

Try it

Customers

2

Your customer directory and audience breakdown.

NameEmailGender
Danilo Sousadanilo@example.comMale
Zahra Ambessazahra@example.comFemale
Step 4-3: click Add User and add a customer. It gets added, but the dialog doesn't close.

Click the button and the background dims as the dialog opens, with gender preselected as Male. Try entering values in the preview above and clicking Submit.

If you run the app at this point, you'll notice a problem: the customer gets added, but the dialog doesn't close. The browser is managing the dialog's open/closed state on its own, so the handler on the server has no way to tell it to close.

4-4. Control the dialog with state (10 min)

The fix is to move "is it open?" into State. This is called the controlled component pattern.

class State(rx.State):
    users: list[User] = [...]
    add_user_dialog_open: bool = False          # 1) keep the open state in State

    @rx.event
    def set_add_user_dialog_open(self, open_: bool):   # 2) define the setter yourself
        self.add_user_dialog_open = open_

    @rx.event
    def add_user(self, form_data: dict):
        self.users.append(User(**form_data))
        self.add_user_dialog_open = False      # 3) close on submit
rx.dialog.root(
    ...,
    open=State.add_user_dialog_open,                  # state decides whether it's open
    on_open_change=State.set_add_user_dialog_open,    # reflect user open/close in state
)
  • If you pass only open= and forget on_open_change, clicking the trigger button won't change the state, so the dialog never opens. The two always go together.
  • on_open_change passes True to the handler when the dialog opens and False when it closes (Cancel, ESC or clicking outside).
  • Reflex 0.10 has no automatic setters. Older tutorials often use State.set_<var_name> without defining it, but 0.10 doesn't generate those methods, so you have to define them yourself as shown above.
  • The parameter is named open_ by convention, to avoid shadowing Python's built-in open function.
  • Even with reset_on_submit=False, closing the dialog removes its content from the page and redraws it when reopened, so the inputs start out empty again.

4-5. The path of a single click (5 min)

  1. The user clicks Submit.
  2. The browser collects the inputs into {"name": ..., "email": ..., "gender": ...}.
  3. That value travels to the server over WebSocket.
  4. The server finds this user's State and runs add_user(form_data).
  5. Only the changed variables (users, add_user_dialog_open) come back to the browser.
  6. The table and badge re-render, and the dialog closes.

Check your browser — With 4-4 applied, clicking Submit closes the dialog, adds a 3rd row to the table and changes the badge to 3. If the dialog stays open, double-check open= and on_open_change= from 4-4.

localhost:3000

Try it

Customers

2

Your customer directory and audience breakdown.

NameEmailGender
Danilo Sousadanilo@example.comMale
Zahra Ambessazahra@example.comFemale
End of Lesson 4: submitting closes the dialog and the badge count goes up.

Exercises

  1. Add "Other" to the gender options.
  2. Add type="email" to the email input and see what happens when you submit an invalid address.
  3. In the handler, if the name is a duplicate, don't add it and return rx.toast.error("This customer already exists") instead. On success, try returning rx.toast.success(...) too.

Checkpoint

  • Adding a customer with Add User updates the table and badge.
  • The dialog closes automatically after you submit.
  • Clicking Cancel doesn't add a customer.
  • You can explain how name=, form_data and the User fields relate to each other.

← Previous

Lesson 3 · State and Vars: let data draw the screen

Next →

Lesson 5 · Shaping data and charts

Get news about new lessons

We'll email you when this course gets new lessons or a follow-up course opens.

We collect: your email address · Purpose: news about new lessons or the launch of this course · Kept until: you ask us to remove it. We never use it for anything else or share it. Reply to any notice email and we'll delete it right away.

Questions & answers

Ask anything about this lesson. Questions from every language version of this page appear together.

Shown as plain text, without links or formatting.

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
한국어English日本語简体中文EspañolDeutsch

© 2026 ReflexKR · Since 2026 · CC BY-NC 4.0

Built with Reflex