Xây dựng Web Dashboard Full-stack bằng Python với Reflex — Không cần JavaScript

Python tutorial - IT technology blog
Python tutorial - IT technology blog

Mình bắt đầu nghịch Reflex khi dự án automation đang ở khoảng 1500 dòng và cần một cái giao diện để theo dõi trạng thái các job. Không muốn học thêm React, không muốn setup Node.js, không muốn viết một chữ JavaScript nào. Reflex (tên cũ là Pynecone) giải quyết đúng vấn đề đó: toàn bộ frontend + backend viết bằng Python thuần, framework tự biên dịch sang React ở phía sau.

Quick Start: Dashboard chạy được trong 5 phút

Không cần cài Node.js hay bất cứ thứ gì ngoài Python. Reflex tự lo phần còn lại:

pip install reflex psutil
mkdir system-dashboard && cd system-dashboard
reflex init

Sau khi init xong, mở file system_dashboard/system_dashboard.py và thay toàn bộ nội dung bằng dashboard hiển thị CPU và RAM thực tế:

import reflex as rx
import psutil

class State(rx.State):
    cpu_usage: float = 0.0
    memory_usage: float = 0.0

    def refresh(self):
        self.cpu_usage = psutil.cpu_percent(interval=0.2)
        self.memory_usage = psutil.virtual_memory().percent

def metric_card(label: str, value: str, color: str = "blue") -> rx.Component:
    return rx.box(
        rx.text(label, color="gray", font_size="0.85rem"),
        rx.text(value, font_size="2.2rem", font_weight="bold", color=color),
        padding="1.5rem",
        border="1px solid #e2e8f0",
        border_radius="0.75rem",
        background="white",
        min_width="180px",
    )

def index() -> rx.Component:
    return rx.vstack(
        rx.heading("System Monitor", size="xl", margin_bottom="1rem"),
        rx.hstack(
            metric_card("CPU", f"{State.cpu_usage:.1f}%", "blue"),
            metric_card("RAM", f"{State.memory_usage:.1f}%", "green"),
            spacing="1rem",
        ),
        rx.button(
            "Refresh",
            on_click=State.refresh,
            color_scheme="blue",
            margin_top="1rem",
        ),
        padding="2rem",
        align_items="start",
    )

app = rx.App()
app.add_page(index, route="/", on_load=State.refresh)
app.compile()

Chạy lên:

reflex run

Mở http://localhost:3000 — bạn đã có dashboard hiển thị CPU và RAM thực tế của máy. Click “Refresh” để cập nhật. Toàn bộ viết bằng Python, không một dòng JavaScript hay HTML nào.

Reflex hoạt động như thế nào?

Cơ chế này là nguồn gốc của hầu hết lỗi khó hiểu khi mới dùng Reflex. Hiểu rõ nó, bạn tiết kiệm được ít nhất vài giờ debug.

State — nguồn sự thật duy nhất

Mọi dữ liệu cần render đều nằm trong class State(rx.State). Khi giá trị trong State thay đổi, Reflex tự động cập nhật UI qua WebSocket — không cần viết event listener hay DOM manipulation thủ công.

class DashboardState(rx.State):
    items: list[dict] = []       # List → render thành table/list
    is_loading: bool = False     # Bool → điều kiện render spinner
    filter_text: str = ""        # String → input binding

    # Computed var — tự cache, chỉ recalculate khi dependency đổi
    @rx.var
    def filtered_items(self) -> list[dict]:
        if not self.filter_text:
            return self.items
        return [i for i in self.items if self.filter_text.lower() in i["name"].lower()]

    # Async handler với intermediate state
    async def load_data(self):
        self.is_loading = True
        yield  # Render trạng thái loading ngay lập tức

        import httpx
        async with httpx.AsyncClient() as client:
            resp = await client.get("https://api.example.com/items")
            self.items = resp.json()

        self.is_loading = False

Trick quan trọng: dùng yield trong async handler để Reflex render intermediate state trước khi tiếp tục xử lý. Thiếu yield, spinner sẽ không hiện — UI chỉ update sau khi toàn bộ handler chạy xong.

Components — xếp hình UI

Bộ component mặc định đủ dùng cho 90% dashboard mà không cần cài thêm gì:

  • Layout: rx.vstack, rx.hstack, rx.grid, rx.flex, rx.box
  • Typography: rx.heading, rx.text, rx.markdown
  • Input: rx.input, rx.select, rx.checkbox, rx.slider
  • Data display: rx.table, rx.badge, rx.progress, rx.stat
  • Charts: rx.recharts.* — tích hợp sẵn Recharts library

Thêm biểu đồ Line Chart theo dõi CPU real-time

