A Beginner's Guide to Vue for React Developers
15th January 2025 • 12 min read — by Aleksandar Trpkovski
Vue is a frontend framework that shares many concepts with React while taking its own unique approach. This guide will walk you through Vue's key concepts and compare them to their React equivalents. By the end, you'll have a solid foundation for working with Vue.
Setting Up a Component
React
In React, a functional component is created like this:
function ExampleComponent() {
return <h1>Hello, React!</h1>;
}
export default ExampleComponent;
Vue
In Vue, components are defined with a template and a script:
<script setup></script>
<template>
<h1>Hello, Vue!</h1>
</template>
Key Differences:
- Vue components are defined in files with the
.vue
extension. - Vue separates logic (
script
) and markup (template
). - Vue doesn't require JSX, instead, you write HTML-like syntax directly in the
template
section of your.vue
file.
Styling Components
Now that we've set up our components, let’s explore how to style them. Styling components in React and Vue can be done in multiple ways, including:
Inline Styles
Both React and Vue support inline styles, though their syntax differs slightly.
React
function ExampleComponent() {
const inlineStyle = { color: "blue", fontSize: "20px" };
return <h1 style={inlineStyle}>Hello, React!</h1>;
}
export default ExampleComponent;
Vue
<script setup>
const inlineStyle = { color: "blue", fontSize: "20px" };
</script>
<template>
<h1 :style="inlineStyle">Hello, Vue!</h1>
</template>
In Vue, :style
is a directive used for binding styles.
External Styles
External styles involve creating a separate CSS file and importing it.
React
You can create a ExampleComponent.css
file and import it into your component:
import "./ExampleComponent.css";
function ExampleComponent() {
return <h1 className="header">Hello, React!</h1>;
}
export default ExampleComponent;
ExampleComponent.css:
.header {
color: blue;
font-size: 20px;
}
Vue
In Vue, styles can be added directly to the .vue
file in a <style>
block:
<script setup></script>
<template>
<h1 class="header">Hello, Vue!</h1>
</template>
<style>
.header {
color: blue;
font-size: 20px;
}
</style>
Key Differences:
- Vue uses
class
attribute for CSS classes, while React usesclassName
(sinceclass
is a reserved word in JavaScript).
Scoped Styles (Vue-Specific)
Vue provides a scoped styling feature to limit styles to the component they belong to.
<script setup></script>
<template>
<h1 class="header">Hello, Vue!</h1>
</template>
<style scoped>
.header {
color: blue;
font-size: 20px;
}
</style>
What are scoped styles in Vue?
- Adding the
scoped
attribute to astyle
tag in Vue automatically assigns unique attributes to the component's HTML elements, ensuring styles only apply within that component. - This prevents style conflicts across components while maintaining all styling code in a single file.
React's Approach to Scoped Styles
While React doesn't include built-in scoped styles, you can achieve similar functionality natively with CSS Modules or by using the styled-components library to isolate styles to specific components.
React CSS Modules:
React supports CSS Modules, which are a way to scope CSS to a specific component. With CSS Modules, the class names are automatically locally scoped by appending unique names. Here’s an example:
Card.module.css
.header {
color: blue;
font-size: 20px;
}
Card.jsx
import styles from "./Card.module.css";
const Card = () => <div className={styles.header}>Hello, React!</div>;
export default Card;
Styled-Components (CSS-in-JS)
For a more dynamic and scoped styling experience, you can use libraries like Styled-Components. It allows you to define component-scoped styles directly within your JavaScript files:
import styled from "styled-components";
const Header = styled.h1`
color: blue;
font-size: 20px;
`;
const Card = () => <Header>Hello, React!</Header>;
export default Card;
State Management with Reactivity
React
React uses useState
for state management:
import { useState } from "react";
function ExampleComponent() {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
export default ExampleComponent;
For more complex state logic, React uses the useReducer
hook. It is ideal when state updates involve multiple sub-values or actions.
import { useReducer } from "react";
function reducer(state, action) {
switch (action.type) {
case "increment":
return { count: state.count + 1 };
case "decrement":
return { count: state.count - 1 };
default:
throw new Error();
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, { count: 0 });
return (
<div>
<p>Current count: {state.count}</p>
<button onClick={() => dispatch({ type: "increment" })}>Increment</button>
<button onClick={() => dispatch({ type: "decrement" })}>Decrement</button>
</div>
);
}
export default Counter;
To derive state based on other variables, React uses memoisation with useMemo
. This hook optimises performance by storing computed values and preventing unnecessary recalculations when dependencies haven't changed.
import { useState, useMemo } from "react";
function DoubleCounter() {
const [count, setCount] = useState(0);
// Derived state
const doubleCount = useMemo(() => count * 2, [count]);
return (
<div>
<p>Count: {count}</p>
<p>Double Count: {doubleCount}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
export default DoubleCounter;
Vue
In Vue, you use ref
to manage state reactively:
<script setup>
import { ref } from "vue";
const count = ref(0);
</script>
<template>
<div>
<p>Count: {{ count }}</p>
<button @click="count++">Increment</button>
</div>
</template>
Alternatively you can use reactive
function to creates a reactive object, which is useful for managing more complex state (e.g., multiple related properties).
<script setup>
import { reactive } from "vue";
const state = reactive({
count: 0,
message: "Hello, Vue 3!",
});
const increment = () => {
state.count++;
};
const updateMessage = () => {
state.message = "You updated the message!";
};
</script>
<template>
<div>
<p>Count: {{ state.count }}</p>
<p>Message: {{ state.message }}</p>
<button @click="increment">Increment</button>
<button @click="updateMessage">Change Message</button>
</div>
</template>
In Vue you can derive state with computed
function, which automatically updates when its dependencies change.
<script setup>
import { ref, computed } from "vue";
const count = ref(0);
const doubleCount = computed(() => count.value * 2);
const increment = () => {
count.value++;
};
</script>
<template>
<div>
<p>Count: {{ count }}</p>
<p>Double Count: {{ doubleCount }}</p>
<button @click="increment">Increment</button>
</div>
</template>
Key Differences:
- Vue uses
ref
for creating reactive variables. - For objects with multiple properties, Vue's
reactive
function is more appropriate. - Use
computed
properties to create derived or computed state. - When working in the
script
section, always accessref
values with.value
. - Vue uses double curly braces
{{ }}
for data binding in templates. Unlike in thescript
section, you don't need to manually unwrap reactive values in templates. - Vue uses a Proxy-based reactivity system that offers several advantages over React. Vue uses Proxies to observe changes in reactive objects and automatically track dependencies, allowing it to detect mutations directly. React, in contrast, relies on explicit updates (via
setState
or similar) and immutable data structures to trigger reactivity, avoiding deep observation of objects.
Two-Way Binding with v-model
In Vue, v-model
simplifies two-way data binding between form inputs and reactive variables. This feature is intuitive and eliminates the need for explicit event listeners and state updates, as commonly done in React.
React
React does not have built-in two-way binding. Instead, developers manually link the state to the input’s value and use event handlers to update the state:
import { useState } from "react";
function TwoWayBindingExample() {
const [inputValue, setInputValue] = useState("");
const handleChange = (event) => {
setInputValue(event.target.value);
};
return (
<div>
<input value={inputValue} onChange={handleChange} placeholder="Type something..." />
<p>You typed: {inputValue}</p>
</div>
);
}
export default TwoWayBindingExample;
In Vue, using v-model
is straightforward. It automatically binds the input’s value to a reactive variable and updates the variable whenever the input changes.
Vue
<script setup>
import { ref } from "vue";
const inputValue = ref("");
</script>
<template>
<div>
<input v-model="inputValue" placeholder="Type something..." />
<p>You typed: {{ inputValue }}</p>
</div>
</template>
Key Differences
- Vue offers cleaner syntax with
v-model
, while React needs manual event handling
Lifecycle Hooks
React
React’s lifecycle hooks like useEffect
handle mount and unmount logic:
import { useEffect } from "react";
function ExampleComponent() {
useEffect(() => {
console.log("Component mounted");
return () => {
console.log("Component unmounted");
};
}, []);
return <div>Lifecycle example</div>;
}
export default ExampleComponent;
Vue
Vue provides lifecycle hooks like onMounted
and onUnmounted
:
<script setup>
import { onMounted, onUnmounted } from "vue";
onMounted(() => {
console.log("Component mounted");
});
onUnmounted(() => {
console.log("Component unmounted");
});
</script>
<template>
<div>Lifecycle example</div>
</template>
Key Differences:
- Vue uses separate lifecycle hooks such as
onMounted
andonUnmounted
to handle lifecycle events.
Watching and Tracking Changes
React
React doesn’t have watchers; you use useEffect
:
import { useState, useEffect } from "react";
function ExampleComponent() {
const [count, setCount] = useState(0);
useEffect(() => {
console.log("Count changed:", count);
}, [count]);
return <button onClick={() => setCount(count + 1)}>Increment</button>;
}
Vue
Vue provides a watch
function to observe changes:
<script setup>
import { ref, watch } from "vue";
const count = ref(0);
watch(count, (newValue) => {
console.log("Count changed:", newValue);
});
</script>
<template>
<button @click="count++">Increment</button>
</template>
Key Differences:
- Vue’s
watch
directly observes reactive variables.
Conditional Rendering
React
React uses JavaScript expressions or ternary operators:
function ExampleComponent() {
const isLoggedIn = true;
return <p>{isLoggedIn ? "Welcome back!" : "Please log in."}</p>;
}
Vue
Vue uses directives like v-if
and v-else
:
Key Differences:
- Vue uses declarative directives (
v-if
,v-else
) for conditional rendering.
<script setup>
const isLoggedIn = true;
</script>
<template>
<p v-if="isLoggedIn">Welcome back!</p>
<p v-else>Please log in.</p>
</template>
Passing Props to Child Components
React
In React, props are passed by adding attributes to the child component in the parent component. The child component accesses these props through the props
parameter.
Parent Component
function Parent() {
return <Child message="Hello from Parent!" />;
}
export default Parent;
Child Component
function Child(props) {
return <h1>{props.message}</h1>;
}
export default Child;
Vue
In Vue, props are passed in a similar way by using attributes on the child component in the parent. However, the child component explicitly defines the props it expects.
Parent Component
<script setup>
import Child from "./Child.vue";
</script>
<template>
<Child message="Hello from Parent!" />
</template>
Child Component
<script setup>
defineProps({
message: String,
});
</script>
<template>
<h1>{{ message }}</h1>
</template>
Key Differences
- Vue requires explicit prop definitions using
defineProps
, while React doesn't require prop declarations - Vue allows direct access to props as variables in the
template
section, while React requires accessing them through the props object unless they are destructured. - Vue has built-in prop type checking with
defineProps
, while React relies on PropTypes or TypeScript
Passing Components in Slots
React
React uses children props for dynamic content:
function Modal({ children }) {
return <div className="modal">{children}</div>;
}
function ExampleComponent() {
return (
<Modal>
<h1>Modal Content</h1>
</Modal>
);
}
Vue
In Vue, components need to be declared in separate files. We use the <slot>
component as a placeholder where we expect the child component to be passed.
Modal Component (Modal.vue
)
<template>
<div class="modal">
<slot></slot>
</div>
</template>
Parent Component
<script setup>
import Modal from "./Modal.vue";
</script>
<template>
<Modal>
<h1>Modal Content</h1>
</Modal>
</template>
Vue Named Slots:
With Vue named slots, we can define multiple slot places in the parent component and pass different children.
Modal.vue
<template>
<div class="modal">
<slot name="header"></slot>
<slot></slot>
</div>
</template>
Parent Component
<template>
<Modal>
<template #header>
<h1>Modal Header</h1>
</template>
<p>Modal Body</p>
</Modal>
</template>
Key Differences:
- Vue’s
slot
allows for flexible content placement within components.
Handling Events Between Parent and Child Components
React
React passes functions as props to children:
function Child({ onClick }) {
return <button onClick={onClick}>Click me</button>;
}
function ExampleComponent() {
const handleClick = () => alert("Button clicked!");
return <Child onClick={handleClick} />;
}
Vue
Vue uses emit
for event communication:
Children Component
<script setup>
const emit = defineEmits(["clicked"]);
</script>
<template>
<button @click="emit('clicked')">Click me</button>
</template>
Parent Component
<script setup>
import Children from "./Children.vue";
const handleClick = () => console.log("Button clicked!");
</script>
<template>
<Children @clicked="handleClick" />
</template>
Key Differences:
- Vue’s
emit
is designed for child-to-parent communication.
Accessing DOM Elements with Refs
React
React uses useRef
to access DOM elements:
import { useRef } from "react";
function ExampleComponent() {
const inputRef = useRef();
const focusInput = () => {
inputRef.current.focus();
};
return (
<div>
<input ref={inputRef} type="text" />
<button onClick={focusInput}>Focus Input</button>
</div>
);
}
Vue
Vue uses template refs to access DOM elements:
<script setup>
import { ref } from "vue";
const inputRef = ref();
const focusInput = () => {
inputRef.value.focus();
};
</script>
<template>
<div>
<input ref="inputRef" type="text" />
<button @click="focusInput">Focus Input</button>
</div>
</template>
Key Differences:
- Vue’s
ref
is bound to DOM elements using theref
attribute in templates. - Access the element via
ref.value
.
Reusable Logic
React
React uses custom hooks for reusable logic:
function useCounter() {
const [count, setCount] = useState(0);
const increment = () => setCount(count + 1);
return { count, increment };
}
function ExampleComponent() {
const { count, increment } = useCounter();
return <button onClick={increment}>Count: {count}</button>;
}
Vue
Vue uses composables for reusable logic:
composables/useCounter.js
import { ref } from "vue";
export function useCounter() {
const count = ref(0);
const increment = () => count.value++;
return { count, increment };
}
Component.vue
<script setup>
import { useCounter } from "./composables/useCounter";
const { count, increment } = useCounter();
</script>
<template>
<button @click="increment">Count: {{ count }}</button>
</template>
Key Differences:
- Composables are plain functions that return reactive state and methods.
- They are imported and used like hooks in React.
- Even though React hooks and Vue composables are very similar, they work fundamentally differently under the hood. For example:
- In Vue, the reactive state defined in a composable is shared and persistent when imported across multiple components. This allows composables to act as a lightweight state management solution, eliminating the need for external libraries like Redux for many use cases.
- Vue composables do not rely on a component's lifecycle or context, unlike React hooks which must be used within a component or another hook. This makes composables more flexible and usable in non-component logic like services or standalone utilities.
Shared State Between Components with Vue Composables
Here’s an example of a Vue composable that creates a shared counter state. This composable will be imported and used in two different components, and the state (count
) will be persistent across both components:
composables/useSharedCounter.js
import { ref } from "vue";
const count = ref(0);
export function useSharedCounter() {
const increment = () => count.value++;
const decrement = () => count.value--;
return { count, increment, decrement };
}
ComponentA.vue
<script setup>
import { useSharedCounter } from "./composables/useSharedCounter";
const { count, increment } = useSharedCounter();
</script>
<template>
<div>
<h2>Component A</h2>
<button @click="increment">Increment in A</button>
<p>Count: {{ count }}</p>
</div>
</template>
ComponentB.vue
<script setup>
import { useSharedCounter } from "./composables/useSharedCounter";
const { count, decrement } = useSharedCounter();
</script>
<template>
<div>
<h2>Component B</h2>
<button @click="decrement">Decrement in B</button>
<p>Count: {{ count }}</p>
</div>
</template>
Both ComponentA
and ComponentB
in your app, they will share the same count
state, and any changes in one will be reflected in the other.
Conclusion
Vue and React both excel in building modern web applications, but they take different paths. Vue's declarative syntax, reactivity, and lifecycle hooks make it approachable for beginners, at least in my experience. Having spent considerable time with both frameworks, I believe that while their approaches differ, they share core concepts and offer similar features.
This guide only scratches the surface. More advanced features like Vue Router and state management with Pinia are beyond the scope of this beginner's guide, but you can explore them at your own pace.
Happy coding!
The code for this is available in the following GitHub repository here.