Reflex: Membangun Aplikasi Web Full-Stack dengan Python Murni
Reflex memungkinkan Anda membangun aplikasi web lengkap — frontend, backend, dan basis data — tanpa keluar dari Python. Berbeda dengan tool yang berorientasi dashboard, Reflex mengompilasi komponen Python Anda menjadi frontend React/Next.js sementara logika aplikasi berjalan di backend FastAPI, sehingga Anda mendapatkan manajemen state, routing, dan persistensi yang sesungguhnya. Tutorial ini membahas arsitektur, model state, komponen, routing, handler async, akses basis data, dan deployment menggunakan satu contoh task manager yang utuh.
Apa Itu Reflex dan Bagaimana Cara Kerjanya
Reflex (sebelumnya Pynecone) adalah framework untuk membangun aplikasi web full-stack di mana Anda hanya menulis Python. Saat proses build, komponen yang Anda deskripsikan dalam Python dikompilasi menjadi frontend React/Next.js. Saat runtime, state aplikasi Anda berada di kelas-kelas Python yang dijalankan di backend FastAPI. Browser dan backend berkomunikasi melalui koneksi WebSocket: interaksi pengguna memicu event handler di server, server mengubah state, dan hanya state yang berubah yang dikirim kembali ke klien untuk me-render ulang komponen yang terdampak.
Model ini berbeda dari Streamlit. Pada Streamlit, setiap interaksi menjalankan ulang seluruh skrip dari atas ke bawah, dan Anda mengelola persistensi melalui st.sessionstate. Reflex justru menyimpan objek state berumur panjang per sesi klien dan memperbarui UI secara reaktif — hanya bagian yang terikat ke state var yang berubah saja yang diperbarui. Anda mendapatkan nuansa single-page application dengan routing sisi klien, ditambah backend nyata yang bisa Anda hubungkan dengan basis data dan background task.
Sekilas Arsitektur
- Frontend: Komponen Python dikompilasi menjadi komponen React yang di-render oleh Next.js. Styling dipetakan ke CSS/Tailwind di balik layar.
- Backend: Server FastAPI meng-host kelas
Statedan event handler Anda. - Transport: Sebuah WebSocket membawa event dari klien ke server dan delta state kembali.
- Basis data (opsional): Integrasi SQLModel bawaan (SQLite secara default) yang dapat diakses dari event handler.
Browser (React/Next.js) <-- WebSocket --> Backend FastAPI (State Python + handler)
|
Basis data (SQLModel)
Instalasi dan Penyiapan Proyek
Reflex membutuhkan Python 3.10 atau lebih baru. Buat environment terisolasi, lalu instal paketnya.
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install reflex
Verifikasi instalasi
reflex --version
Inisialisasi proyek baru di dalam direktori kosong. Perintah init menyusun struktur dan meminta Anda memilih template; pilih template kosong untuk memulai dari awal yang bersih.
mkdir taskmanager && cd taskmanager
reflex init
Saat diminta, pilih template "blank"
Struktur Proyek
Setelah inisialisasi, Anda mendapatkan tata letak seperti berikut:
taskmanager/
├── taskmanager/
│ └── taskmanager.py # Aplikasi Anda: state, komponen, halaman
├── assets/ # File statis (gambar, favicon)
├── rxconfig.py # Konfigurasi aplikasi (nama, dburl, dll.)
└── requirements.txt
File rxconfig.py mendeklarasikan nama aplikasi dan konfigurasi seperti URL basis data serta port frontend/backend.
import reflex as rx
config = rx.Config(
appname="taskmanager",
dburl="sqlite:///taskmanager.db",
)
Menjalankan dalam Mode Pengembangan
Jalankan server pengembangan. Reflex mengompilasi frontend, menjalankan backend FastAPI, dan menyajikan aplikasi dengan hot reload.
reflex run
Frontend di http://localhost:3000, backend di http://localhost:8000
Model State
State adalah inti dari Reflex. Anda mendefinisikan state dengan menurunkan kelas dari rx.State. Atribut kelas dengan anotasi tipe menjadi state var — nilai reaktif yang disimpan per sesi klien. Method pada kelas state adalah event handler: keduanya berjalan di backend dan merupakan satu-satunya tempat yang seharusnya mengubah state.
import reflex as rx
class CounterState(rx.State):
count: int = 0
def increment(self):
self.count += 1
def decrement(self):
self.count -= 1
def setcount(self, value: str):
# Event handler menerima payload event sebagai argumen
self.count = int(value or 0)
Ketika increment berjalan, ia mengubah self.count. Reflex menghitung delta dan mengirimkannya ke klien, yang lalu me-render ulang hanya komponen yang terikat ke count. Tidak ada rerun seluruh skrip: bagian halaman yang tidak terkait tetap utuh, dan state komponen lokal di frontend (posisi scroll, fokus) tetap terjaga.
Menghubungkan State ke Komponen
Anda membangun UI dengan mengembalikan komponen dari sebuah fungsi dan mengikat prop serta event-nya ke state.
def counter() -> rx.Component:
return rx.hstack(
rx.button("-", onclick=CounterState.decrement),
rx.text(CounterState.count),
rx.button("+", onclick=CounterState.increment),
spacing="3",
align="center",
)
CounterState.count di sini bukan integer biasa — melainkan referensi reaktif. Ketika nilainya berubah di backend, rx.text ikut diperbarui secara otomatis. Event seperti onclick diikat ke referensi handler (bukan dipanggil), dan Reflex menjalankannya di server saat dipicu.
Membangun UI dari Komponen
Reflex menyediakan banyak komponen. Primitif tata letak seperti rx.vstack, rx.hstack, dan rx.box menata anak-anak komponen; komponen konten seperti rx.text, rx.heading, rx.input, dan rx.button me-render UI. Prop diberikan sebagai argumen kata kunci.
def taskinput() -> rx.Component:
return rx.vstack(
rx.heading("Tugas Saya", size="6"),
rx.hstack(
rx.input(
placeholder="Apa yang perlu dikerjakan?",
value=TaskState.new
task,
onchange=TaskState.setnewtask,
width="100%",
),
rx.button("Tambah", onclick=TaskState.addtask),
width="100%",
),
rx.box(height="1rem"),
spacing="3",
width="100%",
maxwidth="480px",
)
Di sini prop value input diikat ke sebuah state var dan onchange memperbaruinya pada setiap ketikan. Ikatan dua arah ini bersifat eksplisit: nilai yang ditampilkan selalu mencerminkan state backend, dan setiap perubahan mengalir kembali melalui event handler.
Computed Var serta Rendering Kondisional/Iteratif
Computed var adalah properti yang didekorasi dengan@rx.var yang menurunkan nilai dari state var lain. Ia menghitung ulang otomatis ketika dependensinya berubah, sehingga data turunan tetap berada di luar event handler.
class TaskState(rx.State):
tasks: list[dict] = []
newtask: str = ""
@rx.var
def opencount(self) -> int:
return len([t for t in self.tasks if not t["done"]])
@rx.var
def hastasks(self) -> bool:
return len(self.tasks) > 0
Karena UI dikompilasi, Anda tidak bisa menggunakan if/for Python biasa terhadap state var di dalam komponen — pada saat kompilasi nilai sebenarnya belum diketahui. Sebagai gantinya, gunakan rx.cond untuk rendering kondisional dan rx.foreach untuk iterasi.
def taskrow(task: dict, index: int) -> rx.Component:
return rx.hstack(
rx.checkbox(
checked=task["done"],
on
change=lambda checked: TaskState.toggle(index),
),
rx.text(task["title"]),
rx.spacer(),
rx.button(
"Hapus",
onclick=lambda: TaskState.delete(index),
colorscheme="red",
variant="soft",
),
width="100%",
align="center",
)
def tasklist() -> rx.Component:
return rx.cond(
TaskState.hastasks,
rx.vstack(
rx.foreach(TaskState.tasks, taskrow),
width="100%",
),
rx.text("Belum ada tugas. Tambahkan di atas.", color="gray"),
)
rx.cond(kondisi, jikabenar, jikasalah) me-render salah satu cabang berdasarkan kondisi reaktif. rx.foreach(iterable, fungsirender) memetakan setiap item (dan indeks opsional) menjadi komponen; fungsi render harus didefinisikan pada level modul karena ia dikompilasi, bukan dijalankan per render.
Routing Multi-Halaman
Aplikasi Reflex bersifat multi-halaman secara default. Anda mendaftarkan halaman baik dengan dekorator @rx.page maupun dengan memanggil app.addpage. Setiap halaman dipetakan ke sebuah rute.
app = rx.App()
@rx.page(route="/", title="Tugas")
def index() -> rx.Component:
return rx.container(taskinput(), tasklist())
def about() -> rx.Component:
return rx.container(
rx.heading("Tentang"),
rx.text("Task manager sederhana yang dibangun dengan Reflex."),
rx.link("Kembali ke tugas", href="/"),
)
app.addpage(about, route="/about", title="Tentang")
Rute Dinamis dan Redirect
Segmen dinamis dideklarasikan dengan tanda kurung siku pada rute. Nilainya tersedia melalui state var router.
class DetailState(rx.State):
@rx.var
def taskid(self) -> str:
return self.router.page.params.get("id", "")
@rx.page(route="/task/[id]")
def taskdetail() -> rx.Component:
return rx.container(
rx.heading("Detail tugas"),
rx.text("Melihat tugas: ", DetailState.taskid),
rx.link("Kembali", href="/"),
)
Untuk berpindah halaman secara programatik dari event handler, kembalikan rx.redirect.
def saveandexit(self):
# ... menyimpan perubahan ...
return rx.redirect("/")
Styling, Tema, dan Prop Responsif
Anda bisa men-style komponen secara inline dengan nama prop yang dipetakan ke CSS, mengoper sebuah dict style, atau mendefinisikan tema global pada aplikasi. Reflex menggunakan Tailwind di balik layar dan terintegrasi dengan pustaka komponen berbasis Radix agar tema tetap konsisten.
app = rx.App(
theme=rx.theme(
appearance="light",
accentcolor="indigo",
radius="medium",
),
)
Dict style dan prop responsif memungkinkan adaptasi terhadap ukuran layar. Oper sebuah list ke prop untuk menetapkan nilai per breakpoint (mobile-first).
def card(content: rx.Component) -> rx.Component:
return rx.box(
content,
style={
"padding": "1.5rem",
"border": "1px solid var(--gray-5)",
"borderradius": "12px",
},
width=["100%", "100%", "480px"], # penuh di layar kecil, tetap di layar besar
)
Event Handler Async dan Pembaruan Streaming
Event handler bisa bersifat async, yang penting untuk memanggil API atau model eksternal tanpa memblokir. Dengan yield di dalam handler, Anda mendorong state perantara ke klien sehingga menghasilkan pembaruan UI progresif — berguna untuk men-stream output model.
import asyncio
import reflex as rx
class ChatState(rx.State):
prompt: str = ""
answer: str = ""
isstreaming: bool = False
async def ask(self):
self.isstreaming = True
self.answer = ""
yield # kirim status "streaming dimulai" ke klien
# Simulasi streaming token demi token dari model/API
async for chunk in fakemodelstream(self.prompt):
self.answer += chunk
yield # dorong setiap jawaban parsial ke UI
self.isstreaming = False
async def fakemodelstream(prompt: str):
for word in f"Anda bertanya: {prompt}. Ini balasan streaming.".split():
await asyncio.sleep(0.05)
yield word + " "
Setiap yield mengirim delta state saat ini melalui WebSocket. UI yang terikat ke ChatState.answer diperbarui secara bertahap, dan Anda bisa mengikat spinner ke isstreaming.
def chatview() -> rx.Component:
return rx.vstack(
rx.input(
value=ChatState.prompt,
onchange=ChatState.setprompt,
placeholder="Tanyakan sesuatu...",
),
rx.button("Kirim", onclick=ChatState.ask, loading=ChatState.isstreaming),
rx.text(ChatState.answer),
width="100%",
)
Form dengan Validasi Sisi Klien dan Sisi Server
rx.form mengumpulkan field dan mengirim nilainya sebagai dict ke sebuah handler. Gunakan batasan tingkat HTML (required, type, minlength) untuk pemeriksaan sisi klien, dan validasi ulang di server sebelum menyimpan.
class FormState(rx.State):
error: str = ""
def submit(self, formdata: dict):
title = (formdata.get("title") or "").strip()
if len(title) < 3:
self.error = "Judul minimal 3 karakter."
return
self.error = ""
return TaskState.addtaskfromform(title)
def taskform() -> rx.Component:
return rx.form(
rx.vstack(
rx.input(name="title", placeholder="Judul tugas", required=True),
rx.cond(
FormState.error != "",
rx.text(FormState.error, color="red", size="2"),
),
rx.button("Buat", type="submit"),
),
onsubmit=FormState.submit,
resetonsubmit=True,
)
required di sisi klien memblokir submit kosong di browser; pemeriksaan panjang di sisi server bersifat otoritatif karena klien dapat melewati validasi HTML. Selalu jadikan validasi server sebagai sumber kebenaran.
Akses Basis Data dengan rx.Model
Reflex menyertakan integrasi SQLModel. Turunkan kelas dari rx.Model dengan table=True untuk mendefinisikan sebuah tabel. Anda melakukan query dan mutasi di dalam event handler menggunakan sesi melalui rx.session().
import reflex as rx
from sqlmodel import select
class Task(rx.Model, table=True):
title: str
done: bool = False
Buat skema dengan menghasilkan dan menerapkan migrasi (Reflex membungkus Alembic).
reflex db init # sekali saja, menyiapkan migrasi
reflex db makemigrations --message "add task table"
reflex db migrate
Sekarang baca dan tulis di dalam handler. Muat baris yang tersimpan ke dalam state var agar UI dapat me-render-nya.
class TaskState(rx.State):
tasks: list[Task] = []
newtask: str = ""
def loadtasks(self):
with rx.session() as session:
self.tasks = session.exec(select(Task)).all()
def addtask(self):
title = self.newtask.strip()
if not title:
return
with rx.session() as session:
session.add(Task(title=title))
session.commit()
self.newtask = ""
self.loadtasks()
def toggle(self, taskid: int):
with rx.session() as session:
task = session.get(Task, taskid)
if task:
task.done = not task.done
session.add(task)
session.commit()
self.loadtasks()
Panggil loadtasks saat halaman dimuat dengan melampirkannya ke onload halaman.
@rx.page(route="/", onload=TaskState.loadtasks)
def index() -> rx.Component:
return rx.container(task
input(), tasklist())
Background Task
Untuk pekerjaan berdurasi panjang yang tidak boleh memblokir event loop atau menahan lock state, deklarasikan event background dengan @rx.event(background=True). Di dalamnya, Anda harus mengubah state di dalam blok async with self untuk mengambil lock dengan aman.
class JobState(rx.State):
progress: int = 0
@rx.event(background=True)
async def runjob(self):
for i in range(1, 101):
await asyncio.sleep(0.1)
async with self:
self.progress = i
Cara ini menjaga UI tetap responsif selama job berjalan dan men-stream pembaruan progres seiring loop berlanjut.
Build dan Deployment
Untuk build bergaya produksi secara lokal, ekspor artefak frontend dan backend.
# Menghasilkan frontend statis dan bundel backend
reflex export
Reflex menyediakan perintah deploy terkelola yang menyiapkan dan mengirim aplikasi.
reflex deploy
Untuk self-hosting, kontainerisasi aplikasi. Dockerfile minimal menginstal dependensi, menjalankan migrasi, dan memulai server produksi.
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
RUN reflex export --frontend-only --no-zip
EXPOSE 3000 8000
CMD ["reflex", "run", "--env", "prod"]
Di balik reverse proxy (nginx/Caddy), arahkan port frontend dan proksikan path WebSocket/backend agar klien dan server terhubung dengan benar.
Praktik Terbaik
- Jaga state tetap minimal dan dapat diserialisasi. State var melintasi WebSocket sebagai JSON; simpan data sederhana dan muat objek berat sesuai kebutuhan di dalam handler.
- Ubah state hanya di event handler. Perlakukan komponen sebagai tampilan murni dari state; jangan pernah mengubah state saat rendering.
- Gunakan computed var untuk data turunan. Hindari duplikasi logika antar handler; biarkan
@rx.varmenghitung ulang otomatis. - Pisahkan state berdasarkan fitur. Beberapa subclass
rx.Statemenjaga keterpisahan urusan dan mengurangi pembaruan yang tidak perlu. - Validasi di server. Batasan sisi klien meningkatkan UX, tetapi backend harus menegakkan kebenaran sebelum menulis ke basis data.
- Muat ulang dari basis data setelah penulisan. Setelah commit, segarkan state var terkait agar UI mencerminkan kebenaran yang tersimpan.
- Gunakan handler async untuk I/O dan event background untuk job panjang. Lakukan yield untuk men-stream progres dan menjaga antarmuka tetap responsif.
- Kunci versi Reflex di
requirements.txtagar output kompilasi tetap reprodusibel di seluruh environment.
Kesimpulan dan Poin Penting
Reflex menjembatani jarak antara prototipe Python yang cepat dan aplikasi full-stack yang sesungguhnya. Dengan mengompilasi komponen Python menjadi frontend React/Next.js dan menjalankan state Anda di backend FastAPI, Reflex memberikan pembaruan UI reaktif, routing sisi klien, form, dan persistensi basis data tanpa menulis JavaScript.
Poin penting:
- Reflex bersifat full-stack: komponen Python dikompilasi menjadi frontend React;
Stateberjalan di backend FastAPI melalui WebSocket. - Model state reaktif hanya memperbarui komponen yang berubah, berbeda dengan rerun penuh dari atas ke bawah pada Streamlit.
- Bangun UI dari komponen, ikat prop ke state var, dan hubungkan event ke handler; gunakan
rx.conddanrx.foreachuntuk rendering dinamis. - Computed var
@rx.varmenurunkan data; routing multi-halaman, rute dinamis, danrx.redirectmenangani navigasi. - Handler async dengan
yieldmen-stream pembaruan progresif;rx.Modelmenyediakan akses basis data bawaan; event background menangani pekerjaan berdurasi panjang. - Kirim ke produksi dengan
reflex export,reflex deploy, atau kontainer Docker di balik reverse proxy.
Dengan blok-blok bangunan ini, Anda dapat mengembangkan satu file menjadi aplikasi multi-halaman yang persisten dan mudah dipelihara — semuanya dalam Python murni.