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

Upload

rx.uploadrx.upload.root

Documentación oficial

Pedido del taller

Añade a la pantalla de solicitud de reembolso una zona para adjuntar fotos del recibo. Hasta 3 imágenes, que se añadan nada más soltarlas y muestren su nombre y tamaño.

rx.upload es una zona (dropzone) donde se sueltan archivos arrastrándolos o se eligen haciendo clic. Los archivos elegidos se quedan primero en el navegador y solo van al servidor cuando pasas rx.upload_files() a un event handler. El handler del servidor recibe la lista de archivos y los lee o los guarda.

Los ejemplos de este curso se ejecutan en un servidor compartido, así que no guardan los archivos: solo muestran su nombre y tamaño. El código para guardarlos se muestra aparte, solo como código.

rx.upload(
    rx.text("Drop files here or click to select"),
    id="upload_basic",
    border="2px dashed var(--gray-7)",
    padding="2em",
)

Prueba las propiedades

Estas propiedades deciden cómo se comporta la dropzone. Elige algunos archivos y luego desactiva multiple o activa no_click. Los nombres de los archivos elegidos aparecen abajo como badges.

Prueba las propiedades

label

multiple

disabled

no_click

no_drag

no_keyboard

rx.vstack(rx.upload(rx.text("Drop files here or click to select"), id="upload_lab", multiple=True, disabled=False, no_click=False, no_drag=False, no_keyboard=False, border="2px dashed var(--gray-7)", padding="2em"), rx.foreach(rx.selected_files("upload_lab"), lambda f: rx.badge(f, variant="soft")))
no_click impide elegir haciendo clic, no_drag impide arrastrar y soltar, y no_keyboard impide abrirla con Espacio·Enter. En 0.10 el valor por defecto de multiple es True.
PropiedadValoresPor defectoUso
idCadena (fija)"default"Nombre de esta dropzone. Los Var y eventos especiales de abajo usan el mismo id
accept{MIME: [extensiones...]}Todos los archivosTipos de archivo que se aceptan
multipleTrue / FalseTrueElegir varios
max_filesEnteroSin límiteNúmero máximo de archivos
max_size, min_sizeBytesSin límiteTamaño máximo y mínimo de cada archivo
disabledTrue / FalseFalseDesactivar la dropzone
no_click, no_drag, no_keyboardTrue / FalseFalseImpedir clic / arrastrar y soltar / abrir con el teclado
drag_active_styleDict de estilosNingunoEstilo mientras se arrastra un archivo encima (0.10)
on_dropEventoGuarda los archivos elegidosSe ejecuta en cuanto se sueltan o eligen archivos
on_drop_rejectedEventoToast de errorCuando entra un archivo que no cumple las condiciones

Ver los archivos elegidos: selected_files

Al elegir archivos, entran en una lista del navegador. Con el Var especial rx.selected_files(id) se muestra en pantalla la lista de nombres, y con el evento especial rx.clear_selected_files(id) se vacía. Todavía no se ha enviado nada al servidor.

id es la clave que une la dropzone con los Var y eventos especiales, así que debe ser el mismo valor en todos. Escríbelo como cadena o constante de módulo; no se puede usar una variable de State. El botón "Select File" de dentro de la dropzone es solo decorativo: hagas clic donde hagas clic dentro de la dropzone, se abre la ventana para elegir archivos.

rx.vstack(
    rx.upload(
        rx.vstack(
            rx.button("Select File", variant="outline"),
            rx.text("Drag and drop files here or click to select files", size="2"),
            align="center",
        ),
        id="upload_selected",
        border="1px dotted var(--indigo-8)",
        padding="2em",
    ),
    rx.hstack(rx.foreach(rx.selected_files("upload_selected"), lambda f: rx.badge(f)), wrap="wrap"),
    rx.button("Clear", on_click=rx.clear_selected_files("upload_selected"), variant="soft", color_scheme="gray"),
)

Enviar al servidor: rx.upload_files

Si pasas rx.upload_files(upload_id=id) al handler, los archivos elegidos van al servidor. El handler es un evento async normal que recibe un único files: list[rx.UploadFile]. De cada archivo puedes usar file.name (nombre original), file.size (bytes) y await file.read() (contenido).

