R

ReflexKR

Documentación de Reflex

El taller de componentes: los 100 componentes de Reflex

·

Resumen del curso

Diseño

·

Box

·

Flex

·

Stack

·

Grid

·

Container

·

Section

·

Center

·

Spacer

·

Separator

·

Card

·

Inset

·

Aspect Ratio

·

Fragment

Tipografía

·

Text

·

Heading

·

Link

·

Code

·

Markdown

·

Blockquote

·

Quote

·

Strong

·

Em

·

Kbd

Formularios

·

Button

·

Input

·

Text Area

·

Checkbox

·

Switch

·

Radio Group

·

Select

·

Slider

·

Form

·

Upload

Visualización de datos

·

Badge

·

Avatar

·

Icon

·

Callout

·

List

·

Data List

·

Code Block

·

Progress

·

Spinner

·

Scroll Area

·

Moment

Mostrar y ocultar

·

Tabs

·

Accordion

·

Segmented Control

Superposiciones

·

Tooltip

·

Popover

·

Hover Card

·

Dialog

·

Alert Dialog

·

Drawer

·

Dropdown Menu

·

Context Menu

·

Toast

Renderizado dinámico

·

Cond

·

Match

·

Foreach

·

Auto Scroll

Multimedia

·

Image

·

Audio

·

Video

Tablas y cuadrículas de datos

·

Table

·

Data Table

·

Data Editor

Gráficos

·

Area Chart

·

Bar Chart

·

Line Chart

·

Composed Chart

·

Pie Chart

·

Radar Chart

·

Radial Bar Chart

·

Scatter Chart

·

Funnel Chart

·

Treemap

·

Sankey Chart

·

Error Bar

Piezas de gráficos

·

Axis

·

Cartesian Grid

·

Chart Tooltip

·

Legend

·

Label

·

Reference

·

Brush

Otras bibliotecas de gráficos

·

Plotly

·

Pyplot

Elementos HTML

·

HTML

·

HTML Text

·

HTML Layout

·

HTML Forms

·

HTML Media

·

HTML Tables

·

SVG

Otras herramientas

·

Theme

·

Skeleton

·

Clipboard

·

Html Embed

·

Script

·

Memo

El taller de componentes: los 100 componentes de Reflex
Formularios

Form

rx.formrx.form.rootrx.form.fieldrx.form.labelrx.form.controlrx.form.messagerx.form.submit

Documentación oficial

Pedido del taller

Crea el formulario de pedidos de una cafetería. Debe recoger de una vez el nombre, el tamaño del vaso, la forma de recogida, las peticiones especiales y si es para regalo, y registrar el pedido.

rx.form es un marco que agrupa varias entradas y las envía de una vez. Dentro se colocan entradas como rx.input, rx.checkbox, rx.switch, rx.slider, rx.text_area, rx.radio_group o rx.select, y cada una lleva un name. Al enviarlo, el handler on_submit recibe un único dict {name: valor}.

El envío se produce al pulsar un botón type="submit" o al pulsar Enter en una casilla. Es imprescindible que haya un botón de envío (o una entrada type="submit"). Con reset_on_submit=True, las casillas se vacían tras el envío.

Results: {}

class FormBasicState(rx.State):
    form_data: dict = {}

    @rx.event
    def handle_submit(self, form_data: dict):
        self.form_data = form_data


rx.vstack(
    rx.form(
        rx.vstack(
            rx.input(placeholder="First Name", name="first_name"),
            rx.input(placeholder="Last Name", name="last_name"),
            rx.hstack(
                rx.checkbox("Checked", name="check"),
                rx.text(rx.flex(rx.switch(name="switch"), "Switched", spacing="2"), as_="label", size="2"),
                spacing="4",
            ),
            rx.button("Submit", type="submit"),
        ),
        on_submit=FormBasicState.handle_submit,
        reset_on_submit=True,
    ),
    rx.divider(),
    rx.text("Results: ", rx.code(FormBasicState.form_data.to_string())),
)

Los checkbox y switch desactivados no aparecen en el resultado; al activarlos entran como "on". El motivo se explica más abajo, en «name e id».

Prueba las propiedades

