blog.dopana

Back

htmx là thư viện cho phép bạn xây dynamic web app mà không cần viết JavaScript. Mọi thứ qua HTML attributes.

<!-- Thay thế JSON API + client render bằng HTML từ server -->
<button hx-get="/api/load-more" hx-target="#list" hx-swap="beforeend">
  Load More
</button>

<!-- Click → GET /api/load-more → server trả HTML → append vào #list -->
html

Không build step, không bundle, không SPA framework. Chỉ một <script> tag và HTML.

Tại Sao htmx?#

Vấn đề với SPA#

htmx#

<!-- Chỉ cần HTML -->
<div hx-get="/api/users" hx-trigger="load">
  Loading...
</div>
html

Server trả HTML, htmx đặt vào DOM. Không JSON, không state, không re-render logic.

Cài Đặt#

<!-- CDN — chỉ 1 dòng, ~14KB min+gzip -->
<script src="https://unpkg.com/htmx.org@2.0.4"></script>

<!-- Hoặc npm -->
npm install htmx
html

Không build step, không webpack, không vite config.

Core Attributes#

hx-get / hx-post / hx-put / hx-delete#

<!-- GET request -->
<button hx-get="/api/posts">Load Posts</button>

<!-- POST form -->
<form hx-post="/api/users">
  <input name="name" placeholder="Name">
  <button type="submit">Create</button>
</form>

<!-- PUT + DELETE -->
<button hx-put="/api/users/1" hx-target="#user-1">Update</button>
<button hx-delete="/api/users/1" hx-target="#user-1" hx-swap="delete">
  Delete
</button>
html

hx-target — Nơi đặt kết quả#

<!-- Kết quả vào #result -->
<input hx-get="/api/search" hx-target="#results" name="q">

<!-- Kết quả vào chính nó (mặc định) -->
<button hx-get="/api/refresh">Refresh</button>

<!-- closest — element cha gần nhất -->
<button hx-delete="/api/items/1" hx-target="closest li" hx-swap="delete">
  Delete
</button>
html

hx-swap — Cách đặt vào DOM#

hx-trigger — Khi nào gửi request#

Form Validation#

Server trả HTML fragment — error message hoặc success indicator.

So Sánh: React vs htmx#

Todo App — React#

Todo App — htmx#

<!-- Load danh sách -->
<div hx-get="/api/todos" hx-trigger="load" hx-swap="innerHTML">
  Loading...
</div>

<!-- Form thêm mới -->
<form hx-post="/api/todos" hx-target="#todo-list" hx-swap="beforeend">
  <input name="text" required>
  <button type="submit">Add</button>
</form>
html
<!-- Server trả HTML cho mỗi todo -->
<li>
  <span>Học htmx</span>
  <button hx-delete="/api/todos/1"
          hx-target="closest li"
          hx-swap="delete">

  </button>
</li>
html

Server-Side (Bất Kỳ Ngôn Ngữ Nào)#

Go#

Node.js (Express)#

app.get('/api/todos', (req, res) => {
  const todos = db.select().from(todosTable)
  const html = todos.map(t => `
    <li>
      <span>${t.text}</span>
      <button hx-delete="/api/todos/${t.id}"
              hx-target="closest li"
              hx-swap="delete">✕</button>
    </li>
  `).join('')
  res.send(html)
})
javascript

Python (FastAPI)#

@app.get("/api/todos")
async def get_todos():
    todos = await db.fetch_all("SELECT * FROM todos")
    html = ""
    for t in todos:
        html += f"""
        <li>
            <span>{t['text']}</span>
            <button hx-delete="/api/todos/{t['id']}"
                    hx-target="closest li"
                    hx-swap="delete">✕</button>
        </li>
        """
    return HTMLResponse(html)
python

Advanced Patterns#

Infinite Scroll#

<div hx-get="/api/posts?page=1" hx-trigger="load" id="post-list"></div>

<div hx-get="/api/posts?page=2"
     hx-trigger="revealed"
     hx-target="#post-list"
     hx-swap="beforeend">
  Loading more...
</div>
html

Lazy Loading#

<!-- Chỉ load khi user scroll đến -->
<div hx-get="/api/expensive-component"
     hx-trigger="reveal"
     hx-target="this">
  <div class="skeleton">Loading...</div>
</div>
html

Polling#

<!-- Auto-refresh dashboard -->
<div hx-get="/api/dashboard"
     hx-trigger="every 30s"
     hx-target="this">
  {{ dashboard_html|safe }}
</div>
html

WebSocket#

<div hx-ws="connect:/ws/chat">
  <div id="messages" hx-ws="receive:#messages">
    <!-- Messages appear here -->
  </div>
  <form hx-ws="send">
    <input name="message">
    <button type="submit">Send</button>
  </form>
</div>
html

htmx Ecosystem#

HyperScript#

Scripting companion cho htmx:

<button _="on click add .active to #sidebar">
  Toggle Sidebar
</button>

<button _="on click call navigator.clipboard.writeText('copied!')">
  Copy
</button>
html

Alpine.js#

Cho UI state phức tạp:

<div x-data="{ open: false }">
  <button @click="open = !open">Toggle</button>
  <div x-show="open" hx-get="/api/details" hx-trigger="load">
    Content
  </div>
</div>
html

Khi Nào Dùng htmx?#

✅ Nên dùng#

  • Server-rendered app — Django, Rails, Laravel, Go, Spring
  • CRUD app — form, table, search, pagination
  • Dashboard — polling, lazy load, real-time updates
  • Prototype/MVP — ship nhanh, không cần SPA stack
  • Team backend-heavy — frontend chỉ là HTML templates
  • Thay thế jQuery — legacy app cần dynamic behavior

❌ Không nên#

  • Rich interactive UI — game, canvas, drag-and-drop phức tạp
  • Real-time collaborative — Google Docs, Figma
  • Offline-first — PWA cần client logic phức tạp
  • Native mobile app — React Native, Flutter, SwiftUI
  • Team React chuyên — không có lý do chính đáng để đổi

Tổng Kết#

htmx là lời nhắc nhở: web vốn dĩ đã hypermedia. JavaScript là gia vị, không phải món chính.

<!-- Trước: SPA với JSON API
     Sau: HTML API với htmx -->

<!-- SPA:  → JSON → fetch → parse → setState → render -->
<!-- htmx: → HTML → swap vào DOM -->

<!-- htmx: 1 dòng HTML, 0 dòng JS -->
<button hx-post="/api/like" hx-target="this" hx-swap="outerHTML">
  👍 Like
</button>
html
SPA (React/Vue)htmx
Bundle~100KB+~14KB
Build step
State managementRedux, Zustand, JotaiServer
API formatJSONHTML
SEO⚠️ Cần SSR✅ Tự nhiên
Accessibility⚠️ Tuỳ implementation✅ HTML native
Learning curveCaoThấp

htmx không thay thế React cho mọi thứ. Nó thay thế React cho đa số web app — những app mà “CRUD + server-render” là đủ.

Tài liệu tham khảo#