Al handler de subida no se le pone @rx.event(background=True) ni se usa async with self; eso solo es para la subida por fragmentos de más abajo. Y el botón de subida va fuera de la dropzone: si lo pones dentro, cada clic abre la ventana de archivos y además inicia la subida.

def upload_size_label(size: int) -> str:
    return f"{size / 1024:.1f} KB" if size < 1024 * 1024 else f"{size / 1024 / 1024:.2f} MB"


class UploadInfoState(rx.State):
    files: list[dict[str, str]] = []

    @rx.event
    async def handle_upload(self, files: list[rx.UploadFile]):
        # Read only the name and size. Nothing is saved to disk.
        self.files = [{"name": str(f.name), "size": upload_size_label(f.size or 0)} for f in files]


rx.vstack(
    rx.upload(
        rx.text("Drop up to 5 files (2 MB each)"),
        id="upload_info",
        max_files=5,
        max_size=2 * 1024 * 1024,
        border="2px dashed var(--gray-7)",
        padding="2em",
    ),
    rx.hstack(rx.foreach(rx.selected_files("upload_info"), lambda f: rx.badge(f, variant="soft")), wrap="wrap"),
    rx.hstack(
        rx.button("Upload", on_click=UploadInfoState.handle_upload(rx.upload_files(upload_id="upload_info"))),
        rx.button("Clear", on_click=rx.clear_selected_files("upload_info"), variant="soft", color_scheme="gray"),
    ),
    rx.foreach(
        UploadInfoState.files,
        lambda f: rx.hstack(rx.icon("file", size=14), rx.text(f["name"], size="2"), rx.code(f["size"]), align="center"),
    ),
)

Guardar y mostrar (solo código)

En una app real se guardan en la carpeta que devuelve rx.get_upload_dir() (un Path, por defecto ./uploaded_files, configurable con la variable de entorno REFLEX_UPLOADED_FILES_DIR), y en pantalla se construye la dirección con rx.get_upload_url(nombre_de_archivo) para pasarla a rx.image u otro componente. Abajo está el ejemplo básico de la documentación oficial, que esta página no ejecuta.

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

    @rx.event
    async def handle_upload(self, files: list[rx.UploadFile]):
        for file in files:
            data = await file.read()
            path = rx.get_upload_dir() / file.name
            with path.open("wb") as f:
                f.write(data)
            self.uploaded_files.append(file.name)


def upload_component():
    return rx.vstack(
        rx.upload(id="upload"),
        rx.button("Upload", on_click=State.handle_upload(rx.upload_files("upload"))),
        rx.foreach(
            State.uploaded_files,
            lambda f: rx.image(src=rx.get_upload_url(f), alt="Uploaded file preview"),
        ),
    )
  • rx.get_upload_dir(): la ruta donde guardar en el servidor (backend). Se usa en event handlers.
  • rx.get_upload_url(name): la dirección que usa la pantalla (frontend). No es una cadena de Python sino un Var de frontend, así que solo se usa en código de componentes. La dirección tiene la forma /_upload/nombre_de_archivo, pero no la escribas a mano: pasa siempre por esta función.
  • La carpeta de subidas se publica en /_upload y es accesible para cualquiera. Conviene guardar los archivos con un prefijo aleatorio para que no choquen nombres iguales.
  • En alojamientos como Reflex Cloud, la carpeta de subidas se vacía en cada despliegue. Los archivos que deban durar van en un almacenamiento externo como S3.

Subir de una vez: on_drop y filtros

Si conectas un handler a on_drop, los archivos se suben en cuanto se eligen, sin botón. Con accept se filtra el tipo de archivo, y con max_files y max_size, el número y el tamaño. Los archivos que no cumplen las condiciones llegan a on_drop_rejected; si no das ese evento, 0.10 muestra un toast de error. Prueba a soltar abajo un archivo que no sea imagen ni PDF, o uno de más de 2MB.

class UploadDropState(rx.State):
    names: list[str] = []
    rejected: int = 0

    @rx.event
    async def handle_drop(self, files: list[rx.UploadFile]):
        self.names = [f"{f.name} ({f.size or 0} bytes)" for f in files]

    @rx.event
    def handle_rejected(self, rejections: list):
        self.rejected = len(rejections)


