Apresentando o Reflex
Criando aplicações Web full-stack com Reflex

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** comon_change— equivalente aost.text_inputdo Streamlit. A cada caractere digitado, o handler atualiza o estado em tempo real.**rx.select** comon_change— equivalente aost.selectbox. Retorna o valor selecionado pelo usuário.**rx.checkbox** comcheckedeon_change— equivalente aost.checkbox. O parâmetrocheckedvincula o estado atual;on_changedispara a alternância.**rx.slider** comon_value_commit— equivalente aost.slider. Usamoson_value_commitem vez deon_changepara 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, oif/elsedo Python não funciona com variáveis de estado. Em vez disso, usamosrx.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 atributonamepara 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 ocolor_schemeviarx.cond.**reset_on_submit=True** — limpa o formulário após o envio, equivalente aoclear_on_submitdo 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.Stateevita conflitos e facilita a manutenção. No projeto, usamosDemoState,CadastroStateeInvestState, cada um confinado ao seu contexto. - Use
rx.formpara múltiplos inputs — assim como ost.form()no Streamlit, orx.formagrupa campos e envia tudo de uma vez, evitando atualizações parciais a cada digitação. - Prefira
on_value_commitem sliders — oon_changedispara a cada pixel arrastado; oon_value_commitsó dispara quando o usuário solta o controle, o que é mais eficiente. - Lembre-se do
rx.cond— como o frontend é compilado para JavaScript, oif/elsedo Python não funciona com variáveis de estado. Use semprerx.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