Antes de enviar el formulario, el navegador comprueba primero condiciones como required, type="email" o min·max. Si activas no_validate, se salta esa comprobación y los datos van directos al handler. Prueba a dejar una casilla vacía o a escribir abc o 500, y compara los dos casos.

Prueba las propiedades

Submitted: -

required

no_validate

rx.form(
    rx.vstack(
        rx.input(
            name="email",
            type="email",
            placeholder="Email",
            required=True,
        ),
        rx.input(name="age", type="number", min=1, max=120, placeholder="Age (1-120)"),
        rx.button("Submit", type="submit"),
        rx.text("Submitted: ", rx.code(FormLabState.result), size="2"),
    ),
    on_submit=FormLabState.handle_submit,
    no_validate=False,
)
Con no_validate=True se envía sin comprobación del navegador. En ese caso hay que validar en el servidor (en el handler).
PropiedadValoresPor defectoUso
on_submitEventoAl enviar. Pasa el dict de datos del formulario (o un TypedDict)
reset_on_submitTrue / FalseFalseVacía las casillas tras el envío
no_validateTrue / FalseFalseDesactiva la comprobación del navegador
as_childTrue / FalseFalseSe renderiza como el elemento hijo en lugar de como formulario
name, idCadenaNingunoNombre del formulario. Con id, entradas de otras partes pueden unirse con form="id"
action, method, enc_type, targetAtributos de formulario HTML. Reflex intercepta el envío, así que normalmente no se usan
handle_submit_unique_nameCadenaAutomáticoPara fijar tú el nombre de la función de envío de este formulario
on_clear_server_errorsEventoEn el formulario de bajo nivel, cuando se borran los errores del servidor

name e id

Los datos del formulario se reúnen desde dos sitios. Uno es name: los valores que envía el navegador. Las entradas que "solo tienen valor cuando están activadas", como checkbox, switch o radio, no incluyen su clave si no están activadas. Por eso se leen con form_data.get(...).

El otro es id. En 0.10, una entrada con id también añade ese id como clave, y su valor entra aunque no esté activada; un checkbox pasa a ser True/False. Los input·select·textarea básicos se recogen así automáticamente; un componente de entrada propio necesita _is_form_control = True en la clase. Eso sí, la documentación oficial anuncia que este mecanismo de id desaparecerá en 0.11. Lo más seguro de cara al futuro es usar solo name.

Results: {}

class FormNameIdState(rx.State):
    form_data: dict = {}

    @rx.event
    def handle_submit(self, form_data: dict):
        self.form_data = form_data


rx.form(
    rx.vstack(
        rx.checkbox("By name", name="by_name"),
        rx.checkbox("By id", id="form_by_id_box"),
        rx.button("Submit", type="submit"),
        rx.text("Results: ", rx.code(FormNameIdState.form_data.to_string())),
    ),
    on_submit=FormNameIdState.handle_submit,
)

Varias entradas con el mismo nombre

Si varias entradas comparten el mismo name, como un grupo de checkbox, en el dict solo queda el último valor. La documentación oficial (versión más reciente) declara el argumento del handler como rx.form.FormData y muestra cómo recibirlos todos con getlist. Pero en la 0.10 instalada no existe rx.form.FormData, así que el código de abajo queda solo como referencia.

class ToppingsState(rx.State):
    toppings: list[str] = []

    @rx.event
    def handle_submit(self, form_data: rx.form.FormData[str, str]):
        self.toppings = form_data.getlist("topping")


def toppings_form():
    return rx.form(
        rx.el.input(type="checkbox", name="topping", value="cheese"),
        rx.el.input(type="checkbox", name="topping", value="olives"),
        rx.button("Submit", type="submit"),
        on_submit=ToppingsState.handle_submit,
    )

En 0.10 basta con dar a cada entrada un nombre distinto (topping_cheese …) y reunirlas en el handler por el prefijo.

FORM_TOPPINGS = ["cheese", "olives", "mushrooms"]


class FormToppingState(rx.State):
    toppings: list[str] = []

    @rx.event
    def handle_submit(self, form_data: dict):
        self.toppings = [k.removeprefix("topping_") for k in form_data if k.startswith("topping_")]


