Back to Blog

Vue.js for React Developers: A Hands-On Guide

You're a React developer. You've mastered hooks, understood the virtual DOM, and can debug re-renders in your sleep. But you keep hearing about Vue: how it's easier to learn, has automatic reactivity, and requires less boilerplate. Is the grass really greener?

This guide is for you. We'll explore Vue 3's Composition API, reactivity system, and template syntax through interactive comparisons with React. I'm not here to sell you on Vue. I'm here to show you how it works and where it differs from React, so you can decide for yourself.

Why Learn Vue as a React Developer?

Understanding multiple frameworks makes you a better developer. You'll recognize patterns, make better architectural decisions, and understand the trade-offs behind React's design choices. Plus, you might discover you prefer Vue for certain projects.

Vue and React solve the same problems (reactive UIs, component composition, state management) but with different philosophies. React gives you JavaScript for everything (JSX). Vue separates templates from logic. React requires manual dependency tracking (useMemo, useEffect). Vue uses Proxies for automatic tracking.


Part 1: Reactivity - Proxy Magic vs Manual Dependencies

The Problem

In vanilla JavaScript, changing a variable doesn't update the DOM. Both React and Vue solve this with reactive state, but they detect changes differently.

How React Handles Reactivity

React uses explicit state setters. When you call setState, React knows something changed and schedules a re-render:

const [count, setCount] = useState(0);
 
// React doesn't "watch" count
// You must call setCount to trigger updates
setCount(count + 1);

For derived state, React requires manual dependency arrays:

const fullName = useMemo(() => {
  return `${firstName} ${lastName}`;
}, [firstName, lastName]); // You list dependencies manually

How Vue Handles Reactivity

Vue wraps your state in a JavaScript Proxy that intercepts all reads and writes:

const count = ref(0);
 
// Vue's Proxy detects this write automatically
count.value++;

For derived state, Vue automatically tracks what you access:

const fullName = computed(() => {
  return `${firstName.value} ${lastName.value}`;
}); // No dependency array needed!
Reactivity Comparison: React vs Vue See how React's manual dependency tracking compares to Vue's automatic Proxy-based system
Computed Value:
John Doe
Computed 1 time(s)
This tests whether changing unrelated state causes fullName to recompute
// Update log:
[React] Recomputed fullName (1 times)
Try It Out

Toggle between React and Vue modes above. Change the first or last name, then toggle the theme. Notice how React recomputes fullName even when firstName/lastName didn't change, while Vue only recomputes when dependencies actually change.

Key differences:

  • React requires manual dependency arrays in useMemo. Any state change can trigger re-renders. You must remember to list all dependencies.
  • Vue automatically tracks which properties are accessed. Only dependent computations re-run when data changes. No manual dependency management needed.
Key Insight

React's approach is explicit: you tell it what changed and what depends on what. Vue's approach is automatic: it tracks dependencies at runtime using Proxies. Both work, but Vue reduces boilerplate and eliminates dependency array bugs.

Understanding Proxy-Based Reactive Systems

What exactly is a Proxy, and how does Vue use it for reactivity? A JavaScript Proxy is an object that wraps another object and intercepts operations performed on it. Think of it as a middleman that can watch and control access to your data.

