Trong bài này
Mình viết React ở hầu hết dự án và viết Vue cho hệ thống Condotel ở Cozrum. Bài này không xếp hạng cái nào hơn. Nó kể lại những chỗ mình vấp khi nhảy qua nhảy lại giữa hai bên, và vì sao biết Vue lại làm mình viết React tốt lên.
Chào các bạn.
Trên mạng đã có cả trăm bài "Vue vs React". Phần lớn kết thúc bằng một câu kiểu "tùy dự án và tùy team", đúng nhưng đọc xong không biết thêm được gì. Nên bài này mình thử làm khác đi: không chấm điểm, chỉ kể lại những lần mình ngồi nhìn màn hình và tự hỏi "ủa sao nó không cập nhật".
Hoàn cảnh của mình thế này. Phần lớn thời gian mình viết React: website và CMS của NowCheckin, rồi KroXpaF, giờ là ở Vinova. Riêng thời gian ở Cozrum, mình làm hệ thống quản lý Condotel, và nó viết bằng Vue. Tức là mình là dân React đi làm Vue một thời gian rồi quay về, và cái nhìn trong bài này bị lệch theo hướng đó. Các bạn nhớ trừ hao.
Ngày đầu viết Vue, mình tưởng nó có bug
Mình push một phần tử vào mảng, và giao diện tự cập nhật.
Phản xạ của một người viết React lâu ngày là: cái này sai sai. Mình còn mở DevTools ra xem có ai gọi lại hàm render không. Không có bug nào cả. Vue theo dõi dữ liệu giùm mình. React thì chờ mình nói.
Một bên hỏi "dữ liệu nào vừa đổi?". Bên kia hỏi "giao diện bây giờ trông ra sao?". Cả bài này thật ra chỉ xoay quanh khác biệt đó.
Cùng một form đặt phòng
Số khách, số đêm, tính ra tổng tiền. Màn hình kiểu này bên Condotel có cả chục cái.
Vue
<script setup>
import {ref, computed} from 'vue';
const guests = ref(2);
const nights = ref(3);
const total = computed(() => guests.value * nights.value * 450000);
</script>
<template>
<input v-model.number="guests" type="number" />
<p>{{ total.toLocaleString() }} đ</p>
</template>React
function Booking() {
const [guests, setGuests] = useState(2);
const [nights] = useState(3);
const total = guests * nights * 450000;
return (
<>
<input type="number" value={guests} onChange={(e) => setGuests(Number(e.target.value))} />
<p>{total.toLocaleString()} đ</p>
</>
);
}Bên Vue, v-model lo cả hai chiều: đọc giá trị ra ô nhập và ghi ngược lại khi người dùng gõ. Bên React mình tự viết cả hai. Dài hơn, nhưng không có gì xảy ra mà mình không nhìn thấy trong code.
Để ý dòng total. Bên Vue nó là computed: Vue tự biết nó phụ thuộc vào guests và nights, và chỉ tính lại khi một trong hai đổi. Bên React nó chỉ là một phép nhân, được tính lại mỗi lần component render. Với một phép nhân thì chẳng sao. Với thứ nặng hơn thì trước đây mình phải bọc useMemo.
Vue theo dõi dữ liệu bằng cách nào
Đoạn này hơi lý thuyết nhưng đáng đọc, vì hiểu nó thì tránh được cái bẫy bên dưới.
Trong Vue 3, reactive() bọc object của mình trong một Proxy. Mỗi lần mình đọc một thuộc tính, Vue ghi lại "chỗ này đang dùng thuộc tính đó". Mỗi lần mình ghi, Vue báo cho đúng những chỗ đã đọc. Còn ref() thì làm điều tương tự qua getter và setter của .value, đó là lý do mình phải gõ .value hoài.
Giờ tới cái bẫy:
const booking = reactive({guests: 2, nights: 3});
let {guests} = booking; // guests giờ chỉ là số 2 bình thường
guests++; // màn hình không đổi
Khi mình tách guests ra khỏi object, mình lấy đi một con số, không lấy theo cái Proxy. Từ đó Vue không còn nhìn thấy nó nữa. Mình dính cái này ngay tuần đầu, và nó khó chịu ở chỗ code trông hoàn toàn hợp lý.
Cách sửa là dùng toRefs:
const {guests} = toRefs(booking);
guests.value++;
React thì ngược lại, không có gì tự xảy ra
Về lại React sau mấy tháng viết Vue, mình mắc đúng lỗi ngược lại.
booking.guests.push(newGuest); // sửa thẳng vào mảng cũ
setBooking(booking); // React thấy vẫn là object đó, không render lại
setBooking({...booking, guests: [...booking.guests, newGuest]});
React không theo dõi gì bên trong object. Nó chỉ so xem cái mình đưa vào setBooking có phải là một object mới hay không. Sửa thẳng vào cái cũ rồi đưa lại đúng cái cũ thì với React là chưa có gì thay đổi.
Dòng cuối mới là cách đúng: tạo object mới, mảng mới. Dài dòng, nhưng đổi lại mình luôn biết chính xác lúc nào giao diện sẽ đổi, vì chính tay mình gọi.
watch với useEffect, chỗ mình sai nhiều nhất
Khi phòng hoặc số đêm đổi thì gọi API lấy giá mới.
React
useEffect(() => {
fetchPrice(roomId, nights).then(setPrice);
}, [roomId]); // quên nightsVue
watch([roomId, nights], async ([room, n]) => {
price.value = await fetchPrice(room, n);
});Bên React, mình phải tự liệt kê những thứ effect phụ thuộc vào. Quên một cái, như nights ở trên, là giá hiển thị sai mà không có lỗi nào báo, trừ khi bật ESLint rule của React. Bên Vue, watch bắt mình nói rõ đang theo dõi cái gì ngay từ đầu, nên khó quên hơn.
Còn một chuyện làm mình mất nửa buổi: ở chế độ dev với StrictMode, React cố tình chạy effect hai lần để lộ ra những effect dọn dẹp không sạch. Lần đầu thấy API bị gọi đôi, mình đã đi tìm bug ở một chỗ hoàn toàn khác.
React Compiler có làm mọi thứ dễ hơn không?
Có, một phần.
React Compiler ra bản 1.0 ổn định từ tháng 10 năm 2025. Nó tự thêm memo hóa lúc build, nên phần lớn những chỗ trước đây mình phải gõ useMemo và useCallback bằng tay thì giờ không cần nữa. Hai hook đó vẫn còn, dùng như lối thoát khi mình cần kiểm soát chính xác.
Nhưng nó không viết danh sách phụ thuộc của useEffect giùm mình. Chỗ mình hay sai nhất vẫn còn nguyên.
Bên Vue cũng không đứng yên. Bản ổn định hiện tại là 3.5. Bản 3.6 đang ở giai đoạn release candidate lúc mình viết bài này, và team Vue đang làm Vapor Mode, một cách biên dịch bỏ hẳn Virtual DOM. Mình chưa dùng thử nên không dám nói gì hơn.
Template hay JSX
Cái này thuần sở thích, mình nói ý mình thôi.
Template của Vue gần với HTML, nên ai biết HTML nhìn vào là hiểu được bố cục. Người mới vào dự án Condotel đọc template nhanh hơn hẳn so với người mới đọc JSX. v-if, v-for cũng gọn.
JSX thì là JavaScript. Muốn lặp thì map, muốn rẽ nhánh thì dùng toán tử ba ngôi. Không phải học thêm cú pháp riêng, và TypeScript kiểm tra được hết. Bù lại, một component JSX phức tạp nhìn khá rối.
Theo mình thì với màn hình nhiều form, template thắng. Với component có nhiều logic hiển thị lắt léo, JSX dễ chịu hơn.
Tóm lại thành một bảng
| Vue | React | |
|---|---|---|
| Cập nhật giao diện | Tự theo dõi dữ liệu | Mình gọi hàm set |
| Sửa dữ liệu | Sửa thẳng | Tạo bản mới |
| Giá trị tính ra | computed, tự cache | Tính lại mỗi lần render, compiler lo phần memo |
| Phản ứng khi dữ liệu đổi | watch, nói rõ theo dõi gì | useEffect, tự liệt kê phụ thuộc |
| Form nhiều trường | Rất gọn nhờ v-model | Dài hơn, hoặc dùng thư viện |
| Bẫy hay dính | Quên .value, tách object làm mất reactivity | Sửa thẳng state, quên phụ thuộc |
| Hệ sinh thái | Đủ dùng, đồng bộ | Rất lớn, nhiều lựa chọn |
Mình chọn cái nào cho việc gì
Hệ thống Condotel là một rừng form: tài sản, đặt phòng, khách thuê, màn nào cũng vài chục trường. Vue hợp với việc này tới mức mình viết nhanh hơn thấy rõ so với lúc làm màn hình tương tự bằng React.
Còn NowCheckin thì khác. Website chạy Next.js, CMS chạy React với Vite, app di động là React Native. Ba chỗ dùng chung một cách nghĩ, một kiểu component, và mình không phải đổi đầu óc khi chuyển từ cái này sang cái kia. Ở đó React là lựa chọn hiển nhiên.
Nên nếu các bạn hỏi mình nên học cái nào trước, câu trả lời thật lòng là: cái mà công ty các bạn định xin vào đang dùng. Còn nếu được chọn tự do thì học một cái cho chắc rồi thử cái còn lại vài tháng. Lý do nằm ở đoạn dưới.
Điều bất ngờ nhất
Biết Vue làm mình viết React tốt lên.
Vue bắt mình phân biệt hai thứ: cái gì là dữ liệu gốc (ref) và cái gì chỉ là thứ tính ra từ dữ liệu gốc (computed). Mang thói quen đó về React, mình bắt đầu tự hỏi trước mỗi cái useState: "cái này có thật sự cần là state không, hay nó tính ra được từ state khác?".
Rất nhiều lần câu trả lời là tính ra được. Và mỗi lần như vậy, một cái useEffect dùng để đồng bộ hai state với nhau biến mất, kéo theo một lỗi tiềm ẩn.
Bài tới đây là hết. Mình thiên về React nên có thể mình chưa công bằng với Vue ở vài chỗ, bạn nào viết Vue lâu năm thấy sai thì chỉ mình với nhé. Cảm ơn các bạn đã đọc.