← Back to list

Apresentando o Reflex

Criando aplicações Web full-stack com Reflex

Hugo Habbema · 2026-03-12 11:26 · 12 claps · 10.8 min read
#reflex
Open on Medium ↗

Apresentando o Reflex

Criando aplicações Web full-stack com Reflex

Introdução

Se você já experimentou o Streamlit, sabe o quanto ele acelera a criação de protótipos com pouquíssimo código. Acontece que, para levar um protótipo à condição de produto, o Streamlit começa a mostrar limites: o script inteiro reexecuta a cada clique, a estilização é restrita e o controle de rotas praticamente não existe. É nesse ponto que o Reflex entra em cena.

O Reflex é um framework open-source que compila código Python em um frontend React e um backend FastAPI. A premissa é simples: você programa tudo em Python e, nos bastidores, o framework gera uma aplicação web moderna, reativa e pronta para produção — sem que você precise escrever uma única linha de JavaScript.

O modelo de atualização também muda. Enquanto o Streamlit reexecuta o script de ponta a ponta a cada interação, o Reflex adota um modelo reativo: você define variáveis de estado e a interface atualiza automaticamente apenas os componentes que dependem dessas variáveis. É o mesmo princípio do React, porém orquestrado pela linguagem que você já domina.

Neste artigo, vamos montar um projeto completo com três páginas — uma vitrine de componentes, um formulário com validação e uma calculadora de juros compostos — para que você entenda, na prática, como o Reflex funciona de ponta a ponta.

Passo a passo: criar e rodar o projeto

1. Pré-requisitos

O Reflex exige Python 3.10+ e Node.js 18+ (o Node é usado internamente para compilar o frontend React). Confirme as versões no terminal:

python --version
node --version

Se o Python não estiver na versão mínima, atualize-o pelo gerenciador de pacotes do seu sistema operacional. O mesmo vale para o Node.js — o instalador oficial (nodejs.org) resolve na maioria dos casos.

2. Criar o diretório e o ambiente virtual

mkdir reflex-lab1
cd reflex-lab1
python -m venv .venv
source .venv/bin/activate   # Linux/macOS
# .venv\Scripts\activate    # Windows

3. Instalar o Reflex

pip install reflex

4. Inicializar o projeto

reflex init

Quando solicitado, escolha a opção (1) A blank Reflex app. O comando gera a estrutura inicial do projeto, incluindo os arquivos de configuração e o diretório principal da aplicação:

reflex-lab1/
├── .web/                  # Frontend compilado (não editar)
├── assets/                # Arquivos estáticos
├── reflex_lab1/
│   ├── __init__.py
│   └── reflex_lab1.py     # Arquivo principal do app
└── rxconfig.py            # Configuração do projeto

5. Estrutura final

Ao longo do artigo, vamos adicionar dois arquivos ao projeto. A estrutura final ficará assim:

reflex_lab1/
├── __init__.py
├── reflex_lab1.py         # Página inicial (index)
├── cadastro.py            # Formulário com validação
└── calculadora.py         # Calculadora de investimento

Cada função registrada com app.add_page() corresponde a uma rota; o menu de navegação é definido no próprio código.

6. Como rodar o projeto

Na raiz do projeto, com o ambiente virtual ativo, execute:

reflex run

O navegador abrirá em http://localhost:3000. O Reflex conta com hot reload: basta salvar um arquivo para que a página se atualize automaticamente. Para encerrar, pressione Ctrl+C no terminal.

A anatomia de um app Reflex

Antes de mergulhar no código das páginas, vale compreender os três pilares que sustentam qualquer aplicação Reflex.

State — é o coração da aplicação. Uma classe que herda de rx.State e armazena todas as variáveis reativas. Os métodos dessa classe são chamados de event handlers — eles modificam as variáveis em resposta a ações do usuário.

Componentes — funções Python que retornam rx.Component. São o equivalente dos componentes React, mas escritos em Python puro. Cada componente pode referenciar variáveis do State; quando essas variáveis mudam, o componente se atualiza automaticamente.