Proxy-Based Reactive System Understanding how Vue uses JavaScript Proxies to track state changes
What is a JavaScript Proxy?
A Proxy is a JavaScript object that wraps another object and intercepts operations on it. Think of it like a security guard that logs every time someone enters (reads) or exits (writes) a building.
const state = new Proxy(originalObject, { get(target, prop) { // Intercept reads: state.count console.log('Reading', prop); return target[prop]; }, set(target, prop, value) { // Intercept writes: state.count = 5 console.log('Writing', prop, value); target[prop] = value; return true; } });
Vue wraps your state in a Proxy. When you read state.count, Vue logs which component accessed it. When you write state.count = 5, Vue knows exactly which components to re-render.
// Proxy trap logs:
Click "Run Code" to see how Proxies work...
Try It Out

Read the explanation above to understand what Proxies are, then try running the code examples. See how the Proxy's get and set traps intercept property access? Vue uses this mechanism to track which components read which properties, then re-renders only affected components when those properties change.

How it works:

  • Proxies intercept property access (get) and modification (set)
  • Vue wraps your state in a Proxy to track all reads and writes
  • When you read state.count, Vue knows you depend on it
  • When you write state.count = 5, Vue re-renders components that read it

Side-by-Side: useState vs ref()

Both create reactive state, but with different APIs:

useState vs ref(): State Management Compare how React and Vue handle reactive state
React (useState)
0
Vue (ref)
0
React
import { useState } from 'react';

function Counter() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>
        Increment
      </button>
    </div>
  );
}
Vue
<script setup>
import { ref } from 'vue';

const count = ref(0);
</script>

<template>
  <div>
    <p>Count: {{ count }}</p>
    <button @click="count++">
      Increment
    </button>
  </div>
</template>

Key differences:

  • React: Returns [value, setter]. Use value directly. Call setter to update. Setter can take updater function.
  • Vue: Returns ref object. Access via .value in script. Directly mutate .value. Auto-unwrapped in templates.
Warning

Vue's ref() requires .value in JavaScript but NOT in templates. This trips up newcomers:

// In script setup:
const count = ref(0);
count.value++; // .value required
 
// In template:
<div>{{ count }}</div> <!-- NO .value needed -->

Why? Vue auto-unwraps refs in templates for convenience.

Side-by-Side: useMemo vs computed()

Derived state works similarly, but Vue eliminates the dependency array:

useMemo vs computed(): Derived State See the difference between manual and automatic dependency tracking
React useMemo
Jane Smith
Computed 1 time(s)
Vue computed
Jane Smith
Would compute 0 time(s)
React
import { useState, useMemo } from 'react';

function UserProfile() {
  const [firstName, setFirstName] = useState('Jane');
  const [lastName, setLastName] = useState('Smith');
  const [age, setAge] = useState(25);

  // Manual dependency array required
  const fullName = useMemo(() => {
    return `${firstName} ${lastName}`;
  }, [firstName, lastName]); // Must list deps

  return <h1>{fullName}</h1>;
}
Vue
<script setup>
import { ref, computed } from 'vue';

const firstName = ref('Jane');
const lastName = ref('Smith');
const age = ref(25);

// Automatically tracks dependencies
const fullName = computed(() => {
  return `${firstName.value} ${lastName.value}`;
}); // No dependency array needed!
</script>

<template>
  <h1>{{ fullName }}</h1>
</template>

Key differences:

  • React: You must manually specify [firstName, lastName]. Forget one? Stale data or infinite loops.
  • Vue: Automatically tracks that you accessed firstName.value and lastName.value. No manual work.
  • Result: Vue's computed is less error-prone and more maintainable.
Key Insight

Vue's computed requires zero maintenance. With React's useMemo, you can forget a dependency (stale values), include too many dependencies (unnecessary recomputes), or trigger infinite loops with wrong dependencies. Vue's computed avoids these issues by tracking dependencies automatically.


Part 2: Composition API vs React Hooks

Both Vue 3's Composition API and React Hooks move away from class components toward function-based composition. They're surprisingly similar!

The Evolution Story

React (2013): Classes with lifecycle methods → Hooks (2019) for functional components

Vue (2014): Options API (data, methods, computed) → Composition API (2020) for better composition

Both frameworks recognized that grouping code by feature (not lifecycle) is more maintainable.

script setup: Vue's Concise Syntax

React function components look like this:

import { useState } from 'react';
 
function Counter() {
  const [count, setCount] = useState(0);
 
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
}
 
export default Counter;

Vue's <script setup> is more concise:

<script setup>
import { ref } from 'vue';
 
const count = ref(0);
</script>
 
<template>
  <div>
    <p>Count: {{ count }}</p>
    <button @click="count++">Increment</button>
  </div>
</template>
Component Structure: React Hooks vs <script setup> Compare how React function components and Vue Composition API organize code
React (Function Component)
import { useState } from 'react';

function Counter() {
  const [count, setCount] = useState(0);

  const increment = () => {
    setCount(count + 1);
  };

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={increment}>
        Increment
      </button>
    </div>
  );
}

export default Counter;
Working Demo:
0