rx.vstack(
    rx.upload(
        rx.text("Images or PDF only, up to 3 files", size="2"),
        id="upload_drop",
        multiple=True,
        accept={
            "application/pdf": [".pdf"],
            "image/png": [".png"],
            "image/jpeg": [".jpg", ".jpeg"],
            "image/webp": [".webp"],
        },
        max_files=3,
        max_size=2 * 1024 * 1024,
        no_keyboard=True,
        on_drop=UploadDropState.handle_drop(rx.upload_files(upload_id="upload_drop")),
        on_drop_rejected=UploadDropState.handle_rejected,
        border="2px dashed var(--gray-7)",
        padding="2em",
    ),
    rx.foreach(UploadDropState.names, lambda n: rx.text(n, size="2")),
    rx.cond(UploadDropState.rejected > 0, rx.text("Rejected: ", UploadDropState.rejected, size="2", color_scheme="red")),
)

Dropzone sin estilos: rx.upload.root

rx.upload es una dropzone con borde y relleno por defecto. Para diseñar el aspecto desde cero, usa rx.upload.root. Las propiedades y los eventos son los mismos. Con drag_active_style puedes cambiar el fondo mientras se arrastra un archivo encima.

rx.vstack(
    rx.upload.root(
        rx.box(
            rx.icon("cloud-upload", size=40, color=rx.color("indigo", 9)),
            rx.hstack(rx.text("Click to upload", weight="bold", color=rx.color("indigo", 11)), rx.text("or drag and drop"), size="2"),
            rx.text("SVG, PNG, JPG or GIF (MAX. 5MB)", size="1", color=rx.color("gray", 10)),
            display="flex",
            flex_direction="column",
            align_items="center",
            gap="6px",
            padding="1.5rem",
            text_align="center",
        ),
        id="upload_root_demo",
        max_width="24rem",
        border=f"2px dashed {rx.color('indigo', 7)}",
        border_radius="12px",
        cursor="pointer",
        drag_active_style={"background_color": rx.color("indigo", 3)},
    ),
    rx.hstack(rx.foreach(rx.selected_files("upload_root_demo"), lambda f: rx.badge(f)), wrap="wrap"),
)

Progreso y cancelación

rx.upload_files (y rx.upload_files_chunk) aceptan on_upload_progress. Durante la subida, a ese handler llega un dict {"loaded": bytes enviados, "total": bytes totales, "progress": 0~1}. rx.cancel_upload(id) detiene una subida en curso. Esta página solo acepta archivos de hasta 2MB, así que el progreso llega a 100 enseguida.

Total bytes received: 0

class UploadProgressState(rx.State):
    uploading: bool = False
    progress: int = 0
    total_bytes: int = 0

    @rx.event
    async def handle_upload(self, files: list[rx.UploadFile]):
        self.total_bytes = sum(f.size or 0 for f in files)

    @rx.event
    def handle_upload_progress(self, progress: dict):
        self.uploading = True
        self.progress = round(progress["progress"] * 100)
        if self.progress >= 100:
            self.uploading = False

    @rx.event
    def cancel_upload(self):
        self.uploading = False
        return rx.cancel_upload("upload_progress")


rx.vstack(
    rx.upload(
        rx.text("Drag and drop files here or click to select files", size="2"),
        id="upload_progress",
        max_size=2 * 1024 * 1024,
        border="1px dotted var(--indigo-8)",
        padding="2em",
    ),
    rx.vstack(rx.foreach(rx.selected_files("upload_progress"), lambda f: rx.text(f, size="2"))),
    rx.progress(value=UploadProgressState.progress, max=100),
    rx.cond(
        ~UploadProgressState.uploading,
        rx.button(
            "Upload",
            on_click=UploadProgressState.handle_upload(
                rx.upload_files(
                    upload_id="upload_progress",
                    on_upload_progress=UploadProgressState.handle_upload_progress,
                ),
            ),
        ),
        rx.button("Cancel", on_click=UploadProgressState.cancel_upload, color_scheme="red"),
    ),
    rx.text("Total bytes received: ", UploadProgressState.total_bytes, size="2"),
    width="100%",
)

Archivos grandes: subida por fragmentos (solo código)

