VISASQ Dev Blog

ビザスク開発ブログ

React Vs Vue [English]

Introduction

After spending years developing with React and Nextjs, I recently joined VISASQ where almost all of the frontend development happens in Vue. I was in a team where we migrated from Vue 2 to Vue 3 and currently in a team where we are using Vue3 with Nuxt 3 (recently upgraded to Nuxt 4). This transition gave me a unique opportunity to look at both frameworks from real-world projects. Coming from React's flexible ecosystem into Vue's more structured environment was both refreshing and revealing. Also, this transition made me dive into deeper understanding of how the web works in both of these tools and what are the core differences between them. This isn't a "which is better" post, but a technical differences experienced during the transition.

The fundamental differences: How they render updates

  • React: Pull-based rendering
  • Vue: Push-based reactivity

Both of them use a virtual DOM, but they differ in how they update the DOM. React uses its Fiber architecture to reconcile the changes and re-renders the subtrees. The optimization is done with useMemo, useCallback and React.memo to prevent unnecessary re-renders. Vue compiles templates into efficient render functions and updates only the parts that depend on reactive data instead of comparing the entire component tree. This means fewer re-renders and better performance in complex views. In practice, our Vue 3 pages with heavy data grids and forms felt smoother than the equivalent React ones.

// React:
const [items, setItems] = useState([1,2,3]);
setItems([...items, 4]); // Must create an array

// Vue3 direct mutation:
const items = ref([1,2,3]);
items.value.push(4);

In React, a parent state change often causes child re-renders unless carefully memoized whereas in Vue, it is handled naturally without any optimization. Our vue2 to vue3 migration revealed the depth of this reactivity improvement. Vue2 used Object.defineProperty, which couldn't detect property additions or deletions

Composition API vs hooks

Here is a quick summary of how Vue 3's Composition API compares to React Hooks, based on the topics covered in this blog:

Concept React Vue 3
Function body re-executes Yes No
Dependency tracking Manual (dependencies array) Automatic
Sharing logic Custom hooks Composables
Context Context API Provide / Inject
Lifecycle useEffect, useLayoutEffect, etc. onMounted, onUnmounted, etc.

Component Philosophy: Template vs JSX

React uses JSX everywhere and keeps everything in JavaScript, giving the full flexibility of the language for logic and complex conditional rendering

// React:
return(
    <div>
        {status === "loading" ? <Spinner/>:
        status === "error" ? <Error message={error.message}/>:
        items.length === 0 ? <Empty/>:
        <List items={items}/>}
    </div>
)

// Vue - more verbose:
<template>
<div>
    <Spinner v-if="status === 'loading'"/>
    <Error v-else-if="status === 'error'" :message="error.message"/>
    <Empty v-else-if="items.length === 0"/>
    <List v-else :items="items"/>
</div>
</template>

The Vue compiler can hoist static content outside the render function, cache event handlers automatically. These optimizations aren't possible with JSX because the compiler can't make assumptions about arbitrary JS code. I still prefer JSX for complex conditional rendering. Whenever there are nested ternaries or multiple status checks, JavaScript's natural flow is cleaner than chaining v-if, v-else-if.

State Management

In React, the traditional style of state management is Redux, but I am using zustand. The reason for that is simplicity with minimal boilerplate compared to verbose actions of Redux. Zustand has become my default choice because it requires no providers, has excellent Typescript support, works outside React components and provides great devtools integration.

Vue hsa a similar journey. Vuex felt like early Redux verbose mutations and actions. Pinia, Vue3's recommended solution is simple.

For the separation of client and server state, I used tanstack-query (formerly react-query) and it changed how I think about data fetching. I believe that API data shouldn't live in zustand or Redux,. Tanstack query manages server state, and zustand manages UI state like modals, filters, user preferences etc. Tanstack query works with Vue too, providing the same state management benefits, but currently in our projects we haven't adopted it. While tanstack query is excellent for handling complex server-state scenarios, our Vue/Nuxt architecture already provides a clear, typed and maintainable separation between server and client state. - Pinia manages application and UI state - Nuxt handles data fetching and SSR reactivity - Vueuse enhances UI behavior For our current scale and architecture, this combination offers the right balance of simplicity, flexibility and control without the need for an additional data-fetching library

Styling Approaches

Both frameworks support multiple styling approaches, but they have different defaults and strengths. React developers typically choose between CSS modules for standard CSS, CSS-in-JS libraries for dynamic styling with runtime cost, tailwind for utility-first classes, or vanilla for zero-runtime CSS-in-JS with Typescript support. Vue's Single File Components have scoped styles built-in. You write regular CSS in a <style> block and it automatically scopes to that component. For dynamic values, Vue 3 introduced v-bind() in styles, letting you bind CSS custom properties to reactive JavaScript values without any runtime CSS-in-JS cost. This is more performant than Styled Components because it compiles to CSS variables rather than generating styles at runtime.

Side effects and lifestyle

React overloads useEffect for everything: data fetching, event listeners, document title updates and subscriptions.

function UserProfile({userId}) {
    const [user, setUser] = useState(null);

    // Data fetching
    useEffect(() => {
        const handler = () => console.log('resize');
        window.addEventListener('resize', handler);
        return () => window.removeEventListener('resize', handler);
    },[])

    // Document title
    useEffect(() => {
        document.title = user?.name || 'Loading...';
    }, [user]);

    return <div>{user?.name}</div>
}