React patterns:

  • Function components return JSX
  • Hooks must follow Rules of Hooks
  • State with useState
  • Effects with useEffect
  • Everything in JavaScript

Vue patterns:

  • <script setup> auto-exposes to template
  • No special hook rules
  • State with ref/reactive
  • Effects with watch/watchEffect
  • Separate script and template
Key Insight

Key differences:

  • React: Everything is JavaScript (JSX). State, logic, and template mixed.
  • Vue: Separate <script> and <template>. Clean separation of concerns.
  • React: Must return JSX from component function.
  • Vue: Everything in <script setup> auto-exposes to template.

Lifecycle Hooks: More Granular in Vue

React combines mount, update, and unmount into useEffect:

useEffect(() => {
  console.log('Mounted or updated');
 
  return () => console.log('Cleanup');
}, [dependency]); // Runs when dependency changes

Vue separates each phase:

onBeforeMount(() => console.log('About to mount'));
onMounted(() => console.log('Mounted'));
onBeforeUpdate(() => console.log('About to update'));
onUpdated(() => console.log('Updated'));
onBeforeUnmount(() => console.log('About to unmount'));
onUnmounted(() => console.log('Unmounted'));
Lifecycle Hooks Comparison See when React's useEffect and Vue's lifecycle hooks fire
Current Phase:
unmounted
// Lifecycle events:
Click "Mount Component" to see lifecycle events...
React (useEffect)Vue (Lifecycle Hooks)When
useEffect(() => {...}, [])onMounted()After first render
useEffect(() => {...})onUpdated()After any update
useEffect cleanuponUnmounted()Before component removed
No direct equivalentonBeforeMount()Before first render
No direct equivalentonBeforeUpdate()Before re-render
Try It Out

Click through the lifecycle phases above. Vue gives you hooks before and after each phase, while React combines everything into useEffect.

Key difference:

  • React: useEffect combines mount, update, and unmount. Deps array controls when it runs.
  • Vue: Separate hooks for each phase (onMounted, onUpdated, onUnmounted) for clarity.
  • Vue Advantage: More granular control - you can run code specifically before/after mount or update.

Side Effects: useEffect vs watch()

React's useEffect is general-purpose for all side effects:

useEffect(() => {
  console.log('searchQuery changed:', searchQuery);
 
  const timeout = setTimeout(() => {
    saveToLocalStorage(searchQuery);
  }, 300);
 
  return () => clearTimeout(timeout); // Cleanup
}, [searchQuery]); // Manual dependency

Vue's watch is specifically for watching reactive data:

watch(searchQuery, (newValue, oldValue) => {
  console.log('searchQuery changed:', newValue);
 
  const timeout = setTimeout(() => {
    saveToLocalStorage(newValue);
  }, 300);
 
  // Cleanup via onInvalidate
  return () => clearTimeout(timeout);
}); // Automatic tracking
Side Effects: useEffect vs watch() Compare how React and Vue handle reactive side effects
// Update log:
No updates yet...
React
import { useState, useEffect } from 'react';

function Search() {
  const [searchQuery, setSearchQuery] = useState('');

  useEffect(() => {
    // Run whenever searchQuery changes
    console.log('Query changed:', searchQuery);

    const timeout = setTimeout(() => {
      localStorage.setItem('query', searchQuery);
    }, 300);

    // Cleanup function
    return () => clearTimeout(timeout);
  }, [searchQuery]); // Manual dependency

  return <input value={searchQuery} />;
}
Vue
<script setup>
import { ref, watch } from 'vue';

const searchQuery = ref('');

watch(searchQuery, (newQuery, oldQuery) => {
  // Automatically tracks searchQuery
  console.log('Query changed:', newQuery);

  const timeout = setTimeout(() => {
    localStorage.setItem('query', newQuery);
  }, 300);

  // Cleanup (onInvalidate)
  return () => clearTimeout(timeout);
});
</script>

<template>
  <input v-model="searchQuery" />
</template>

Comparison:

React useEffect:

  • General-purpose effect hook
  • Requires dependency array
  • Runs after every render (if deps change)
  • Return cleanup function

