As a React developer, you’re already familiar with building dynamic, component-based user interfaces. But what if you want to expand your toolkit? Vue.js, a progressive JavaScript framework, shares many core principles with React—like component-based architecture and a virtual DOM—yet differs in syntax, state management, and tooling. This guide will help you leverage your React knowledge to master Vue.js, highlighting key similarities, differences, and actionable tips for a smooth transition.
Table of Contents
- Introduction to Vue.js for React Developers
- Core Concepts: What’s Similar, What’s Different?
- Project Setup: From Create React App to Vue CLI/Vite
- Component Structure: JSX vs. Vue Templates & Single-File Components (SFCs)
- State Management: useState/useReducer vs. ref/reactive/Pinia
- Lifecycle & Side Effects: useEffect vs. Vue Lifecycle Hooks & Watchers
- Routing: React Router vs. Vue Router
- Styling: Scoped CSS, Modules, and Beyond
- Tooling & Ecosystem: DevTools, Testing, and TypeScript
- Migration Example: Converting a React Component to Vue
- Common Pitfalls to Avoid
- Conclusion
- References
Core Concepts: What’s Similar, What’s Different?
Let’s start with the fundamentals. Both React and Vue prioritize component reusability, reactivity, and declarative rendering, but their implementations differ in key ways.
1. Virtual DOM & Reactivity
- React: Uses a virtual DOM to diff changes and update the real DOM efficiently. React’s reactivity is “push-based”: when state changes, you explicitly trigger re-renders with
setStateor hooks likeuseState. - Vue: Also uses a virtual DOM (optimized in Vue 3 with a compiler that generates more efficient code). Vue’s reactivity is proxy-based (in Vue 3) and “pull-based”: it automatically tracks dependencies and updates only the components that need to re-render when state changes. No need to call
setState—you mutate state directly, and Vue reacts.
2. Declarative Rendering
Both frameworks let you describe UI as a function of state, but the syntax differs:
- React: Uses JSX, a syntax extension that embeds HTML-like code in JavaScript. Example:
function Greeting({ name }) { return <h1>Hello, {name}!</h1>; } - Vue: Defaults to HTML templates (separated from logic) with mustache syntax
{{ }}for expressions. Example:
Note: Vue also supports JSX if you prefer—more on that later!<template> <h1>Hello, {{ name }}!</h1> </template> <script setup> const props = defineProps({ name: String }); </script>
Project Setup: From Create React App to Vue CLI/Vite
Setting up a new project is straightforward in both ecosystems, but Vue’s tooling has evolved to prioritize speed with Vite (a build tool that replaces Webpack for faster development).
React: Create React App (CRA)
npx create-react-app my-react-app
cd my-react-app
npm start
Vue: Vite (Recommended for Vue 3)
Vite is the official build tool for Vue 3, offering instant hot module replacement (HMR) and faster builds.
npm create vite@latest my-vue-app -- --template vue
cd my-vue-app
npm install
npm run dev
Project Structure Comparison
| React (CRA) | Vue (Vite) | Purpose |
|---|---|---|
public/ | public/ | Static assets (e.g., index.html). |
src/ | src/ | Source code (components, logic, etc.). |
src/App.js | src/App.vue | Root component (Vue uses SFCs by default). |
src/index.js | src/main.js | Entry point (mounts the app). |
Component Structure: JSX vs. Vue Templates & Single-File Components (SFCs)
React components are typically JavaScript/JSX files. Vue uses Single-File Components (SFCs) with a .vue extension, which encapsulate template (HTML), script (JavaScript), and style (CSS) in one file.
React Component (Functional with Hooks)
// src/components/Counter.jsx
import { useState } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
Vue Component (SFC with <script setup>)
Vue’s SFCs separate concerns into three sections: <template>, <script>, and <style>. The <script setup> syntax (recommended for Vue 3) simplifies component logic with automatic imports and reactivity.
<!-- src/components/Counter.vue -->
<template>
<div>
<p>Count: {{ count }}</p>
<button @click="increment">Increment</button>
</div>
</template>
<script setup>
import { ref } from 'vue';
// Reactive state (like useState in React)
const count = ref(0);
// Function to update state
const increment = () => {
count.value++; // Mutate directly (Vue reacts!)
};
</script>
<style scoped>
/* Scoped CSS: styles only apply to this component */
button {
padding: 0.5rem 1rem;
background: #42b983;
color: white;
border: none;
border-radius: 4px;
}
</style>
Key Differences in Components
| Feature | React | Vue (Composition API) |
|---|---|---|
| Template Syntax | JSX (HTML in JS) | HTML templates (separate from logic) |
| State Definition | useState(initialValue) | ref(initialValue) (primitives) or reactive({}) (objects) |
| State Updates | setState(newValue) | Mutate directly: count.value++ |
| Props | Destructure from function args | defineProps({ propName: Type }) |
| Event Handling | onClick={handler} | @click="handler" (shorthand for v-on:click) |
State Management: useState/useReducer vs. ref/reactive/Pinia
State management is critical in both frameworks. Let’s map React’s state tools to Vue’s equivalents.
Local State
React: useState and useReducer
React uses useState for simple state and useReducer for complex state logic:
// useState example
const [count, setCount] = useState(0);
// useReducer example (for complex state)
function todoReducer(state, action) {
switch (action.type) {
case 'ADD_TODO': return [...state, action.payload];
default: return state;
}
}
const [todos, dispatch] = useReducer(todoReducer, []);
Vue: ref and reactive
Vue 3’s Composition API uses ref and reactive for local state:
ref: For primitive values (strings, numbers, booleans) or to make objects reactive (wraps the value in a{ value }object).reactive: For objects/arrays (directly makes the object reactive without wrapping).
<script setup>
import { ref, reactive } from 'vue';
// ref for primitives (like useState)
const count = ref(0); // { value: 0 }
count.value++; // Update state
// reactive for objects (like useReducer for complex state)
const user = reactive({
name: 'Alice',
age: 30
});
user.age++; // Update nested state directly
</script>
Global State
React: Context API + useReducer or Redux
For global state, React often uses Context API with useReducer or third-party libraries like Redux.
// React Context example
const ThemeContext = createContext();
function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
{children}
</ThemeContext.Provider>
);
}
// Use in components with useContext
const { theme } = useContext(ThemeContext);
Vue: Pinia (Replacement for Vuex)
Vue’s official global state library is Pinia (simpler than the older Vuex). It’s lightweight, TypeScript-friendly, and aligns with Vue 3’s Composition API.
Step 1: Define a Pinia Store
// src/stores/theme.js
import { defineStore } from 'pinia';
export const useThemeStore = defineStore('theme', {
state: () => ({ theme: 'light' }),
actions: {
toggleTheme() {
this.theme = this.theme === 'light' ? 'dark' : 'light';
}
}
});
Step 2: Use the Store in Components
<template>
<div :class="theme">
<button @click="toggleTheme">Toggle Theme</button>
</div>
</template>
<script setup>
import { useThemeStore } from '@/stores/theme';
const themeStore = useThemeStore();
// Access state: themeStore.theme
// Call actions: themeStore.toggleTheme()
</script>
Key Takeaway
- Local State:
ref≈useState,reactive≈useReducer(for objects). - Global State: Pinia (Vue) ≈ Redux/Context (React), but Pinia is simpler with less boilerplate.
Lifecycle & Side Effects: useEffect vs. Vue Lifecycle Hooks & Watchers
React’s useEffect handles side effects (e.g., API calls, subscriptions). Vue uses lifecycle hooks and watch for similar tasks.
React: useEffect
useEffect(() => {
// Run on mount and whenever `count` changes
document.title = `Count: ${count}`;
// Cleanup function (runs on unmount or before re-run)
return () => {
// Cancel subscriptions, etc.
};
}, [count]); // Dependency array
Vue: Lifecycle Hooks and watch
Vue 3’s Composition API provides explicit lifecycle hooks (e.g., onMounted) and watch for reacting to state changes.
<script setup>
import { ref, onMounted, watch } from 'vue';
const count = ref(0);
// Run on mount (equivalent to useEffect with empty dependency array)
onMounted(() => {
console.log('Component mounted!');
});
// Watch for changes to `count` (equivalent to useEffect with [count])
watch(count, (newValue, oldValue) => {
document.title = `Count: ${newValue}`;
});
</script>
Lifecycle Hook Mapping
| React Lifecycle | Vue Lifecycle Hook |
|---|---|
useEffect(() => {}, []) (mount) | onMounted |
useEffect(() => { return () => {} }, []) (unmount) | onUnmounted |
useEffect(() => {}, [state]) (update) | watch(state, callback) |
Routing: React Router vs. Vue Router
Both frameworks use dedicated routing libraries. Let’s compare React Router v6 and Vue Router v4.
Setup
React Router
npm install react-router-dom
Vue Router
npm install vue-router@4
Basic Route Definition
React Router
// src/App.jsx
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import Home from './pages/Home';
import About from './pages/About';
function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
</Routes>
</BrowserRouter>
);
}
Vue Router
// src/router/index.js
import { createRouter, createWebHistory } from 'vue-router';
import Home from './pages/Home.vue';
import About from './pages/About.vue';
const routes = [
{ path: '/', component: Home },
{ path: '/about', component: About }
];
const router = createRouter({
history: createWebHistory(),
routes
});
export default router;
Then import and use the router in main.js:
// src/main.js
import { createApp } from 'vue';
import App from './App.vue';
import router from './router';
createApp(App).use(router).mount('#app');
Navigation and Dynamic Routes
| Feature | React Router | Vue Router |
|---|---|---|
| Linking | <Link to="/about">About</Link> | <RouterLink to="/about">About</RouterLink> |
| Dynamic Routes | <Route path="/user/:id" element={<User />} /> | { path: '/user/:id', component: User } |
| Access Params | useParams() | useRoute().params |
Styling: Scoped CSS, Modules, and Beyond
Styling in React often uses CSS Modules, styled-components, or Emotion. Vue has built-in support for scoped styles and CSS Modules.
Vue: Scoped CSS
Vue SFCs let you scope styles to the component using the scoped attribute, preventing style leakage:
<style scoped>
/* Only applies to elements in this component's template */
button {
color: red;
}
</style>
Vue: CSS Modules
For more control, use CSS Modules (similar to React’s CSS Modules):
<style module>
/* Class names are hashed to avoid conflicts */
.redButton {
color: red;
}
</style>
<template>
<button :class="$style.redButton">Click me</button>
</template>
Tooling & Ecosystem
| Tool | React | Vue |
|---|---|---|
| DevTools | React DevTools (browser extension) | Vue DevTools (browser extension) |
| Testing | Jest + React Testing Library | Jest + Vue Test Utils |
| TypeScript | First-class support | First-class support (Vue 3 + Composition API) |
| Build Tool | Webpack (CRA) or Vite | Vite (official recommendation) |
Migration Example: Converting a React Component to Vue
Let’s convert a simple React counter component to Vue to solidify what we’ve learned.
React Counter Component
// Counter.jsx
import { useState } from 'react';
export default function Counter({ initialCount = 0 }) {
const [count, setCount] = useState(initialCount);
const increment = () => setCount(prev => prev + 1);
const decrement = () => setCount(prev => prev - 1);
return (
<div className="counter">
<h2>Count: {count}</h2>
<button onClick={decrement}>-</button>
<button onClick={increment}>+</button>
</div>
);
}
Equivalent Vue Component
<!-- Counter.vue -->
<template>
<div class="counter">
<h2>Count: {{ count }}</h2>
<button @click="decrement">-</button>
<button @click="increment">+</button>
</div>
</template>
<script setup>
import { ref } from 'vue';
// Define props
const props = defineProps({ initialCount: { type: Number, default: 0 } });
// Local state (reactive)
const count = ref(props.initialCount);
// Methods
const increment = () => count.value++;
const decrement = () => count.value--;
</script>
<style scoped>
.counter {
display: flex;
gap: 1rem;
align-items: center;
}
button {
padding: 0.5rem 1rem;
}
</style>
Common Pitfalls to Avoid
- Mutating State: In React, you must use
setState; in Vue, you can mutateref/reactivestate directly. Don’t callsetStatein Vue! - Template Syntax: Vue templates use
{{ }}for expressions, not{ }like JSX. Also, attribute bindings use:(e.g.,:class="active"instead ofclassName={active}). - Event Names: Vue uses kebab-case for events (
@submit.preventinstead ofonSubmit={handleSubmit}withevent.preventDefault()). - Props Validation: Use
definePropswith type checks in Vue (e.g.,defineProps({ age: Number })) instead of PropTypes.
Conclusion
Vue.js and React share core principles but differ in syntax and tooling. As a React developer, you already understand component-based architecture, reactivity, and state management—skills that transfer directly to Vue. By focusing on Vue 3’s Composition API, you’ll find a familiar, hook-like paradigm that minimizes the learning curve.
Start small: convert a simple React component to Vue, experiment with Vite, and explore Pinia for state management. With practice, you’ll be building Vue apps with confidence in no time!