rx.form(
    rx.vstack(
        rx.hstack(*[rx.checkbox(t, name="topping_" + t) for t in FORM_TOPPINGS], spacing="4"),
        rx.button("Submit", type="submit"),
        rx.hstack(rx.foreach(FormToppingState.toppings, lambda t: rx.badge(t))),
    ),
    on_submit=FormToppingState.handle_submit,
)

Validar los datos del formulario con TypedDict

Si declaras el argumento del handler con TypedDict en lugar de dict, el editor autocompleta las claves y Reflex lo comprueba al construir el formulario. Para cada clave obligatoria del TypedDict debe haber en el formulario una entrada con el mismo name (o id); si falta una sola, se produce un EventHandlerValueError antes de que arranque la app. Las claves opcionales se marcan con NotRequired.

from typing import TypedDict

from typing_extensions import NotRequired


class FormContactData(TypedDict):
    first_name: str
    last_name: str
    email: str
    message: NotRequired[str]


class FormTypedState(rx.State):
    greeting: str = ""

    @rx.event
    def handle_submit(self, form_data: FormContactData):
        self.greeting = f"Hi {form_data['first_name']} {form_data['last_name']} <{form_data['email']}>"


rx.vstack(
    rx.form(
        rx.vstack(
            rx.input(placeholder="First Name", name="first_name"),
            rx.input(placeholder="Last Name", name="last_name"),
            rx.input(placeholder="Email", name="email", type="email"),
            rx.text_area(placeholder="Message", name="message"),
            rx.button("Submit", type="submit"),
        ),
        on_submit=FormTypedState.handle_submit,
        reset_on_submit=True,
    ),
    rx.text(FormTypedState.greeting),
)

Si te olvidas de la entrada email, como abajo, el mensaje de error muestra las claves esperadas, las que faltan y las encontradas. La comprobación solo se hace cuando name es una cadena fija; se omite con nombres dinámicos creados con rx.foreach o cuando el formulario tiene id. En ejecución, form_data es siempre un dict normal.

class SignupForm(TypedDict):
    username: str
    email: str


class SignupState(rx.State):
    @rx.event
    def handle_submit(self, form_data: SignupForm): ...


# EventHandlerValueError: the form has no control named "email".
rx.form(
    rx.input(name="username"),
    rx.button("Submit", type="submit"),
    on_submit=SignupState.handle_submit,
)

La documentación oficial (versión más reciente) explica que un campo list[str] de un TypedDict reúne todos los valores con el mismo nombre y que un campo bool rellena con False los checkbox desactivados. Ese relleno no existe en 0.10. En 0.10, TypedDict solo hace la "comprobación al construir" de arriba, y los checkbox desactivados siguen sin incluir su clave, así que se tratan con NotRequired y .get().

class PreferencesForm(TypedDict):
    toppings: list[str]  # every checked "toppings" checkbox
    subscribe: bool  # False when the checkbox is unchecked
    notify: bool | None  # None when the switch is off

Formularios dinámicos

Si generas las entradas recorriendo una lista del State con rx.foreach, obtienes un formulario en el que el usuario puede añadir casillas. Las casillas añadidas también entran en los datos enviados. Aquí se limita a un máximo de 8. Hay dos formularios uno junto al otro: el de abajo solo se encarga de añadir casillas.

Results: {}

class FormDynamicState(rx.State):
    form_data: dict = {}
    form_fields: list[str] = ["first_name", "last_name", "email"]

    @rx.var
    def form_field_placeholders(self) -> list[str]:
        return [" ".join(w.capitalize() for w in field.split("_")) for field in self.form_fields]

    @rx.event
    def add_form_field(self, form_data: dict):
        new_field = form_data.get("new_field")
        if not new_field or len(self.form_fields) >= 8:
            return
        field_name = new_field.strip().lower().replace(" ", "_")
        if field_name and field_name not in self.form_fields:
            self.form_fields.append(field_name)

    @rx.event
    def handle_submit(self, form_data: dict):
        self.form_data = form_data