Real-time chart là thứ biến dashboard từ nice-to-have thành thực sự cần thiết. Recharts tích hợp sẵn trong Reflex — không cần npm install, không cần import thêm gì. Rolling window 30 điểm đủ để bắt spike ngắn mà WebSocket vẫn nhẹ:

from datetime import datetime

class MonitorState(rx.State):
    history: list[dict] = []  # [{"time": "14:30:01", "cpu": 45.2}, ...]

    def record(self):
        self.history.append({
            "time": datetime.now().strftime("%H:%M:%S"),
            "cpu": psutil.cpu_percent(interval=0.2),
        })
        # Giữ 30 data points gần nhất
        if len(self.history) > 30:
            self.history = self.history[-30:]

def cpu_chart() -> rx.Component:
    return rx.recharts.line_chart(
        rx.recharts.line(
            data_key="cpu",
            stroke="#3b82f6",
            stroke_width=2,
            dot=False,
        ),
        rx.recharts.x_axis(data_key="time", tick={"fontSize": 11}),
        rx.recharts.y_axis(domain=[0, 100]),
        rx.recharts.tooltip(),
        rx.recharts.cartesian_grid(stroke_dasharray="3 3", opacity=0.4),
        data=MonitorState.history,
        width="100%",
        height=280,
    )

Nâng cao: Auto-refresh và kết nối Database

Tự động cập nhật mỗi N giây

Background task giải quyết polling mà không cần user phải click liên tục:

import asyncio

class State(rx.State):
    running: bool = False

    @rx.background
    async def start_monitor(self):
        self.running = True
        while True:
            async with self:
                self.record()  # Cập nhật metrics
            await asyncio.sleep(5)  # Nghỉ 5 giây

    def stop_monitor(self):
        self.running = False

Kết nối SQLite với SQLModel tích hợp sẵn

Reflex đi kèm SQLModel — define model và query thẳng trong State, không cần setup ORM riêng:

import reflex as rx
from sqlmodel import Field

class Task(rx.Model, table=True):
    id: int | None = Field(default=None, primary_key=True)
    title: str
    status: str = "pending"  # pending / done

class TaskState(rx.State):
    tasks: list[Task] = []

    def load(self):
        with rx.session() as db:
            self.tasks = db.exec(Task.select()).all()

    def complete_task(self, task_id: int):
        with rx.session() as db:
            task = db.get(Task, task_id)
            task.status = "done"
            db.add(task)
            db.commit()
        self.load()  # Reload để cập nhật UI

Tips thực tế từ kinh nghiệm

Dự án automation lớn dần từ 200 lên 2000 dòng. Khi thêm Reflex dashboard vào, mình mắc đủ thứ lỗi mất vài ngày debug — ghi lại đây để bạn khỏi lặp lại:

1. Tách State theo domain, không để tất cả vào một class

State khổng lồ 50 variables là ác mộng bảo trì. Chia nhỏ theo chức năng:

# Tốt hơn nhiều
class MetricState(rx.State):
    cpu: float = 0.0
    memory: float = 0.0

class TaskState(rx.State):
    tasks: list[dict] = []
    filter: str = "all"

2. Cẩn thận với kích thước State

State được serialize qua WebSocket mỗi lần update. Lưu 10,000 rows trong State → mỗi lần bấm nút là gửi 10,000 rows đó xuống client. Giải pháp: chỉ lưu trang hiện tại, pagination server-side.

3. Dùng rx.cond thay vì Python if trong component

# Sai — Python if không reactive
def index():
    if State.is_loading:  # Không hoạt động như bạn nghĩ
        return rx.spinner()
    return rx.table(...)

# Đúng — rx.cond reactive với state
def index():
    return rx.cond(
        State.is_loading,
        rx.spinner(),
        rx.table(...),
    )

4. Deploy nhanh với Reflex Cloud

# Đăng nhập và deploy — free tier có sẵn
reflex login
reflex deploy

Hoặc tự host trên VPS: chạy reflex run --env prod rồi dùng nginx reverse proxy — cổng 3000 cho frontend, 8000 cho backend.

5. Hot reload hoạt động rất mượt

Save file → browser tự cập nhật trong 1-2 giây, không cần restart server. Ngoại lệ duy nhất: thêm route mới hoặc thay đổi schema rx.Model.

Mình không dùng Reflex cho mọi thứ. SEO quan trọng hay site công khai traffic cao thì Next.js hay Nuxt vẫn là lựa chọn đúng hơn. Nhưng internal dashboard, admin panel, monitoring tool — bất cứ thứ gì cần UI nhanh từ script Python có sẵn — Reflex tiết kiệm cho mình vài ngày mỗi lần.

Share: