htmx — Hypermedia Thay JSON Cho Web App
htmx xây dynamic web app bằng HTML attributes, không cần JavaScript. Gọi API, swap DOM, validation — tất cả từ server-rendered HTML.
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 -->htmlKhô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#
// React App điển hình
// 1. Fetch JSON từ API
// 2. Parse JSON
// 3. setState
// 4. Re-render component
// 5. Handle loading/error states
// 6. Handle stale data
// 7. Bundle, build, deploy
const [data, setData] = useState(null)
const [loading, setLoading] = useState(true)
useEffect(() => {
fetch('/api/users')
.then(r => r.json()) // JSON parse
.then(d => { setData(d); setLoading(false) })
}, [])typescripthtmx#
<!-- Chỉ cần HTML -->
<div hx-get="/api/users" hx-trigger="load">
Loading...
</div>htmlServer 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 htmxhtmlKhô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>htmlhx-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>htmlhx-swap — Cách đặt vào DOM#
<!-- innerHTML (mặc định) -->
<div hx-get="/api/content" hx-swap="innerHTML"></div>
<!-- outerHTML — thay thế chính element đó -->
<div hx-get="/api/replace" hx-swap="outerHTML"></div>
<!-- beforebegin / afterbegin / beforeend / afterend -->
<div hx-get="/api/more" hx-swap="beforeend" hx-target="#list">
Load More
</div>
<!-- delete — xoá target -->
<button hx-delete="/api/todos/1" hx-target="closest li" hx-swap="delete">
Done
</button>
<!-- none — không swap (dùng cho side effect) -->
<button hx-post="/api/log" hx-swap="none">Log</button>htmlhx-trigger — Khi nào gửi request#
<!-- Mặc định: click cho button, submit cho form, change cho input -->
<button hx-get="/api/data">Click me</button>
<!-- load — khi element xuất hiện trong DOM -->
<div hx-get="/api/data" hx-trigger="load">Loading...</div>
<!-- reveal — khi element scroll vào viewport -->
<img hx-get="/api/image" hx-trigger="reveal">
<!-- every — interval -->
<div hx-get="/api/clock" hx-trigger="every 5s">--:--</div>
<!-- keyup + debounce -->
<input hx-get="/api/search"
hx-trigger="keyup changed delay:300ms"
hx-target="#results">
<!-- custom event -->
<button hx-get="/api/refresh" hx-trigger="refresh from:body">
Manual Refresh
</button>htmlForm Validation#
<!-- Real-time validation với htmx -->
<form hx-post="/api/users" hx-target="#form-errors">
<div>
<input name="email" type="email"
hx-post="/api/validate/email"
hx-target="next .error"
hx-trigger="change">
<span class="error"></span>
</div>
<div>
<input name="password" type="password"
hx-post="/api/validate/password"
hx-target="next .error"
hx-trigger="keyup changed delay:300ms">
<span class="error"></span>
</div>
<button type="submit">Register</button>
</form>
<div id="form-errors"></div>htmlServer trả HTML fragment — error message hoặc success indicator.
So Sánh: React vs htmx#
Todo App — React#
function TodoApp() {
const [todos, setTodos] = useState([])
useEffect(() => {
fetch('/api/todos')
.then(r => r.json())
.then(setTodos)
}, [])
const addTodo = async (text) => {
const res = await fetch('/api/todos', {
method: 'POST',
body: JSON.stringify({ text }),
})
const todo = await res.json()
setTodos([...todos, todo])
}
return (
<ul>
{todos.map(t => (
<li key={t.id}>
<span>{t.text}</span>
<button onClick={() => deleteTodo(t.id)}>✕</button>
</li>
))}
</ul>
)
}tsxTodo 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>htmlServer-Side (Bất Kỳ Ngôn Ngữ Nào)#
Go#
func main() {
http.HandleFunc("/api/todos", func(w http.ResponseWriter, r *http.Request) {
todos := getTodos()
for _, t := range todos {
fmt.Fprintf(w, `<li>
<span>%s</span>
<button hx-delete="/api/todos/%d"
hx-target="closest li"
hx-swap="delete">✕</button>
</li>`, t.Text, t.ID)
}
})
http.HandleFunc("/api/todos/add", func(w http.ResponseWriter, r *http.Request) {
text := r.FormValue("text")
todo := insertTodo(text)
fmt.Fprintf(w, `<li>
<span>%s</span>
<button hx-delete="/api/todos/%d" ...>✕</button>
</li>`, todo.Text, todo.ID)
})
log.Fatal(http.ListenAndServe(":3000", nil))
}goNode.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)
})javascriptPython (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)pythonAdvanced 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>htmlLazy 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>htmlPolling#
<!-- Auto-refresh dashboard -->
<div hx-get="/api/dashboard"
hx-trigger="every 30s"
hx-target="this">
{{ dashboard_html|safe }}
</div>htmlWebSocket#
<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>htmlhtmx 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>htmlAlpine.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>htmlKhi 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 management | Redux, Zustand, Jotai | Server |
| API format | JSON | HTML |
| SEO | ⚠️ Cần SSR | ✅ Tự nhiên |
| Accessibility | ⚠️ Tuỳ implementation | ✅ HTML native |
| Learning curve | Cao | Thấ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à đủ.