App — a instância rx.App() que registra as rotas com app.add_page(). É onde tudo se conecta: componentes encontram estados e ganham endereço na aplicação.

Veja um exemplo mínimo que ilustra os três pilares:

import reflex as rx

# 1. STATE
class State(rx.State):
    nome: str = ""
    def set_nome(self, valor: str):
        self.nome = valor
# 2. COMPONENTE
def index() -> rx.Component:
    return rx.box(
        rx.input(
            placeholder="Seu nome",
            on_change=State.set_nome,
        ),
        rx.text("Olá, ", State.nome),
    )
# 3. APP
app = rx.App()
app.add_page(index)

Quando o usuário digita no campo, o evento on_change dispara o handler set_nome, que atualiza a variável nome. O componente rx.text, que referencia State.nome, reflete a mudança instantaneamente — sem recarregar a página e sem reexecutar o script.

Página inicial — Componentes básicos de entrada

A primeira página do projeto funciona como uma vitrine: ela demonstra os principais widgets de entrada do Reflex — campos de texto, seleção, checkbox e slider — todos conectados a um estado reativo. O objetivo é que você perceba a diferença em relação ao Streamlit: cada widget é um componente React compilado; a interação não reexecuta o script.

O estado da página

Substitua o conteúdo de reflex_lab1/reflex_lab1.py pelo código a seguir. Primeiro, definimos o DemoState com as variáveis e os handlers:

import reflex as rx

class DemoState(rx.State):
    nome: str = ""
    email: str = ""
    departamento: str = "TI"
    nota: int = 7
    aceito_termos: bool = False
    def set_nome(self, valor: str):
        self.nome = valor
    def set_email(self, valor: str):
        self.email = valor
    def set_departamento(self, valor: str):
        self.departamento = valor
    def set_nota(self, valor: list[int]):
        self.nota = valor[0]
    def toggle_termos(self, valor: bool):
        self.aceito_termos = valor

Barra de navegação reutilizável

Em seguida, criamos uma função navbar() que será importada pelas demais páginas, garantindo navegação consistente em toda a aplicação:

def navbar() -> rx.Component:
    return rx.hstack(
        rx.heading("Reflex Lab 1", size="5"),
        rx.spacer(),
        rx.link("Início", href="/"),
        rx.link("Cadastro", href="/cadastro"),
        rx.link("Calculadora", href="/calculadora"),
        padding="1em", bg="gray.100",
        width="100%", spacing="4",
    )

A interface completa

A função index() monta a interface com seções de texto, seleção e slider. O código completo ocupa cerca de 80 linhas. Eis os trechos mais relevantes e o que cada um ensina:

def index() -> rx.Component:
    return rx.box(
        navbar(),
        rx.container(
            rx.heading("Componentes de Entrada", size="6", margin_y="1em"),
            rx.text(
                "Explore os componentes básicos do Reflex. "
                "Diferente do Streamlit, cada widget é um componente React "
                "compilado — sem reexecução do script."
            ),
            rx.divider(margin_y="1em"),
# --- Texto ---
            rx.heading("Texto e Números", size="4", margin_y="0.5em"),
            rx.hstack(
                rx.vstack(
                    rx.text("Nome completo", font_weight="bold"),
                    rx.input(
                        placeholder="Ex: Ana Silva",
                        on_change=DemoState.set_nome,
                        width="100%",
                    ),
                    rx.cond(
                        DemoState.nome != "",
                        rx.text("Olá, ", DemoState.nome, "!", color="green"),
                    ),
                    width="50%",
                ),
                rx.vstack(
                    rx.text("E-mail", font_weight="bold"),
                    rx.input(
                        placeholder="ana@empresa.com",
                        type="email",
                        on_change=DemoState.set_email,
                        width="100%",
                    ),
                    width="50%",
                ),
                width="100%", spacing="4",
            ),
            rx.divider(margin_y="1em"),
            # --- Seleção ---
            rx.heading("Seleção", size="4", margin_y="0.5em"),
            rx.hstack(
                rx.vstack(
                    rx.text("Departamento (select)", font_weight="bold"),
                    rx.select(
                        ["TI", "RH", "Vendas", "Financeiro"],
                        default_value="TI",
                        on_change=DemoState.set_departamento,
                    ),
                    rx.text("Selecionado: ", DemoState.departamento),
                    width="50%",
                ),
                rx.vstack(
                    rx.text("Aceito os termos (checkbox)", font_weight="bold"),
                    rx.checkbox(
                        "Li e aceito os Termos de Uso",
                        checked=DemoState.aceito_termos,
                        on_change=DemoState.toggle_termos,
                    ),
                    rx.cond(
                        DemoState.aceito_termos,
                        rx.text("Termos aceitos", color="green"),
                        rx.text("Termos pendentes", color="red"),
                    ),
                    width="50%",
                ),
                width="100%", spacing="4",
            ),
            rx.divider(margin_y="1em"),
            # --- Slider ---
            rx.heading("Slider", size="4", margin_y="0.5em"),
            rx.text("Nota (0 a 10): ", DemoState.nota, font_weight="bold"),
            rx.slider(
                default_value=[7],
                min=0, max=10, step=1,
                on_value_commit=DemoState.set_nota,
                width="50%",
            ),
            padding="2em", max_width="900px",
        ),
    )

Observações sobre os componentes utilizados:

  • **rx.input** com on_change — equivalente ao st.text_input do Streamlit. A cada caractere digitado, o handler atualiza o estado em tempo real.
  • **rx.select** com on_change — equivalente ao st.selectbox. Retorna o valor selecionado pelo usuário.
  • **rx.checkbox** com checked e on_change — equivalente ao st.checkbox. O parâmetro checked vincula o estado atual; on_change dispara a alternância.
  • **rx.slider** com on_value_commit — equivalente ao st.slider. Usamos on_value_commit em vez de on_change para disparar o handler somente quando o usuário solta o controle, evitando atualizações a cada pixel arrastado.
  • **rx.cond** para renderização condicional — como o Reflex compila o frontend em React, o if/else do Python não funciona com variáveis de estado. Em vez disso, usamos rx.cond(condição, componente_se_true, componente_se_false).

Registro das rotas

Ao final do arquivo, registramos a rota principal e importamos as páginas adicionais:

app = rx.App()
app.add_page(index, route="/", title="Reflex Lab 1 - Início")

# Importações no final para evitar dependências circulares
from reflex_lab1.cadastro import cadastro_page
from reflex_lab1.calculadora import calculadora_page
app.add_page(cadastro_page, route="/cadastro", title="Cadastro")
app.add_page(calculadora_page, route="/calculadora",
             title="Calculadora de Investimento")

As importações ficam no final do arquivo porque cadastro.py e calculadora.py importam a navbar() definida aqui — colocá-las no topo criaria uma dependência circular.

Formulário com validação — Cadastro de funcionário

A segunda página mostra como o Reflex lida com formulários. Enquanto na página inicial cada input dispara seu próprio evento, aqui agrupamos todos os campos dentro de um rx.form e enviamos os dados de uma só vez quando o usuário clica em "Cadastrar" — exatamente como o st.form() faz no Streamlit.

O estado do cadastro

Crie o arquivo reflex_lab1/cadastro.py. O CadastroState armazena os dados do formulário e inclui um handler de validação:

import reflex as rx
from reflex_lab1.reflex_lab1 import navbar

class CadastroState(rx.State):
    nome: str = ""
    email: str = ""
    cargo: str = ""
    salario: str = ""
    mensagem: str = ""
    sucesso: bool = False
    def handle_submit(self, form_data: dict):
        nome = form_data.get("nome", "").strip()
        email = form_data.get("email", "").strip()
        cargo = form_data.get("cargo", "Analista")
        salario = form_data.get("salario", "0")
        if not nome:
            self.mensagem = "Nome é obrigatório."
            self.sucesso = False
            return
        if not email:
            self.mensagem = "E-mail é obrigatório."
            self.sucesso = False
            return
        self.nome = nome
        self.email = email
        self.cargo = cargo
        self.salario = salario
        self.mensagem = f"{nome} cadastrado(a) como {cargo}!"
        self.sucesso = True

O componente do formulário

A função cadastro_page() organiza os campos em duas colunas — nome e cargo à esquerda, e-mail e salário à direita — e exibe feedback visual após a submissão:

def cadastro_page() -> rx.Component:
    return rx.box(
        navbar(),
        rx.container(
            rx.heading("Cadastro de Funcionário", size="6", margin_y="1em"),
            rx.text(
                "O rx.form agrupa os inputs e só envia quando o usuário clica "
                "no botão de submit — equivalente ao st.form() do Streamlit."
            ),
            rx.divider(margin_y="1em"),
            rx.form(
                rx.vstack(
                    rx.hstack(
                        rx.vstack(
                            rx.text("Nome completo *", font_weight="bold"),
                            rx.input(
                                placeholder="Ex: Ana Silva",
                                name="nome",
                                required=True,
                            ),
                            rx.text("Cargo", font_weight="bold"),
                            rx.select(
                                ["Analista", "Dev", "Gerente", "Diretor"],
                                name="cargo",
                                default_value="Analista",
                            ),
                            width="50%",
                        ),
                        rx.vstack(
                            rx.text("E-mail corporativo *", font_weight="bold"),
                            rx.input(
                                placeholder="ana@empresa.com",
                                name="email",
                                type="email",
                                required=True,
                            ),
                            rx.text("Salário (R$)", font_weight="bold"),
                            rx.input(
                                placeholder="5000.00",
                                name="salario",
                                type="number",
                            ),
                            width="50%",
                        ),
                        width="100%", spacing="4",
                    ),
                    rx.button(
                        "Cadastrar funcionário",
                        type="submit",
                        width="100%",
                        color_scheme="green",
                        margin_top="1em",
                    ),
                    spacing="3",
                ),
                on_submit=CadastroState.handle_submit,
                reset_on_submit=True,
            ),
            # Resultado
            rx.cond(
                CadastroState.mensagem != "",
                rx.callout(
                    CadastroState.mensagem,
                    icon="info",
                    color_scheme=rx.cond(
                        CadastroState.sucesso, "green", "red"
                    ),
                    margin_top="1em",
                ),
            ),
            rx.cond(
                CadastroState.sucesso,
                rx.box(
                    rx.text("E-mail: ", CadastroState.email),
                    rx.text("Salário: R$ ", CadastroState.salario),
                    padding="1em",
                    border="1px solid #ddd",
                    border_radius="8px",
                    margin_top="0.5em",
                ),
            ),
            padding="2em", max_width="900px",
        ),
    )

Pontos importantes deste código:

  • **rx.form + on_submit** — agrupa todos os inputs e envia seus valores como um dicionário ao handler. Cada input precisa do atributo name para ser identificado.
  • Validação no handler — a lógica de validação reside no backend (dentro do handle_submit), nunca nos componentes. Diferente do Streamlit, o handler não reexecuta a página inteira; ele apenas atualiza as variáveis de estado.
  • **rx.callout + rx.cond** — exibimos mensagens de sucesso (verde) ou erro (vermelho) de forma condicional, alternando o color_scheme via rx.cond.
  • **reset_on_submit=True** — limpa o formulário após o envio, equivalente ao clear_on_submit do Streamlit.

Exemplo prático — Calculadora de investimento

A terceira página põe o Reflex à prova com um caso de uso tangível: simular o crescimento de um investimento com juros compostos e aportes mensais. É a mesma fórmula que usamos no artigo sobre Streamlit, agora transposta para o modelo reativo.

O estado do investimento

Crie o arquivo reflex_lab1/calculadora.py. O InvestState recebe os dados do formulário e executa o cálculo:

import reflex as rx
from reflex_lab1.reflex_lab1 import navbar

