In this post
I write React on most projects and wrote Vue for the Condotel system at Cozrum. This post does not rank them. It is about the places I tripped while switching back and forth, and why knowing Vue made me better at React.
There are already a hundred "Vue vs React" posts. Most end with some version of "it depends on your project and your team", which is true and leaves you knowing nothing new. So I am trying something else: no scores, just the times I sat staring at a screen wondering why it had not updated.
Some context. Most of my time goes to React: the NowCheckin website and CMS, then KroXpaF, now Vinova. The exception is my time at Cozrum, where I worked on the Condotel management system, which is written in Vue. So I am a React person who did Vue for a while and came back, and everything below leans that way. Adjust accordingly.
On my first day of Vue I thought it had a bug
I pushed an item into an array and the screen updated.
The reflex of someone who has written React for a long time is: that cannot be right. I even opened DevTools to see what had triggered a render. There was no bug. Vue watches the data for me. React waits to be told.
One asks "which data just changed?". The other asks "what does the interface look like now?". This whole post is really about that difference.
The same booking form
Guests, nights, a total. The Condotel system has dozens of screens like this.
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>
</>
);
}In Vue, v-model handles both directions: it puts the value in the input and writes it back as the user types. In React I write both myself. It is longer, but nothing happens that I cannot see in the code.
Look at the total line. In Vue it is a computed: Vue knows it depends on guests and nights and only recalculates when one of them changes. In React it is plain multiplication that runs on every render. For one multiplication that is fine. For anything heavier I used to wrap it in useMemo.
How Vue tracks data
This part is a little theoretical, but it is worth it, because it explains the trap below.
In Vue 3, reactive() wraps your object in a Proxy. Every time you read a property, Vue records "this place uses that property". Every time you write, Vue notifies exactly the places that read it. ref() does the same through the getter and setter of .value, which is why I type .value all day.
Now the trap:
const booking = reactive({guests: 2, nights: 3});
let {guests} = booking; // guests is now just the number 2
guests++; // the screen does not change
When I pull guests out of the object, I take a number with me and leave the Proxy behind. From then on Vue cannot see it. I hit this in my first week, and what makes it nasty is that the code looks perfectly reasonable.
The fix is toRefs:
const {guests} = toRefs(booking);
guests.value++;
React is the opposite: nothing happens by itself
Back in React after months of Vue, I made exactly the reverse mistake.
booking.guests.push(newGuest); // mutates the old array
setBooking(booking); // React sees the same object and does not re-render
setBooking({...booking, guests: [...booking.guests, newGuest]});
React does not track anything inside an object. It only checks whether what I pass to setBooking is a new object. Mutate the old one and hand the same one back, and as far as React is concerned nothing changed.
The last line is the right way: a new object, a new array. It is wordy, but in return I always know exactly when the screen will change, because I am the one who called it.
watch and useEffect, where I get it wrong most
When the room or the number of nights changes, fetch a new price.
React
useEffect(() => {
fetchPrice(roomId, nights).then(setPrice);
}, [roomId]); // forgot nightsVue
watch([roomId, nights], async ([room, n]) => {
price.value = await fetchPrice(room, n);
});In React I have to list what the effect depends on. Miss one, like nights above, and the price is wrong with no error to tell me, unless the React ESLint rule is on. In Vue, watch makes me say what I am watching up front, so it is harder to forget.
One more thing cost me half a morning: in development with StrictMode, React deliberately runs effects twice to expose ones that do not clean up properly. The first time I saw my API called twice, I went hunting for a bug somewhere else entirely.
Does the React Compiler make this easier?
Partly, yes.
The React Compiler reached a stable 1.0 in October 2025. It adds memoization at build time, so most of the places where I used to type useMemo and useCallback by hand no longer need them. Both hooks still exist, as an escape hatch for when I need precise control.
But it does not write useEffect dependency lists for me. The place I get wrong most is still there.
Vue is not standing still either. The current stable release is 3.5. Version 3.6 is at release candidate stage as I write this, and the Vue team is working on Vapor Mode, a compilation strategy that drops the Virtual DOM. I have not tried it, so I will say no more.
Templates or JSX
This one is pure taste, so take it as my opinion.
Vue templates are close to HTML, so anyone who knows HTML can read the layout. New people on the Condotel project read templates much faster than newcomers read JSX. v-if and v-for are compact too.
JSX is JavaScript. To loop you use map, to branch you use a ternary. There is no extra syntax to learn and TypeScript checks all of it. In return, a complicated JSX component gets hard to look at.
For screens full of forms, I think templates win. For components with tricky display logic, JSX is more comfortable.
All of it in one table
| Vue | React | |
|---|---|---|
| Updating the screen | Tracks data by itself | I call a setter |
| Changing data | Mutate in place | Make a new copy |
| Derived values | computed, cached | Recomputed on render, the compiler handles memoization |
| Reacting to change | watch, names what it watches | useEffect, you list the dependencies |
| Forms with many fields | Very compact thanks to v-model | Longer, or use a library |
| Usual traps | Forgetting .value, losing reactivity by destructuring | Mutating state, missing a dependency |
| Ecosystem | Enough, and consistent | Huge, with many choices |
Which one I pick for what
The Condotel system is a forest of forms: properties, reservations, tenants, dozens of fields on every screen. Vue suits that so well that I was noticeably faster than when I built similar screens in React.
NowCheckin is different. The website runs on Next.js, the CMS on React with Vite, and the mobile app is React Native. Three places share one way of thinking and one kind of component, and I do not have to switch my head when moving between them. React was the obvious choice there.
So if you ask me which to learn first, the honest answer is: the one the company you want to join is using. If the choice is free, learn one properly and then spend a few months with the other. The reason is in the next section.
The biggest surprise
Knowing Vue made me write better React.
Vue forces a distinction: what is source data (ref) and what is merely calculated from source data (computed). Carrying that habit back to React, I started asking before every useState: "does this really need to be state, or can it be calculated from other state?".
Very often it can be calculated. And each time, a useEffect that existed only to keep two pieces of state in sync disappears, along with a bug waiting to happen.
That is all. I lean towards React, so I may have been unfair to Vue in places. If you have written Vue for years and see something wrong, I would like to hear it. Thanks for reading.