Vue 3 vs React 18: A Comprehensive Comparison from a Developer’s Perspective
Vue 3 vs React 18: A Comprehensive Comparison from a Developer’s Perspective
### Introduction
As a Sr frontend developer who has recently worked extensively with Vue 3 (alongside Vuetify, Pinia, and Vite) and React 18 with Hooks, Context, State Management, etc. throughout the years, I’ve experienced firsthand the strengths and nuances of both frameworks.
This comparison will dive deep into three key areas that significantly impact day-to-day development working with modern Vue 3 and React 18 and it’s ecosystem. Here are the comparisons I have worked with and can distinguish between the two powerful frameworks:
1. Templates and Syntax: JSX vs Vue Templates
2. State Management: Pinia vs Context API and Redux
3. Component Logic: Vue 3 Composition API vs React Hooks
Whether you’re deciding which framework to adopt for your next project or simply curious about how they stack up, this comparison aims to shed light on their practical differences and similarities.
### 1\. Templates and Syntax: JSX vs Vue Templates
### React’s JSX
JSX (JavaScript XML) is a syntax extension for JavaScript that allows you to write HTML-like code within JavaScript. It enables you to describe the UI in a syntax familiar to both JavaScript and HTML developers. Here’s an example of typical React JSX using the good old counter example.
import React, { useState } from 'react';
function Counter() {
const \[count, setCount\] = useState(0);
const doubled = count \* 2;
return (
React Counter
=============
Count: {count}
Doubled: {doubled}
setCount(count + 1)}>
Click me
{count > 5 && (
Count is greater than 5!
)}
{Array.from({ length: count }, (\_, i) => (
))}
);
}
export default Counter;
### Vue’s Template Syntax
Vue uses an HTML-based template syntax that allows you to declaratively bind the rendered DOM to the underlying component’s data. This is similar to what Angular uses as well with [Template Syntax](https://angular.dev/guide/templates#). Here’s the Vue 3 flavor :
<br>import { ref, computed } from 'vue';<br><br>const count = ref(0);<br>const doubled = computed(() => count.value \* 2);<br><br>function increment() {<br> count.value++;<br>}<br>
<br>.counter {<br> padding: 20px;<br>}<br>
### Detailed Analysis
JSX Advantages:
JSX Challenges:
Vue Template Advantages:
Vue Template Challenges:
### 2\. State Management: Pinia vs Context/Redux
#### Pinia (Vue 3)
//stores/userStore.ts
import { defineStore } from 'pinia';
interface User {
id: number;
name: string;
email: string;
}
export const useUserStore = defineStore('users', {
state: () => ({
users: \[\] as User\[\],
loading: false,
error: null as string | null
}),
getters: {
getUserById: (state) => {
return (userId: number) => state.users.find(u => u.id === userId)
}
},
actions: {
async fetchUsers() {
this.loading = true;
try {
const response = await fetch('/api/users');
this.users = await response.json();
} catch (err: any) {
this.error = err.message;
} finally {
this.loading = false;
}
},
addUser(user: User) {
this.users.push(user);
}
}
});
// UserList.vue
<br>import { useUserStore } from '@/stores/userStore';<br>import { onMounted } from 'vue';<br><br>const userStore = useUserStore();<br><br>onMounted(() => {<br> userStore.fetchUsers();<br>});<br>
#### React Context API
// Context/UserContext.tsx
import React, { createContext, useState, useContext } from 'react';
interface User {
id: number;
name: string;
email: string;
}
interface UserContextType {
users: User\[\];
loading: boolean;
error: string | null;
fetchUsers: () => Promise;
addUser: (user: User) => void;
}
const UserContext = createContext(undefined);
export function UserProvider({ children }: { children: React.ReactNode }) {
const \[users, setUsers\] = useState(\[\]);
const \[loading, setLoading\] = useState(false);
const \[error, setError\] = useState(null);
async function fetchUsers() {
setLoading(true);
try {
const response = await fetch('/api/users');
const data = await response.json();
setUsers(data);
} catch (err: any) {
setError(err.message);
} finally {
setLoading(false);
}
}
function addUser(user: User) {
setUsers(prev => \[...prev, user\]);
}
return (
{children}
);
}
// UserList.tsx
function UserList() {
const context = useContext(UserContext);
if (!context) {
throw new Error('UserList must be used within UserProvider');
}
const { users, loading, error, fetchUsers } = context;
useEffect(() => {
fetchUsers();
}, \[\]);
if (loading) return
Loading...
;
if (error) return
Error: {error}
;
return (
{users.map(user => (
))}
);
}
#### Redux Toolkit Example
// store / User / userSlice.ts
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
interface User {
id: number;
name: string;
email: string;
}
interface UserState {
users: User\[\];
loading: boolean;
error: string | null;
}
const initialState: UserState = {
users: \[\],
loading: false,
error: null
};
export const fetchUsers = createAsyncThunk(
'users/fetchUsers',
async () => {
const response = await fetch('/api/users');
return response.json();
}
);
const userSlice = createSlice({
name: 'users',
initialState,
reducers: {
addUser: (state, action) => {
state.users.push(action.payload);
}
},
extraReducers: (builder) => {
builder
state.loading = true;
})
state.users = action.payload;
state.loading = false;
})
state.error = action.error.message || null;
state.loading = false;
});
}
});
// UserList.tsx with Redux
import { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
function UserList() {
const dispatch = useDispatch();
const { users, loading, error } = useSelector(state => state.users);
useEffect(() => {
dispatch(fetchUsers());
}, \[dispatch\]);
if (loading) return
Loading...
;
if (error) return
Error: {error}
;
return (
{users.map(user => (
))}
);
}
### State Management Analysis
Pinia Advantages:
Pinia Challenges:
Context API Advantages:
Context API Challenges:
Redux Advantages:
Redux Challenges:
### 3\. Component Logic: Vue 3 Composition API vs React Hooks
#### Custom Window Width Hook/Composable Example
#### Vue 3 Composable:
// Composables / useWindowWidth.ts
import { ref, onMounted, onUnmounted } from 'vue';
export function useWindowWidth() {
const width = ref(window.innerWidth);
function handleResize() {
width.value = window.innerWidth;
}
onMounted(() => {
window.addEventListener('resize', handleResize);
});
onUnmounted(() => {
window.removeEventListener('resize', handleResize);
});
return width;
}
// Usage in component
<br>import { useWindowWidth } from './useWindowWidth';<br><br>const width = useWindowWidth();<br>
#### React Hook:
// Hooks / useWindowWidth.ts
import { useState, useEffect } from 'react';
function useWindowWidth() {
const \[width, setWidth\] = useState(window.innerWidth);
useEffect(() => {
function handleResize() {
setWidth(window.innerWidth);
}
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, \[\]);
return width;
}
// Usage in component
function WindowWidth() {
const width = useWindowWidth();
return
Window width: {width}px
;
}
#### Data Fetching Example
#### Vue 3 Composition API:
<br>import { ref, onMounted } from 'vue';<br><br>const posts = ref(\[\]);<br>const loading = ref(true);<br>const error = ref(null);<br><br>async function fetchPosts() {<br> try {<br> const response = await fetch('https://api.example.com/posts');<br> posts.value = await response.json();<br> } catch (e) {<br> error.value = e.message;<br> } finally {<br> loading.value = false;<br> }<br>}<br><br>onMounted(fetchPosts);<br>
#### React Hooks:
function Posts() {
const \[posts, setPosts\] = useState(\[\]);
const \[loading, setLoading\] = useState(true);
const \[error, setError\] = useState(null);
useEffect(() => {
async function fetchPosts() {
try {
const response = await fetch('https://api.example.com/posts');
const data = await response.json();
setPosts(data);
} catch (e) {
setError(e.message);
} finally {
setLoading(false);
}
}
fetchPosts();
}, \[\]);
if (loading) return
Loading...
;
if (error) return
{error}
;
return (
{posts.map(post => (
))}
);
}
### Component Logic Analysis
Vue Composition API Advantages:
Vue Composition API Challenges:
React Hooks Advantages:
React Hooks Challenges:
### Conclusion
After extensive work with both frameworks, here are my key takeaways:
Vue 3 Strengths:
React Strengths:
The choice between Vue 3 and React 18 often depends on:
1. Team expertise and preferences
2. Project scale and requirements
3. Ecosystem needs
4. Performance requirements
5. Learning curve considerations
Both frameworks are excellent choices for modern web development, with Vue 3 excelling in developer experience and simplicity, while React offers more flexibility and a larger ecosystem. The decision should be based on your specific project needs rather than general superiority of one over the other.
In the next article I’ll be comparing these examples to Angular 18 as well. Thanks for reading!

Write a comment