class InvestState(rx.State):
    valor_inicial: float = 1000.0
    aporte_mensal: float = 200.0
    taxa_anual: float = 12.0
    prazo_anos: int = 10
    calculado: bool = False
    montante: float = 0.0
    total_investido: float = 0.0
    rendimento: float = 0.0
    def handle_submit(self, form_data: dict):
        self.valor_inicial = float(form_data.get("valor_inicial", 1000))
        self.aporte_mensal = float(form_data.get("aporte_mensal", 200))
        self.taxa_anual = float(form_data.get("taxa_anual", 12))
        self.prazo_anos = int(form_data.get("prazo_anos", 10))
        taxa_mensal = (1 + self.taxa_anual / 100) ** (1 / 12) - 1
        prazo_meses = self.prazo_anos * 12
        fator = (1 + taxa_mensal) ** prazo_meses
        self.montante = self.valor_inicial * fator
        if taxa_mensal > 0:
            self.montante += self.aporte_mensal * ((fator - 1) / taxa_mensal)
        else:
            self.montante += self.aporte_mensal * prazo_meses
        self.total_investido = (
            self.valor_inicial + self.aporte_mensal * prazo_meses
        )
        self.rendimento = self.montante - self.total_investido
        self.calculado = True

A matemática por trás

Dois passos compõem o cálculo. Primeiro, convertemos a taxa anual para mensal usando a equivalência de taxas:

taxa_mensal = (1 + taxa_anual / 100) ^ (1/12) - 1

Depois, aplicamos a fórmula de valor futuro com aportes periódicos:

FV = PV × (1 + r)^n  +  PMT × (((1 + r)^n - 1) / r)

Onde PV é o valor inicial, PMT é o aporte mensal, r é a taxa mensal e n é o prazo em meses. O caso r = 0 é tratado à parte para evitar divisão por zero.

A interface de resultados

O formulário divide os inputs em duas colunas. Após o clique em “Calcular”, o handler atualiza InvestState.calculado para True e os resultados — montante final, total investido e rendimento — aparecem automaticamente em cards estilizados com rx.box e rx.heading. No Streamlit, usaríamos st.metric para exibir esses valores; no Reflex, montamos os cards manualmente, o que dá mais liberdade visual.

def calculadora_page() -> rx.Component:
    return rx.box(
        navbar(),
        rx.container(
            rx.heading("Calculadora de Investimento", size="6", margin_y="1em"),
            rx.text(
                "Juros compostos: valor inicial + aportes mensais. "
                "Mesma fórmula do exemplo em Streamlit."
            ),
            rx.divider(margin_y="1em"),
            rx.form(
                rx.hstack(
                    rx.vstack(
                        rx.text("Valor inicial (R$)", font_weight="bold"),
                        rx.input(
                            name="valor_inicial",
                            type="number",
                            default_value="1000",
                            min="0", step="100",
                        ),
                        rx.text("Aporte mensal (R$)", font_weight="bold"),
                        rx.input(
                            name="aporte_mensal",
                            type="number",
                            default_value="200",
                            min="0", step="50",
                        ),
                        width="50%",
                    ),
                    rx.vstack(
                        rx.text("Taxa de juros anual (%)", font_weight="bold"),
                        rx.input(
                            name="taxa_anual",
                            type="number",
                            default_value="12",
                            min="0", max="30", step="0.5",
                        ),
                        rx.text("Prazo (anos)", font_weight="bold"),
                        rx.input(
                            name="prazo_anos",
                            type="number",
                            default_value="10",
                            min="1", max="40",
                        ),
                        width="50%",
                    ),
                    width="100%", spacing="4",
                ),
                rx.button(
                    "Calcular",
                    type="submit",
                    width="100%",
                    color_scheme="blue",
                    margin_top="1em",
                ),
                on_submit=InvestState.handle_submit,
            ),
            # Resultados
            rx.cond(
                InvestState.calculado,
                rx.box(
                    rx.divider(margin_y="1em"),
                    rx.hstack(
                        rx.box(
                            rx.text("Montante final", color="gray"),
                            rx.heading(
                                "R$ ",
                                InvestState.montante.to(int).to(str),
                                size="5", color="green",
                            ),
                            width="33%", text_align="center",
                        ),
                        rx.box(
                            rx.text("Total investido", color="gray"),
                            rx.heading(
                                "R$ ",
                                InvestState.total_investido.to(int).to(str),
                                size="5",
                            ),
                            width="33%", text_align="center",
                        ),
                        rx.box(
                            rx.text("Rendimento", color="gray"),
                            rx.heading(
                                "R$ ",
                                InvestState.rendimento.to(int).to(str),
                                size="5", color="blue",
                            ),
                            width="33%", text_align="center",
                        ),
                        width="100%", padding="1em",
                        border="1px solid #ddd", border_radius="8px",
                    ),
                ),
            ),
            padding="2em", max_width="900px",
        ),
    )