Vue watch:

  • Specifically for watching data
  • Automatically tracks source
  • Gets old and new values
  • Built-in cleanup via onInvalidate
Key Insight

Vue's watch is more explicit about what it's watching. React's useEffect is more flexible but requires careful dependency management. Both can do the same things, but Vue's API is more specific to the use case.


Part 3: Template Syntax vs JSX

This is where React and Vue diverge most philosophically.

The Problem

Should you use JavaScript for everything (React's JSX), or separate templates from logic (Vue)? There's no right answer. It's about what feels more natural to you and your team.

Conditional Rendering: && and ?: vs v-if

React uses JavaScript expressions:

{show && <div>Message is visible!</div>}
 
{mode === 'A' ? <ComponentA /> : <ComponentB />}

Vue uses template directives:

<div v-if="show">Message is visible!</div>
 
<ComponentA v-if="mode === 'A'" />
<ComponentB v-else />
Conditional Rendering: JSX vs v-if Compare React's JavaScript expressions with Vue's template directives
Mode:
Output:
Component A
React
function Conditional() {
  const [show, setShow] = useState(false);
  const [mode, setMode] = useState('A');

  return (
    <div>
      {/* Conditional rendering with && */}
      {show && <div>Message is visible!</div>}

      {/* Ternary for if/else */}
      {mode === 'A' ? (
        <ComponentA />
      ) : (
        <ComponentB />
      )}
    </div>
  );
}
Vue
<script setup>
import { ref } from 'vue';

const show = ref(false);
const mode = ref('A');
</script>

<template>
  <div>
    <!-- v-if directive -->
    <div v-if="show">Message is visible!</div>

    <!-- v-if with v-else -->
    <ComponentA v-if="mode === 'A'" />
    <ComponentB v-else />
  </div>
</template>

Vue's additional directives:

  • v-if / v-else-if / v-else - Full conditional rendering
  • v-show - Toggle CSS display (element stays in DOM)
  • React: Only {expression && <JSX />} or ternary operator
Key Insight

Vue's additional directives:

  • v-if / v-else-if / v-else - Full conditional chains
  • v-show - Toggle CSS display (element stays in DOM)

React only has {condition && <JSX />} and ternaries. It's more flexible but more verbose for complex conditions.

List Rendering: .map() vs v-for

React uses JavaScript's .map():

<ul>
  {items.map(item => (
    <li key={item.id}>{item.name}</li>
  ))}
</ul>

Vue uses the v-for directive:

<ul>
  <li v-for="item in items" :key="item.id">
    {{ item.name }}
  </li>
</ul>
List Rendering: map() vs v-for Compare how React and Vue render lists of data
Items (3):
Apple
Banana
Cherry
React
function List({ items }) {
  return (
    <ul>
      {items.map(item => (
        <li key={item.id}>
          {item.name}
        </li>
      ))}
    </ul>
  );
}
Vue
<script setup>
import { ref } from 'vue';

const items = ref([...]);
</script>

<template>
  <ul>
    <li v-for="item in items" :key="item.id">
      {{ item.name }}
    </li>
  </ul>
</template>
Try It Out

Add, remove, and shuffle items above. The key prop/directive is critical in both frameworks for efficient updates. Without it, both frameworks struggle with reordering.

Warning

Keys are critical in both frameworks:

  • React: key prop helps identify which items changed
  • Vue: :key directive does the same thing
  • Without keys, both frameworks struggle with reordering
  • Try shuffling above - keys make it smooth!

Two-Way Binding: Controlled Inputs vs v-model

Vue is particularly concise here.

React requires explicit value + onChange:

<input
  type="text"
  value={text}
  onChange={(e) => setText(e.target.value)}
/>

Vue's v-model is syntactic sugar:

<input type="text" v-model="text" />
Two-Way Binding: Controlled Inputs vs v-model See how React's explicit binding compares to Vue's v-model syntactic sugar
Value: ""
Value: false
Value: "apple"
React
import { useState } from 'react';

function Form() {
  const [text, setText] = useState('');
  const [checked, setChecked] = useState(false);
  const [selected, setSelected] = useState('apple');

  return (
    <div>
      <input
        type="text"
        value={text}
        onChange={(e) => setText(e.target.value)}
      />

      <input
        type="checkbox"
        checked={checked}
        onChange={(e) => setChecked(e.target.checked)}
      />

      <select
        value={selected}
        onChange={(e) => setSelected(e.target.value)}
      >
        <option value="apple">Apple</option>
        <option value="banana">Banana</option>
      </select>
    </div>
  );
}
Vue
<script setup>
import { ref } from 'vue';

const text = ref('');
const checked = ref(false);
const selected = ref('apple');
</script>

<template>
  <div>
    <input type="text" v-model="text" />

    <input type="checkbox" v-model="checked" />

    <select v-model="selected">
      <option value="apple">Apple</option>
      <option value="banana">Banana</option>
    </select>
  </div>
</template>

React (Controlled):

  • Explicit value prop
  • Explicit onChange handler
  • More verbose but clear
  • Full control over updates

Vue (v-model):

  • Syntactic sugar
  • Auto-binds value + @input
  • Concise and clean
  • Less boilerplate
Key Insight

What v-model actually does:

<input v-model="text" />
 
<!-- Expands to: -->
<input
  :value="text"
  @input="text = $event.target.value"
/>

It's just sugar, but it eliminates a lot of boilerplate for forms.

DOM Refs: useRef vs Template Refs

Both frameworks let you access DOM elements directly:

DOM Refs: useRef vs template refs Compare how React and Vue access DOM elements
React
import { useRef } from 'react';

function InputFocus() {
  const inputRef = useRef(null);

  const focusInput = () => {
    inputRef.current?.focus();
  };

  return (
    <div>
      <input ref={inputRef} />
      <button onClick={focusInput}>
        Focus Input
      </button>
    </div>
  );
}
Vue
<script setup>
import { ref } from 'vue';

const inputRef = ref(null);

const focusInput = () => {
  inputRef.value?.focus();
};
</script>

<template>
  <div>
    <input ref="inputRef" />
    <button @click="focusInput">
      Focus Input
    </button>
  </div>
</template>

React useRef:

  • Create with useRef(null)
  • Attach with ref={inputRef}
  • Access via inputRef.current
  • Works for DOM and values

Vue template ref:

  • Create with ref(null)
  • Attach with ref="inputRef"
  • Access via inputRef.value
  • Template refs are reactive
Warning

Confusing: Vue uses ref() for BOTH state and DOM refs!

const count = ref(0);      // Reactive state
const inputRef = ref(null); // DOM ref

Both use .value. React separates these: useState for state, useRef for DOM.


Part 4: Component Lifecycle & Deeper Concepts

Let's dig into how components mount, update, and unmount in both frameworks.

Mounting Phase: Step by Step

Mounting Phase Timeline Step through the component mounting process in both frameworks
1. Component Created
Constructor/function called
2. Render Phase
JSX elements created
3. Commit to DOM
Real DOM updated
4. useEffect Runs
Side effects execute
Try It Out

Use the step controls to walk through mounting in React vs Vue. Notice Vue's more granular hooks (onBeforeMount, onMounted) versus React's single useEffect.

Vue has more granular hooks (onBeforeMount, onMounted) while React combines everything into useEffect with an empty dependency array.

Update Phase: How Changes Propagate

When state changes, both frameworks:

  1. Detect the change
  2. Re-render the component
  3. Diff the virtual DOM
  4. Update only changed DOM nodes
Update Phase: How Changes Propagate See what happens when state changes
Current Count:
0
// Update log:
No updates yet...
React Update Flow:
1. setState called
2. Component re-renders
3. Virtual DOM diff
4. DOM updated
5. useEffect runs (if deps changed)
Vue Update Flow:
1. Reactive data changed
2. onBeforeUpdate fires
3. Virtual DOM diff
4. DOM patched
5. onUpdated fires
Key Insight

React re-renders the entire component tree on state change. Uses useMemo/useCallback to prevent unnecessary work.

Vue only re-renders components that use changed reactive data (thanks to Proxy tracking). Less manual optimization needed.

Parent-Child Communication: Props & Events

Both use "props down, events up" pattern:

Props & Events: Parent-Child Communication Compare how React and Vue handle component communication
Child Component:
Count: 0
// Update log:
No updates yet...
React
// Parent Component
function Parent() {
  const [count, setCount] = useState(0);

  const handleIncrement = (value) => {
    setCount(count + value);
  };

  return (
    <Child count={count} onIncrement={handleIncrement} />
  );
}

// Child Component
function Child({ count, onIncrement }) {
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => onIncrement(1)}>+1</button>
      <button onClick={() => onIncrement(5)}>+5</button>
    </div>
  );
}
Vue
<!-- Parent Component -->
<script setup>
import { ref } from 'vue';

