Mỗi lần một founder đang chạy shop Rails nhờ tôi scope một dashboard B2B SaaS, đến tầm giờ thứ ba thì cùng một câu hỏi lại nổi lên: “hay là mình làm React luôn cho gọn?”. Câu hỏi này hợp lý. Câu trả lời thật thà thì nhạt hơn cả hai phe đang kỳ vọng. Bài này là rubric tôi thực sự dùng — đúng thứ tôi sẽ đưa cho một founder không rành kỹ thuật trước khi ký hợp đồng — để chọn giữa Hotwire và React SPA chạy trên Rails API. Không có kiểu phẩy tay “còn tùy”. Một bộ tiêu chí gọn, các failure mode tôi luôn để mắt, và bản phác mỗi kiến trúc trông ra sao khi nó đang chạy ngon. Nếu bạn đến đây để lấy một câu chốt, tôi sẽ chốt. Chỉ là cái chốt đó không giống nhau cho mọi sản phẩm.
Chúng ta đang so cái gì
Hotwire là cái ô bao trùm Turbo và Stimulus — đi kèm Rails 7+, render HTML ở server, dùng cập nhật HTML-over-the-wire cộng với những Stimulus controller nhỏ cho hành vi phía client. “Framework” thực ra chỉ là vài thuộc tính data- và một WebSocket channel.
React-on-Rails-API nghĩa là một bundle frontend riêng (Vite, Next.js, gì cũng được) nói chuyện với Rails qua JSON, thường có React Query hoặc RTK để fetch data, cộng với session cookie hoặc auth dạng token.
Tôi đang giới hạn rất rõ ở dashboard B2B SaaS: các màn hình đã auth, hướng nội bộ, nặng dữ liệu. Không phải trang marketing. Không phải trải nghiệm tương tác kiểu consumer. Không phải thứ cần chạy offline. Nhóm dashboard này chiếm phần lớn việc tôi nhận làm — admin kiểu CRUD, settings multi-tenant, báo cáo nặng bảng/biểu đồ, internal tools — và đây cũng là nhóm mà hai stack thực sự cạnh tranh sòng phẳng.
Một rubric, không phải một thứ tôn giáo
Lựa chọn thường gói lại trong năm câu hỏi. Tôi chấm điểm thật mỗi câu trước khi viết bất kỳ dòng code nào.
| Câu hỏi | Nghiêng Hotwire | Nghiêng React |
|---|---|---|
| Màn hình bận nhất tương tác đến đâu? | Form, list, drill-down | Drag-drop, canvas, edit cộng tác realtime |
| Ai sẽ maintain nó 18 tháng nữa? | Team thiên backend hoặc một Rails dev solo | Frontend engineer chuyên trách trong biên chế |
| Bao nhiêu phần UI là “list các record”? | Phần lớn | Thiểu số |
| Có cần một native mobile app dùng chung API không? | Không, hoặc “có thể sau này” | Có, ngay trong round gọi vốn này |
| Realtime multi-user state có phải feature lõi? | Cập nhật và notification | Shared cursor, presence, conflict resolution |
Nếu ba dòng trở lên nghiêng Hotwire, tôi mặc định chọn Hotwire. Ba dòng trở lên nghiêng React, mặc định chọn React. Hai-ba là chỗ duy nhất tôi thực sự phải cân nhắc, và lúc đó tôi nâng trọng số dòng thứ hai — cấu trúc team — lên cao nhất, vì chọn sai stack so với team sẽ ngốn chi phí nhiều hơn tất cả các yếu tố còn lại cộng lại.
Hai thứ rubric này cố ý không tính vào: page-load performance và bundle size. Cả hai stack đều ổn cho dashboard đã auth chạy trên broadband. Nếu user của bạn ngồi kho hàng với kết nối phập phù, Hotwire thắng ở first paint — nhưng đó là một niche đáng để bàn riêng, không phải tiêu chí mặc định để phá thế hòa. Đừng chọn kiến trúc chỉ vì cắt được 80kb khỏi bundle mà user chỉ tải đúng một lần.
Mỗi bên âm thầm hỏng ở đâu
Failure mode của Hotwire không phải “nó không đủ tương tác”. Mà là những Stimulus controller đáng lẽ phải là một frontend framework. Bạn sẽ thấy điều này ở các codebase bắt đầu với một tương tác drag-drop và giờ có một Stimulus controller 600 dòng quản lý optimistic UI state, undo history, và ba modal lồng nhau. Đến đó thì bạn đang viết React mà không có công cụ của React. Cái Turbo Stream response chỉ update một cell biến thành Turbo Stream response re-render cả một tree, và bạn đã đốt đúng khoản ngân sách tiết kiệm được từ server rendering để tự implement reactivity một cách dở.
Failure mode của React thì ngược lại: re-implement Rails một cách dở bằng TypeScript. Tôi đã kế thừa những codebase React mà mỗi Rails model được mirror thành một TypeScript interface, mỗi controller thành một service class, mỗi validation thành một Zod schema lặp lại từ phía Rails. Repo frontend trở thành một backend thứ hai mà tình cờ không sở hữu database. Mỗi feature giờ cần hai endpoint — một trong Rails, một trong Next.js BFF — và team không nói nổi cái nào sở hữu logic authorization.
Dashboard không phải là nơi sản phẩm sống. Sản phẩm sống trong data model. Hãy chọn frontend nào cho phép số lượng người ít nhất có thể sở hữu data model đó từ đầu đến cuối.
Các anti-pattern tôi từ chối ship: Stimulus controller dài quá ~150 dòng. React component fetch từ hơn hai endpoint khác nhau. Bất kỳ kiến trúc nào mà một dev junior không trả lời nổi câu “validation này sống ở đâu” trong vòng ba mươi giây. Nếu câu trả lời là “cả hai chỗ”, chính đó là bug.
Phiên bản Hotwire trong thực tế trông ra sao
Với một dashboard B2B điển hình — ví dụ admin panel cho một vertical SaaS với users, organizations, billing, và một workflow engine — bố cục Hotwire tôi hay với tay tới đại khái thế này:
<%# app/views/workflows/index.html.erb %>
<%= turbo_frame_tag "workflows", src: workflows_path(format: :turbo_stream) do %>
<%= render @workflows %>
<% end %>
# app/controllers/workflows_controller.rb
def update
@workflow.update!(workflow_params)
respond_to do |format|
format.turbo_stream
format.html { redirect_to @workflow }
end
end
Đó là cả pattern. Server render, Turbo Frame swap, Stimulus controller lo những thứ cần cảm giác tức thời — keyboard shortcut, toggle optimistic, mấy chi tiết form cho mượt. Một team chỉ một Rails engineer có thể ship và maintain ngần này. Data model và HTML đã render sống cùng repo, cùng PR, cùng một lần deploy.
Phiên bản React tương đương sẽ là một Rails API expose JSON, một app Next.js hoặc Vite consume nó, React Query để cache, và nhiều khả năng có thêm một TypeScript client được generate để giữ API contract trung thực. Không có gì sai cả — chỉ là nhiều phần hơn, nhiều PR hơn, nhiều target deploy hơn, và thêm ít nhất một người trong bảng lương. Với một founder đang thuê contractor ship trong 4–8 tuần, phiên bản Hotwire thường là phiên bản duy nhất vừa với runway. Với một team Series A có ba frontend engineer, phiên bản React mới là cái cho phép họ làm song song mà không giẫm chân nhau.
Về nhân lực thị trường: senior Rails ở Việt Nam ship được dashboard kiểu này một cách tự tin thường tính khoảng 800k–1,5tr VNĐ/giờ tùy kinh nghiệm và địa lý của client. Nếu bạn build cho thị trường nước ngoài và xuất hóa đơn theo team Việt Nam, con số quy ra USD vẫn là kiểu hợp đồng cross-border SaaS mà nhiều shop Rails ở VN đang nhận trong 2026.
Thành công trông như thế nào
Một dashboard Hotwire chạy ổn sau sáu tháng nhìn thế này: phần lớn PR chạm vào một file trong app/views, một file trong app/controllers, thi thoảng thêm một Stimulus controller. Feature mới ship dưới dạng Turbo Frame nhét vào layout sẵn có. Cả team mô tả được toàn bộ frontend trong một buổi chiều. Chuyển trang cảm giác nhanh vì JSON payload chưa bao giờ là bottleneck — query database mới là, và đó là vấn đề Rails mà bạn đã biết cách xử.
Một dashboard React-on-Rails-API chạy ổn sau sáu tháng nhìn thế này: API contract được version và document, deploy frontend và backend tách rời, có một câu chuyện auth rõ ràng không phụ thuộc session cookie. Team có ít nhất một engineer thực sự sở hữu repo frontend và trả lời được các câu hỏi về render performance, bundle split, và accessibility.
Phép chẩn đoán cho cả hai: một engineer mới có ship được một feature không tầm thường end-to-end trong tuần đầu không? Nếu được, kiến trúc đang hoạt động. Nếu họ mất ba ngày để hỏi repo nào sở hữu cái gì, thì không.
Khi nào rubric này không áp dụng
Nó không áp dụng nếu bạn ship sản phẩm consumer mà một độ trễ tương tác dưới một giây cũng đủ làm bạn mất lượt đăng ký. Nó không áp dụng nếu dashboard chỉ là phụ — sản phẩm thật của bạn là app iOS và web chỉ là tool admin. Nó không áp dụng nếu bạn kế thừa một codebase có sẵn, trong trường hợp đó “thứ đang có” vượt qua mọi dòng trong bảng; rewrite đốt runway nhiều hơn tiết kiệm được. Và nó không áp dụng nếu team của bạn, kiểu, hai cựu engineer Vercel năm năm chưa chạm Rails. Hãy dùng thứ team có thể bảo vệ được lúc 2 giờ sáng.
Một claim đáng để bạn phản bác
Đây là phần có thể bị bác bỏ: với một dashboard B2B SaaS được maintain bởi ít hơn ba engineer trong hai năm đầu, chọn React thay vì Hotwire sẽ ngốn của bạn ít nhất một engineer-month mỗi năm cho chi phí phối hợp — duplicate schema, version API, bridging auth, choreography deploy — mà bạn lẽ ra không phải tiêu nếu chọn Hotwire. Tôi không chứng minh nổi nếu không nhìn codebase của bạn, nhưng tôi sẵn sàng đặt cược một buổi discovery call cho chuyện này. Nếu bạn đo được điều ngược lại, tôi muốn nhìn số.