rx.vstack(
    rx.form(
        rx.vstack(
            rx.foreach(
                FormDynamicState.form_fields,
                lambda field, idx: rx.input(
                    placeholder=FormDynamicState.form_field_placeholders[idx],
                    name=field,
                ),
            ),
            rx.button("Submit", type="submit"),
        ),
        on_submit=FormDynamicState.handle_submit,
        reset_on_submit=True,
    ),
    rx.form(
        rx.hstack(
            rx.input(placeholder="New Field", name="new_field"),
            rx.button("+", type="submit", variant="soft"),
        ),
        on_submit=FormDynamicState.add_form_field,
        reset_on_submit=True,
    ),
    rx.divider(),
    rx.text("Results: ", rx.code(FormDynamicState.form_data.to_string())),
)

Formulario de bajo nivel: field · label · control · message · submit

La documentación oficial indica que el formulario de bajo nivel es experimental y recomienda el de alto nivel de arriba para servicios reales. Su ventaja es que conecta automáticamente los mensajes de validación junto a cada entrada. La estructura es esta:

form.root(
    form.field(
        form.label(...),
        form.control(...),
        form.message(...),
    ),
    form.submit(...),
)
  • rx.form.root: el formulario completo. on_submit y reset_on_submit funcionan igual que en el de alto nivel.
  • rx.form.field: una entrada junto con su etiqueta y sus mensajes. name pasa a ser el nombre de la entrada interior, y server_invalid marca un fallo de validación del servidor.
  • rx.form.label: la etiqueta. rx.form.control: el hueco de la entrada; para usar otro componente, envuélvelo con as_child=True. En 0.10 dentro de control solo cabe rx.input (checkbox, select, etc. van fuera del field o directamente bajo root).
  • rx.form.message: el mensaje de validación. match indica con qué fallo se muestra ("valueMissing", "typeMismatch", "patternMismatch", "tooShort", "rangeOverflow" …). force_match es para la validación del servidor.
  • rx.form.submit: el botón de envío. Para usar otro botón, as_child=True.

Validación del navegador (lado del cliente)

El navegador comprueba type="email" y required, y aparece el mensaje cuyo match coincide. Si la validación falla, el formulario no se envía aunque pulses el botón. En 0.10, rx.form.message tiene el color del texto fijado en blanco y no se ve sobre fondo claro, así que en el ejemplo se le da un color aparte.

Submitted:

class FormLowState(rx.State):
    email: str = ""

    @rx.event
    def handle_submit(self, form_data: dict):
        self.email = form_data.get("email", "")


rx.vstack(
    rx.form.root(
        rx.form.field(
            rx.flex(
                rx.form.label("Email"),
                rx.form.control(
                    rx.input(placeholder="Email Address", type="email", required=True),
                    as_child=True,
                ),
                rx.form.message("Please enter your email", match="valueMissing", color=rx.color("red", 11)),
                rx.form.message("Please enter a valid email", match="typeMismatch", color=rx.color("red", 11)),
                rx.form.submit(rx.button("Submit"), as_child=True),
                direction="column",
                spacing="2",
                align="stretch",
            ),
            name="email",
        ),
        on_submit=FormLowState.handle_submit,
        reset_on_submit=True,
        width="280px",
    ),
    rx.text("Submitted: ", rx.code(FormLowState.email), size="2"),
)

Validación del servidor

La validación del servidor se hace con una variable calculada del State. Se conecta una variable calculada bool que signifique "no válido" tanto al server_invalid del field como al force_match del message. force_match solo se usa cuando hay match (sin match, 0.10 descarta force_match, y un mensaje sin match se ve siempre). Por eso se pone match="badInput", que nunca se produce solo en una entrada de texto, y se deja que force_match decida si se muestra. Prueba a escribir admin abajo: aparece el mensaje y el botón queda bloqueado.

Signed up:

class FormServerCheckState(rx.State):
    username: str = ""
    submitted: str = ""

    @rx.event
    def set_username(self, value: str):
        self.username = value

    @rx.var
    def username_taken(self) -> bool:
        return self.username.strip().lower() in ["admin", "root", "reflex"]

    @rx.event
    def handle_submit(self, form_data: dict):
        if not self.username_taken:
            self.submitted = form_data.get("username", "")
            self.username = ""


rx.vstack(
    rx.form.root(
        rx.form.field(
            rx.flex(
                rx.form.label("Username (try 'admin')"),
                rx.form.control(
                    rx.input(
                        placeholder="Username",
                        value=FormServerCheckState.username,
                        on_change=FormServerCheckState.set_username,
                        required=True,
                    ),
                    as_child=True,
                ),
                rx.form.message(
                    "This name is taken",
                    match="badInput",
                    force_match=FormServerCheckState.username_taken,
                    color=rx.color("red", 11),
                ),
                rx.form.submit(rx.button("Sign up", disabled=FormServerCheckState.username_taken), as_child=True),
                direction="column",
                spacing="2",
                align="stretch",
            ),
            name="username",
            server_invalid=FormServerCheckState.username_taken,
        ),
        on_submit=FormServerCheckState.handle_submit,
        width="280px",
    ),
    rx.text("Signed up: ", rx.code(FormServerCheckState.submitted), size="2"),
)

Los archivos van aparte

Los archivos subidos no llegan a on_submit del formulario. Si hay un Upload dentro del formulario, los archivos se envían con rx.upload_files() desde un botón aparte type="button" o desde on_drop, y solo el resto de casillas se reciben con on_submit. Se explica en la página Upload.

Completar el pedido

Cada uno de los cinco tipos de entrada lleva su name y un solo handler los recibe. El select es required, así que no se envía hasta elegir algo, y como el checkbox desactivado no incluye su clave, se lee con .get("gift") y se convierte en "yes"/"no".

class FormOrderState(rx.State):
    last_order: dict = {}

    @rx.event
    def place_order(self, form_data: dict):
        self.last_order = {
            "name": form_data.get("name", ""),
            "size": form_data.get("size", ""),
            "pickup": form_data.get("pickup", ""),
            "gift": "yes" if form_data.get("gift") else "no",
            "note": form_data.get("note", "")[:100],
        }


rx.card(
    rx.form(
        rx.vstack(
            rx.input(name="name", placeholder="Your name", required=True),
            rx.select(["Small", "Medium", "Large"], name="size", placeholder="Cup size", required=True),
            rx.radio_group(["Store", "Delivery"], name="pickup", default_value="Store"),
            rx.text_area(name="note", placeholder="Note (optional)", max_length=100, rows="2"),
            rx.checkbox("Gift wrap", name="gift"),
            rx.button("Place order", type="submit"),
            rx.cond(
                FormOrderState.last_order,
                rx.code_block(FormOrderState.last_order.to_string(), language="json", width="100%"),
            ),
            align="stretch",
        ),
        on_submit=FormOrderState.place_order,
        reset_on_submit=True,
    ),
    width="100%",
    max_width="360px",
)

Notas del taller

  • rx.form(..., on_submit=handler) + un name para cada entrada de dentro. El handler recibe un dict {name: valor}.

  • Hace falta sí o sí un botón type="submit". Con reset_on_submit=True se vacía tras el envío.

  • Los checkbox, switch y radio desactivados no incluyen su clave. Léelos con form_data.get(). El mecanismo de id desaparece en 0.11.

  • Si declaras el argumento del handler como TypedDict, detecta las casillas que faltan al construir el formulario.

  • Para poner los mensajes de validación junto a cada entrada: field · control · message de bajo nivel (match, force_match + server_invalid).

← Anterior

Formularios · Slider

Siguiente →

Formularios · Upload

Recibe avisos de nuevas lecciones

Te avisaremos por correo cuando este curso tenga lecciones nuevas o se abra un curso de continuación.

Datos: tu correo electrónico · Finalidad: avisos de nuevas lecciones o de la publicación de este curso · Conservación: hasta que pidas que lo borremos. No lo usamos para nada más ni lo compartimos. Responde a cualquier aviso y lo borraremos enseguida.

Preguntas y respuestas

Pregunta lo que quieras sobre esta lección. Aquí se reúnen las preguntas de todas las versiones de idioma de esta página.

Se muestra como texto plano, sin enlaces ni formato.

R

ReflexKR

Aprende Python y Reflex haciendo. Todos los ejemplos se ejecutaron y verificaron en Reflex 0.10.0.

ReflexKR es gratis y sin banners publicitarios. Si te ayudó, puedes apoyarlo con un café. ☕ Invítanos un café

Aviso de afiliados
한국어English日本語简体中文EspañolDeutsch

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

Built with Reflex