This creates challenges with dependency arrays, cleanup functions, and understanding when effects run. The dependency array is a constant source of bugs, missing dependencies cause stale closures, while object or function dependencies causes infinite loops. This is one of the reasons why, whenever I setup a Next.js project from scratch, tanstack query is my default go-to to replace useEffect.

Vue 3's Composition API has specific lifecycle hooks that make intent clearer:

<script setup>
import {ref, onMounted, onUnMounted, watch, watchEffect} from 'vue';

const user = ref(null);
const props = defineProps(['userId']);

// Setup with cleanup
onMounted(() => {
    const handler = () => console.log('resize');
    window.addEventListener('resize', handler);

    onUnmounted(() => {
        window.removeEventListener('resize', handler);
    })
});

// Watch specific dependencies
watch(() => props.userId, async(newId) => {
    user.value = await fetchUser(newId);
}, { immediate: true });

// Auto track all dependencies
watchEffect(() => {
    document.title = user.value?.name || 'Loading...';
});
</script>

The key advantage is there is no dependency arrays. Vue's reactivity automatically tracks what you use. This removes the bugs caused by outdated closures and missing dependencies that trouble React developers. The timing is also different. Vue's watchEffect runs synchronously on mount by default, while React's useEffect runs after paint.

SSR, SSG, SPA, SEO

Next.js has evolved significantly with the App Router introducing Server Components. The default is now server-side rendering with zero JavaScript for non-interactive parts. You mark interactive components with 'use client', and Next.js automatically code-splits and hydrates only what's necessary.

Nuxt 3 takes a different approach with universal rendering by default. The same code runs on both server and client, and useFetch automatically handles the server-to-client state transfer. Nuxt's route rules let you set different rendering strategies per route. Both frameworks excel at SEO with built-in meta tag management.

Build Systems and Performance

The build tool landscape has consolidated around Vite for both frameworks. React used to rely on Create React App with webpack, but that's now deprecated. Vite provides instant server start, fast hot module replacement, and quick production builds. Next.js has its own build system that historically used Webpack but is transitioning to Turbopack for faster builds. Vue officially adopted Vite, giving developers a consistent and efficient experience. In terms of bundle size, Vue is much smaller than React, which can make a noticeable difference for content-heavy sites. However, for larger apps, the framework size is usually small compared to the rest of the code. Both ecosystems support tree-shaking and code-splitting, ensuring users only download what is needed for the current page. Next.js and Nuxt handles this automatically, so performance optimization comes built-in.

Next js 15 vs Nuxt 3

Next.js is a flexible React framework for building SPAs, static sites, or server-rendered apps. Its App Router organizes pages, layouts, and loading states together, while Server Components reduce client-side JavaScript. Nuxt is Vue's full-stack framework built on convention over configuration. It offers universal rendering, auto-imports, and a clear folder structure that keeps code organized with minimal setup. Both support middleware for auth and redirects. Next.js separates data fetching between Server and Client Components, while Nuxt's useFetch works seamlessly on both. `For deployment, Nuxt's Nitro engine supports multiple platforms out of the box, while Next.js is optimized for Vercel but adaptable elsewhere

Testing with Vitest

Vitest has become the standard test runner for both React and Vue projects using Vite. It's Jest-compatible but dramatically faster because it uses Vite's transformation pipeline. For React, you combine Vitest with Testing Library. Vue testing uses Vue Test Utils. The performance difference is substantial. A test suite with 500 tests might take 15-20 seconds in Jest but only 3-5 seconds in Vitest. The watch mode is nearly instantaneous, re-running only affected tests in under a second.

Practical Pros and Cons

Framework Pros Cons
React - Largest ecosystem and community
- JSX allows complex logic
- React Native for mobile code sharing
- Predictable data flow due to explicit re-renders
- Larger bundle size
- Requires manual optimization to avoid unnecessary re-renders
- Complex hoods and dependency arrays
- Fragmented tooling choices
Vue - Smaller bundle size
- Better performance via fine-grained reactivity
- Less boilerplate
- Gentler learning curve
- Official Integrated solutions (routing, state management)
- Compiler handles most optimizations
- Great developer experience (auto-imports, scoped styles)
- Smaller ecosystem and fewer third-party libraries
- Smaller hiring pool
- Template limitations for complex logic
- Less adoption in enterprise environments
Next.js - Cutting edge SSR features (Server Components, streaming)
- Excellent image optimizations
- Best deployment experience on Vercel
- Advanced rendering strategies (SSR)
- Complexity of Server vs Client Components
- Breaking changes between major versions
- App Router has a steep learning curve
Nuxt - Convention over configuration, less boilerplate
- Auto-imports everywhere
- Flexible deployment via Nitro presets
- Smaller bundle size
- Unified rendering code
- Intuitive file-based routing
- Smaller ecosystem than Next.js
- Opinions can feel constraining
- "Magic" features may confuse debugging
- Less corporate adoption (through growing)

Conclusion

After few years with React and recent intensive work with Vue, I've learned that both frameworks are exceptional tools that excel in different ways. The fundamental differences in reactivity versus re-rendering isn't just technical, it changes how you think about updates. My perspective has evolved from "React is the only way" to appreciating what both frameworks bring.

What matters isn't React vs Vue. What matters is building great experiences for users, and both frameworks enable that when used thoughtfully.

ビザスクではエンジニアの仲間を募集しています! 少しでもビザスク開発組織にご興味を持たれた方は、ぜひ一度カジュアルにお話ししましょう! recruit.visasq.co.jp