const count = ref(0);

const handleIncrement = (value) => {
  count.value += value;
};
</script>

<template>
  <Child :count="count" @increment="handleIncrement" />
</template>

<!-- Child Component -->
<script setup>
defineProps(['count']);
const emit = defineEmits(['increment']);
</script>

<template>
  <div>
    <p>Count: {{ count }}</p>
    <button @click="emit('increment', 1)">+1</button>
    <button @click="emit('increment', 5)">+5</button>
  </div>
</template>

React pattern:

  • Props down via attributes
  • Events up via callback props
  • onEventName convention
  • Direct function calls

Vue pattern:

  • Props down via :propName
  • Events up via @eventName
  • defineEmits() in child
  • emit() to trigger events

React passes callbacks as props (onEventName convention). Vue uses defineEmits() and emit() for explicit event declarations.

Cleanup and Unmounting

Both frameworks let you clean up side effects (timers, subscriptions, etc.):

Unmounting & Cleanup How React and Vue handle component removal
Component Status:
MOUNTED
// Update log:
No updates yet...
React Cleanup:
useEffect(() => {
  const timer = setInterval(...);

  // Cleanup function
  return () => {
    clearInterval(timer);
  };
}, []);
Vue Cleanup:
onMounted(() => {
  const timer = setInterval(...);
});

onUnmounted(() => {
  clearInterval(timer);
});

React uses useEffect cleanup functions. Vue has a dedicated onUnmounted hook. Same functionality, different API.


When to Choose What?

Framework Decision Matrix Select factors that apply to your project
React Score
0
Vue Score
0
Remember: Both are excellent frameworks. The best choice depends on your team, project requirements, and personal preference. You can't go wrong with either!
Try It Out

Select the factors that apply to your project. The matrix will score React vs Vue based on your needs.

Key Takeaways

Key Insight
  1. Reactivity: Vue's Proxy-based automatic tracking vs React's manual dependency arrays. Vue has less boilerplate and fewer bugs. React is more explicit with full JavaScript control.

  2. Component Structure: Composition API ≈ Hooks. Vue's <script setup> is more concise and separates template from logic. React's JSX mixes everything in JavaScript.

  3. Templates: v-if, v-for, v-model vs JSX expressions. Vue is more HTML-like and familiar to web developers. React gives you full JavaScript power and more flexibility.

  4. Lifecycle: Vue's granular hooks vs React's unified useEffect. Vue has separate hooks for each phase (onMounted, onUpdated, etc.). React's useEffect handles everything (simpler but less specific).

  5. Ecosystem & Jobs: React is larger, but Vue is growing fast. React has a bigger ecosystem, more jobs, and is backed by Meta. Vue has an easier learning curve, great docs, and is strong in Asia/Europe.

Your Next Steps

If you're coming from React and want to try Vue:

  1. Official Docs: vuejs.org - Excellent tutorial
  2. Try It: Build the same component in React and Vue
  3. Play with Proxies: Understanding reactivity is key
  4. Explore Pinia: Vue's state management (similar to Redux/Zustand)

If you decide Vue isn't for you, that's fine! You've learned about alternative approaches to reactivity, templates, and lifecycle management. This knowledge makes you a better React developer.


Final Thoughts

Vue and React are both excellent frameworks. React's philosophy is "it's just JavaScript": full control, explicit everything. Vue's philosophy is "make it easy": automatic reactivity, less boilerplate, clearer separation.

Neither is objectively better. It depends on your team's experience, project requirements, personal preference, and ecosystem needs.

The best developers understand why frameworks make different choices. Now you do.