Streamlit vs. Reflex — quando usar cada um

Ambos os frameworks permitem construir aplicações web em Python, mas atendem a momentos diferentes do ciclo de vida de um projeto.

Na prática, a escolha depende do destino do projeto. Se o objetivo é validar uma ideia rapidamente — explorar dados, apresentar gráficos a um time técnico, testar uma hipótese — , o Streamlit é imbatível em velocidade. Mas quando o protótipo precisa virar produto, com rotas dedicadas, formulários robustos, autenticação e controle fino de layout, o Reflex é o próximo passo natural, porque mantém tudo em Python sem sacrificar a experiência do usuário.

Boas práticas com Reflex

Ao longo do projeto, seguimos um conjunto de práticas que valem para qualquer aplicação Reflex:

  • Separe o State por página — cada página com seu próprio rx.State evita conflitos e facilita a manutenção. No projeto, usamos DemoState, CadastroState e InvestState, cada um confinado ao seu contexto.
  • Use rx.form para múltiplos inputs — assim como o st.form() no Streamlit, o rx.form agrupa campos e envia tudo de uma vez, evitando atualizações parciais a cada digitação.
  • Prefira on_value_commit em sliders — o on_change dispara a cada pixel arrastado; o on_value_commit só dispara quando o usuário solta o controle, o que é mais eficiente.
  • Lembre-se do rx.cond — como o frontend é compilado para JavaScript, o if/else do Python não funciona com variáveis de estado. Use sempre rx.cond().
  • Valide no event handler — a lógica de validação pertence ao backend, dentro dos handlers. Os componentes apenas exibem o resultado.
  • Crie componentes reutilizáveis — funções como a navbar() podem ser importadas por todas as páginas, garantindo consistência visual sem duplicação de código.
  • Registre todas as rotas em um único arquivo principal — centralizar o registro evita surpresas e facilita a navegação pelo código.

Recapitulando

Ao longo deste artigo, você construiu um app Reflex completo com três páginas:

  • Página inicial — vitrine de componentes básicos (input, select, checkbox, slider) conectados a um estado reativo.
  • Cadastro de funcionário — formulário com rx.form, validação no handler e feedback condicional.
  • Calculadora de investimento — juros compostos com aportes mensais e exibição reativa de resultados.

Tudo foi escrito em Python. Nos bastidores, o Reflex compilou o frontend em React e manteve o backend em FastAPI com WebSockets — uma arquitetura moderna que você não precisou configurar manualmente.

Para revisar o que construiu, basta rodar reflex run e navegar entre as três rotas: /, /cadastro e /calculadora.

Se quiser ir além, a documentação oficial é o melhor ponto de partida: reflex.dev/docs

Até a próxima!


메타데이터
post_id
4fbd6da9fc01
slug
apresentando-o-reflex-4fbd6da9fc01
url
https://medium.com/@habbema/apresentando-o-reflex-4fbd6da9fc01
canonical_url
https://medium.com/@habbema/apresentando-o-reflex-4fbd6da9fc01
author_url
https://medium.com/@habbema
status
ok
fetched_at
2026-07-13 11:39:10