await file.read() carga el archivo entero en memoria. Los archivos grandes se reciben por partes con rx.upload_files_chunk(upload_id=...) y se escriben directamente en disco. El handler de fragmentos debe ser @rx.event(background=True), recibir chunk_iter: rx.UploadChunkIterator y leerlo hasta el final (si sale antes, la subida falla). Cada fragmento tiene chunk.filename, chunk.offset, chunk.content_type y chunk.data. Como es código que escribe en disco, aquí no se ejecuta.

class ChunkUploadState(rx.State):
    uploaded_files: list[str] = []

    @rx.event(background=True)
    async def handle_large_upload(self, chunk_iter: rx.UploadChunkIterator):
        file_handles = {}
        try:
            async for chunk in chunk_iter:  # must consume every chunk
                fh = file_handles.get(chunk.filename)
                if fh is None:
                    path = rx.get_upload_dir() / "stream" / chunk.filename
                    path.parent.mkdir(parents=True, exist_ok=True)
                    fh = file_handles[chunk.filename] = path.open("wb")
                fh.seek(chunk.offset)
                fh.write(chunk.data)
        finally:
            for fh in file_handles.values():
                fh.close()
        async with self:
            self.uploaded_files = sorted(file_handles)


# rx.button("Upload Large Files",
#           on_click=ChunkUploadState.handle_large_upload(rx.upload_files_chunk(upload_id="large_upload")))

Subidas dentro de un formulario

Los archivos no llegan a on_submit del formulario. Si hay una dropzone dentro del formulario, los archivos se envían aparte con rx.upload_files() desde un botón type="button" o desde on_drop, y solo el resto de casillas se reciben con on_submit.

Completar el pedido

La dropzone filtra primero el tipo de imagen, el número y el tamaño, y con on_drop el servidor recibe el nombre y el tamaño en cuanto se sueltan. La lista solo conserva los tres últimos. No se guarda nada.

Receipt photos (max 3, 2 MB each)

class UploadReceiptState(rx.State):
    files: list[dict[str, str]] = []

    @rx.event
    async def attach(self, files: list[rx.UploadFile]):
        added = [{"name": str(f.name), "size": f"{(f.size or 0) / 1024:.0f} KB"} for f in files]
        self.files = (self.files + added)[-3:]

    @rx.event
    def clear(self):
        self.files = []


rx.card(
    rx.vstack(
        rx.text("Receipt photos (max 3, 2 MB each)", weight="bold", size="2"),
        rx.upload(
            rx.hstack(rx.icon("image-plus", size=18), rx.text("Drop or click to attach", size="2"), align="center"),
            id="upload_receipt",
            accept={"image/png": [".png"], "image/jpeg": [".jpg", ".jpeg"]},
            max_files=3,
            max_size=2 * 1024 * 1024,
            on_drop=UploadReceiptState.attach(rx.upload_files(upload_id="upload_receipt")),
            border="1px dashed var(--gray-8)",
            border_radius="8px",
            padding="1em",
            width="100%",
            drag_active_style={"background_color": "var(--accent-3)"},
        ),
        rx.foreach(
            UploadReceiptState.files,
            lambda f: rx.hstack(rx.icon("image", size=14), rx.text(f["name"], size="2"), rx.spacer(), rx.code(f["size"]), width="100%", align="center"),
        ),
        rx.button("Remove all", on_click=UploadReceiptState.clear, size="1", variant="soft", color_scheme="gray"),
        width="100%",
    ),
    width="100%",
    max_width="380px",
)

Notas del taller

  • Dropzone con rx.upload(id="id fijo"). Archivos elegidos: rx.selected_files(id); vaciar: rx.clear_selected_files(id).

  • Para enviar, handler(rx.upload_files(upload_id=id)). El handler es async y recibe files: list[rx.UploadFile].

  • Con on_drop se suben al elegirlos. Se filtra con accept · max_files · max_size, y los rechazados van a on_drop_rejected.

  • Guardar: rx.get_upload_dir(); mostrar: rx.get_upload_url(). La carpeta de subidas es pública.

  • El botón de subida, fuera de la dropzone. Archivos grandes: rx.upload_files_chunk + handler background.

← Anterior

Formularios · Form

Siguiente →

Visualización de datos · Badge

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