Learning Vue for React Developers
Being familiar with multiple frameworks is pretty common in the front-end world. Most of them share the same foundation. A component-based…

Learning Vue for React Developers
Being familiar with multiple frameworks is pretty common in the front-end world. Most of them share the same foundation. A component-based architecture, state management, routing, declarative rendering, etc. Many of these concept are common between frameworks, and at the end of the day, it’s all just JavaScript after all.
If you already know React, learning Vue should be easy. Let me prove this by comparing some code examples.
Table of Contents
- Component Declaration
- Templating
- Reactive Data Declaration
- Event Handling
- Conditional Rendering
- Class Binding
- Lists
- Parent-to-Child Communication (Props)
- Child-to-Parent Communication (Emits)
- Derived State
- Component Performance
- Side Effects and Watchers
- Lifecycle Hooks
- Data Fetching
- CSS Styles
- Animations
- Custom Hooks and Composables
- Global State Management
- Tools
- Routing
1. Component Declaration
The first fundamental difference is in component declaration. In React, a component is just an exported function that has to return a template:
export function MyComponent() {
return <h1>Hello, React World!</h1>;
}
In Vue, components are declared inside a special file with the .vue extension, called Single File Components (SFC).
SFC have three distinct sections:
<template>
<!-- Template: The UI structure -->
<h1>Hello, Vue World!</h1>
</template>
<script>
// Script: Logic like variables and methods
</script>
<style scoped>
/\* Styles: Component-specific CSS \*/
</style>
All three sections are optional. We will learn more about them later on.
2. Templating
This is another fundamental difference. The templates in the previous examples might look like plain HTML, but they are not.
React uses JSX, a special syntax for representing the DOM. It has some differences, including many custom rules and the ability to write JavaScript inside it.
On the other hand Vue’s template syntax, looks much closer to plain HTML but also supports features like directives and JavaScript expressions.
This difference is behind many of the sections that follow.
3. Reactive Data Declaration
Both frameworks support reactivity. They automatically track changes and update every dependency, including the UI.
In React, reactive data is declared using the useState hook, which creates a state variable and a corresponding update function.
import { useState, useEffect } from "react";
export function MyComponent() {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
</div>
);
}
In this example, count is the reactive data, and setCount is the function that updates it. Whenever setCount is called, React re-renders the component to reflect the updated value of count. The single bracket { } syntax is used to display the value of a variable or to enter JavaScript mode within the template.
In Vue, reactive data is managed using the ref (or [reactive](/vue-3-why-both-ref-and-reactive-are-needed-344bb5da2593)) helpers from the Composition API. They both return a reactive variable.
<template>
<div>
<p>Count: {{ count }}</p>
</div>
</template>
<script setup>
import { ref } from "vue";
const count = ref(0);
count.value += 1;
</script>
Here count is made reactive using the ref helper. Every variable or function declared inside the script will be automatically available in the template. Vue uses the double brackets {{ }} syntax for interpolation.
Unlike React, where you need to explicitly call a state update function, Vue tracks changes to count automatically and re-renders the UI as needed. Inside the script section, we can update the value using count.value, in templates the .value can be omitted.
4. Event Handling
In React, events are handled in JSX using camelCase attributes, such as onClick.
import { useState } from "react";
export function MyComponent() {
const [count, setCount] = useState(0);
const handleClick = () => {
setCount(count + 1);
};
return (
<div>
<p>Count: {count}</p>
<button onClick={handleClick}>Increment</button>
</div>
);
}
React will attach the handleClick function to the button. When clicked, the function updates the state and re-renders the component.
Vue uses the same idea but with a slightly different syntax.
<template>
<div>
<p>Count: {{ count }}</p>
<button @click="increment">Increment</button>
</div>
</template>
<script setup>
import { ref } from "vue";
const count = ref(0);
const increment = () => {
count.value++;
};
</script>
The increment function is attached to the button with @click. When the button is clicked, the function updates the reactive variable, and the template updates automatically.
We can also use modifiers like .prevent, .stop, .once [and more](https://vuejs.org/guide/essentials/event-handling#event-modifiers) to control event behaviour directly in the template.
<button @click.prevent="increment">Increment</button>
5. Conditional Rendering
React and Vue handle conditional rendering in different ways.
In React, we can use a ternary operator or a [short-circuit evaluation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Nullish_coalescing) (&&) for conditional rendering.
import { useState } from "react";
export function MyComponent() {
const [isVisible, setIsVisible] = useState(true);
return (
<div>
{isVisible && <p>This is visible</p>}
{isVisible ? <p>This is visible</p> : <p>This is hidden</p>}
</div>
);
}
To handle multiple cases a nested ternary is often used.
{
condition1 ? (
<p>Condition 1</p>
) : condition2 ? (
<p>Condition 2</p>
) : (
<p>Else</p>
)
}
In Vue, conditional rendering is done using the built-in directives v-if and v-else
<template>
<p v-if="isVisible">This is visible</p>
<p v-else>This is hidden</p>
</template>
<script setup>
import { ref } from "vue";
const isVisible = ref(true);
</script>
Multiple cases are handled with v-else-if
<p v-if="condition1">Condition 1</p>
<p v-else-if="condition2">Condition 2</p>
<p v-else>Else</p>
6. Class Attribute Bindings
The ternary operation can also be used to conditionally apply a class in React.
import { useState } from "react";
export function MyComponent() {
const [isVisible, setIsVisible] = useState(true);
return (
<div>
<p
className={`message
${isVisible ? "visible" : "hidden"}
${isVisible ? "is-visible" : ""}`}
>
This is a conditionally styled message.
</p>
</div>
);
}
In Vue the syntax is a bit different with the class directive accepting an object as a parameter.
<template>
<p
:class="{
visible: isVisible,
hidden: !isVisible,
'is-visible': isVisible,
}"
>
This is a conditionally styled message.
</p>
</template>
<script setup>
import { ref } from "vue";
const isVisible = ref(true);
</script>
7. Lists
Iterations in React are done inside an interpolation with the JavaScript .map() method.
export function MyComponent() {
const items = ["Item 1", "Item 2", "Item 3"];
return (
<ul>
{items.map((item, index) => (
<li key={index}>{item}</li>
))}
</ul>
);
}
In Vue, we haveto usev-for another build-in directive.
<template>
<ul>
<li v-for="(item, index) in items" :key="index">{{ item }}</li>
</ul>
</template>
<script setup>
const items = ["Item 1", "Item 2", "Item 3"];
</script>
8. Parent-to-Child Communication (Props)
Both frameworks use the concept of props to pass data between components.
In React, props are passed as attributes in the template. They are read-only and can be accessed as arguments in the child component.
export function ParentComponent() {
const message = "Hello from React!";
return <ChildComponent message={message} />;
}
export function ChildComponent({ message }) {
return <p>{message}</p>;
}
Same idea in Vue but the child component needs to explicitly declare every prop using the defineProps helper. Each prop can be typed and can [have an optional validator or default value](https://vuejs.org/guide/components/props#prop-validation).
<!-- ParentComponent.vue -->
<template>
<ChildComponent message="Hello from Vue!" />
</template>
<script setup>
import ChildComponent from "./ChildComponent.vue";
</script>
<!-- ChildComponent.vue -->
<template>
<p>{{ message }}</p>
</template>
<script setup>
defineProps({
message: String,
});
</script>
9. Child-to-Parent Communication (Emits)
Props allow data to flow from parent to child, but what about the other direction?
React doesn’t have a special feature for this. Instead, we can pass a callback function as a prop to a child component, which can be used to update the parent’s state.
export function ParentComponent() {
const handleCustomEvent = (message) => {
console.log(message);
};
return <ChildComponent onCustomEvent={handleCustomEvent} />;
}
export function ChildComponent({ onCustomEvent }) {
return (
<button onClick={() => onCustomEvent("Hello from the child!")}>
Click Me
</button>
);
}
In Vue, a different pattern is followed. Child components can emit custom events using the defineEmits helper.
<!-- ChildComponent.vue -->
<template>
<button @click="sendMessage">Click Me</button>
</template>
<script setup>
const emit = defineEmits(["customEvent"]);
function sendMessage() {
emit("customEvent", "Hello from the child!");
}
</script>
<!-- ParentComponent.vue -->
<template>
<ChildComponent @customEvent="handleCustomEvent" />
</template>
<script setup>
import ChildComponent from "./ChildComponent.vue";
function handleCustomEvent(message) {
console.log(message);
}
</script>
The child component emits a customEvent with some optional data, and the parent listens to it using the @customEvent directive. The defineEmits helper provides type safety and clarity for declared events.
10. Derived State
In React, derived state is calculated using the useMemo hook, which memoizes the result to avoid unnecessary recalculations.
import { useState, useMemo } from "react";
export function MyComponent() {
const [firstName, setFirstName] = useState("Frodo");
const [lastName, setLastName] = useState("Baggins");
const fullName = useMemo(
() => `${firstName} ${lastName}`,
[firstName, lastName],
);
return <p>Full Name: {fullName}</p>;
}
Vue uses computed properties. Their value updates only when their dependencies change and have memorization built in.
<template>
<p>Full Name: {{ fullName }}</p>
</template>
<script setup>
import { ref, computed } from "vue";
const firstName = ref("Frodo");
const lastName = ref("Baggins");
const fullName = computed(() => `${firstName.value} ${lastName.value}`);
</script>
11. Component Performance
It’s important to know that in React, components re-render after every state or prop change. While this is usually efficient, it can become expensive when there are complex calculations or large components involved. To prevent unnecessary recalculations React provides tools like React.memo, useMemo, and useCallback.
React.memo prevents re-renders of a functional component if its props haven’t changed.
import React, { useState } from "react";
const ChildComponent = React.memo(({ count }) => {
console.log("Child rendered");
return <p>Count: {count}</p>;
});
export function ParentComponent() {
const [count, setCount] = useState(0);
return (
<div>
<ChildComponent count={count} />
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
useMemo avoids recalculating expensive functions unless their dependencies change. (Also see derived state section)
import { useState, useMemo } from "react";
export function MyComponent() {
const [count, setCount] = useState(0);
const expensiveCalculation = useMemo(() => {
console.log("Calculating...");
return count \* 2;
}, [count]);
return (
<div>
<p>Count: {count}</p>
<p>Calculated: {expensiveCalculation}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
useCallback memoizes functions to prevent their re-creation, particularly useful when passing callbacks as props.
import { useState, useCallback } from "react";
export function MyComponent() {
const [count, setCount] = useState(0);
const increment = useCallback(() => {
setCount((prevCount) => prevCount + 1);
}, []);
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>Increment</button>
</div>
);
}
The code to do the same optimizations in Vue is the following.
In Vue, many of the optimizations are handled automatically by its reactivity system. It tracks dependencies for reactive variables and recomputes only when necessary. There is no need for manual low level performance tuning.
12. Side Effects and Watchers
In React, useEffect is used to monitor specific values and execute a callback when those values change.
import { useState, useEffect } from "react";
export function MyComponent() {
const [count, setCount] = useState(0);
useEffect(() => {
console.log(`Count changed to ${count}`);
}, [count]);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
The dependency array [count] ensures the effect runs only when the count changes.
In Vue, we have watchers to do the same thing.
<template>
<p>Count: {{ count }}</p>
</template>
<script setup>
import { ref, watch } from "vue";
const count = ref(0);
watch(count, (newValue, oldValue) => {
console.log(`Count changed from ${oldValue} to ${newValue}`);
});
</script>
The callback will run every time count is updated.
There is also watchEffect but it’s
[not cool](/vue-3-watcheffect-is-impressive-but-watch-is-still-the-best-choice-8903b62fdc19)!
13. Lifecycle Hooks
The useEffect hook combined with an empty dependency array can be used to run code when the component initiates or before it is destroyed.
import { useEffect } from "react";
export function MyComponent() {
useEffect(() => {
console.log("Component mounted");
return () => {
console.log("Component unmounted");
};
}, []);
return <div>My React Component</div>;
}
Vue offers dedicated hooks like onMounted and onUnmounted for the stages of a [component’s lifecycle](https://vuejs.org/guide/essentials/lifecycle).
<template>
<p>Hello world!</p>
</template>
<script setup>
import { onMounted, onUnmounted } from "vue";
onMounted(() => {
console.log("Component mounted");
});
onUnmounted(() => {
console.log("Component unmounted");
});
</script>
14. Data Fetching
Both frameworks often use third-party libraries like Axios, React Query, or [**Vue Query**](https://tanstack.com/query/v5/docs/framework/vue/overview) for data fetching. However, the main logic remains similar to using the built-in fetch method.
Here’s a typical use case:
In React data fetching is handled inside the useEffect hook.
import { useState, useEffect } from "react";
export function MyComponent() {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
async function fetchData() {
const response = await fetch("https://api.example.com/data");
const result = await response.json();
setData(result);
setLoading(false);
}
fetchData();
}, []);
if (loading) return <p>Loading...</p>;
return <p>Data: {data.message}</p>;
}
In Vue, data fetching can be handled directly when the component is initiated.
<template>
<div>
<p v-if="loading">Loading...</p>
<p v-else>Data: {{ data.message }}</p>
</div>
</template>
<script setup>
import { ref } from "vue";
const data = ref(null);
const loading = ref(true);
async function fetchData() {
const response = await fetch("https://api.example.com/data");
data.value = await response.json();
loading.value = false;
}
fetchData();
</script>
15. CSS Styles
There are many ways to style a React component. From inline styles to CSS modules and libraries like Emotion and Styled Components, things can get complicated and probably deserve a separate article by themselves. We will only explore the most popular use cases, but feel free to read more about [styling options in React](https://medium.com/@ignatovich.dm/react-and-css-in-js-styling-your-components-with-emotion-and-styled-components-ba1cc1068135).
Using inline styles:
export function MyComponent() {
const style = {
color: "blue",
fontSize: "20px",
};
return <p style={style}>Styled Text</p>;
}
Using external CSS Files:
import "./styles.css";
export function MyComponent() {
return <p className="styled-text">Styled Text</p>;
}
Notice that React uses
classNameinstead ofclassto avoid conflicts with JavaScript reserved keywords. This is one of the quirks of JSX.
Using CSS Modules**:**
import styles from "./MyComponent.module.css";
export function MyComponent() {
return <p className={styles.styledText}>Styled Text</p>;
}
CSS Modules will automatically scope styles to the current component and they are currently a very popular choice.
In Vue, styles are one of the three sections inside a Single File Component.
<template>
<p class="styled-text">Styled Text</p>
</template>
<style scoped>
.styled-text {
color: blue;
font-size: 20px;
}
</style>
The scoped attribute ensures the CSS only affects the current component.
Preprocessors like Scss, Less, Stylus or postcss are also supported by adding a lang attribute to the style tag. Scss is the popular choice.
<style lang="scss" scoped>
.styled-text {
color: blue;
font-size: 20px;
&:hover {
color: red;
}
}
</style>
The
langattribute can also be used to the other sections to convert your scripts to typescript or your template to pug for example.
16. Animations
React doesn’t do animations, but animations can be done in React using external libraries like Framer Motion, [**React Spring**](https://www.react-spring.dev/), or even CSS transitions.
While using third-party libraries is supported in Vue, it offers a powerful built-in mechanism that can handle both simple and complex animations.
Wrapping an element in a transition or transition-group component automatically applies classes during animation, which can be targeted using CSS for custom effects.
A simple example.
<template>
<div>
<button @click="isVisible = !isVisible">Toggle</button>
<transition name="fade">
<p v-if="isVisible">Animated Text</p>
</transition>
</div>
</template>
<script setup>
import { ref } from "vue";
const isVisible = ref(false);
</script>
<style>
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.5s;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
</style>
[Vue 3 Real Life Transitions and Micro-Interactions](/vue-js-real-life-transitions-and-micro-interactions-e86bd51301b8)
17. Custom Hooks and Composables
This is where all of the fun* happens.
* complex business logic implementation that will certainly cause headaches
In React, custom hooks are just JavaScript functions that can hold the state and functions. They can use other hooks like useState or useEffect.
import { useState } from "react";
function useCounter(initialValue = 0) {
const [count, setCount] = useState(initialValue);
const increment = () => setCount(count + 1);
const decrement = () => setCount(count - 1);
return { count, increment, decrement };
}
export function MyComponent() {
const { count, increment, decrement } = useCounter();
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>Increment</button>
<button onClick={decrement}>Decrement</button>
</div>
);
}
In Vue, composables do the same thing. They hold state and reusable logic.
// useCounter.js
import { ref } from "vue";
export function useCounter(initialValue = 0) {
const count = ref(initialValue);
const increment = () => count.value++;
const decrement = () => count.value\--;
return {
count,
increment,
decrement
};
}
<template>
<p>Count: {{ count }}</p>
<button @click="increment">Increment</button>
<button @click="decrement">Decrement</button>
</template>
<script setup>
import { useCounter } from "./useCounter";
const { count, increment, decrement } = useCounter();
</script>
Composables as State Management in Vue3
18. Global State Management
Another complex topic that could be a separate article. We will focus on a basic use case but you can read more about managing state in React or [Vue](https://vuejs.org/guide/scaling-up/state-management) if you want more.
In React, global state management is often handled using libraries like Redux, Zustand, or the built-in Context API. Here’s an example using the later.
import { createContext, useContext, useState } from "react";
const CounterContext = createContext();
export function CounterProvider({ children }) {
const [count, setCount] = useState(0);
return (
<CounterContext.Provider value={{ count, setCount }}>
{children}
</CounterContext.Provider>
);
}
export function useCounter() {
return useContext(CounterContext);
}
// Example usage:
export function MyComponent() {
const { count, setCount } = useCounter();
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
In Vue, the newer and officially recommended library to handle state management is [Pinia](https://pinia.vuejs.org/).
import { defineStore } from "pinia";
export const useCounterStore = defineStore("counter", () => {
const count = ref(0);
const increment = () => {
count.value++;
};
return { count, increment };
});
<template>
<div>
<p>Count: {{ count }}</p>
<button @click="increment">Increment</button>
</div>
</template>
<script setup>
import { useCounterStore } from "./store/counter";
const { count, increment } = useCounterStore();
</script>
Notice how it resembles a composable? That’s because it is! The main benefit of using Pinia is easier inspection and debugging using Vue DevTools.
19. Tools
Tooling would be a really long section a couple of years ago. But thankfully nowadays, Vite dominates both ecosystems and is widely considered the best option. It provides fast builds, hot module replacement, code splitting, and plugins, and it’s much easier to configure.
npm create vite@latest my\-react\-app \-- --template react
npm create vite@latest my\-vue\-app \-- --template vue
20. Routing
Both frameworks rely on external libraries for routing, but their approaches differ in terms of syntax and structure.
React uses [**React Router**](https://reactrouter.com/). The configuration resembles a component declaration and might feel a bit unnatural at first.
import { BrowserRouter as Router, Route, Routes, Link } from "react-router-dom";
export function App() {
return (
<Router>
<nav>
<Link to="/">Home</Link>
<Link to="/about">About</Link>
</nav>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
</Routes>
</Router>
);
}
function Home() {
return <h1>Home Page</h1>;
}
function About() {
return <h1>About Page</h1>;
}
Vue uses [**Vue Router**](https://router.vuejs.org/). The configuration is more declarative, defining route mappings in a configuration object.
<!-- router.js -->
import { createRouter, createWebHistory } from "vue-router";
import Home from "./components/Home.vue";
import About from "./components/About.vue";
const routes = [
{ path: "/", component: Home },
{ path: "/about", component: About },
];
export const router = createRouter({
history: createWebHistory(),
routes,
});
<template>
<nav>
<router-link to="/">Home</router-link>
<router-link to="/about">About</router-link>
</nav>
<router-view />
</template>
<router-link> and <router-view> are globally available and don’t need to be explicitly imported.
Conclusion
Now it’s time for the million-dollar question. Which one is better?
Well…
It depends.
Learning the API of a new framework is only a matter of a few weeks. The real challenge is implementing complex business logic inside custom hooks or composables, managing state and …centering divs. And none of them are part of any framework.
For me, the choice is simple and it depends on whether you enjoy writing JSX or not. React relies heavily on it while Vue follows a more declarative, template-driven syntax.
Beyond that, the differences are insignificant when you look at the bigger picture. Both frameworks are powerful and can handle just about any large-scale application. At the end of the day, it’s less about the tools and more about how you use them. So use them well!



![[Vue 3] Why both Ref and Reactive are needed](/_astro/img-1.Dm6ZyWyJ_5ALc3.webp)