DEV Community

Cover image for Vue to React Migration: Why Runtime Wrappers Hit a Ceiling
Ryan John
Ryan John

Posted on

Vue to React Migration: Why Runtime Wrappers Hit a Ceiling

If you have ever loved Vue's Composition API and <script setup> but still had to ship a React codebase because of platform or team constraints, you already know the pain: the code is runnable, but it feels awkward, brittle, and expensive to maintain.

That tension is exactly what VuReact is designed to solve. VuReact is a compiler toolchain for migrating from Vue to React — and for writing React with Vue 3 syntax.

In this post, I want to answer a very practical question:

Can we write with familiar Vue semantics and still end up with clean, idiomatic React?

The answer is yes. And more importantly, the final output is not a pile of wrapper code. It is maintainable React that fits the target ecosystem.

You do not need to learn a brand-new authoring model, and you do not need to keep bouncing between Vue and React mental models. The examples in this article all have source-to-output references in the semantic conversion guide, so you can compare the input and the generated result side by side.

The Limits of Existing Approaches

Let us start with the reality of migration work.

Manual rewriting gives you the most control, but the cost is obvious: duplicated business logic, larger test surface area, and a long period where old and new code coexist.

Many syntax conversion tools are also limited in a similar way. They can do surface-level replacement, recognize v-if, and mechanically rewrite v-model, but they cannot understand the full semantic contract behind computed, scoped slots, or the data flow hidden inside a component design.

The final output often looks like React, but behaves like a fragile transplant.

Some teams then ask the obvious next question:

What if we just let AI rewrite the code?

That route looks fast, but it hides a few serious problems. AI is great at generating new code, yet it is not naturally good at preserving the logic, lifecycle assumptions, and edge-case behavior of an existing codebase. It can miss business intent, fail to respect why a particular watch exists, and still fall short of faithfully mapping Vue's reactivity model into React.

Single-pass generation is useful. But when the project evolves, it becomes difficult to safely patch or maintain code that was produced as a black box. And because AI output can vary from prompt to prompt, the hidden cost of long-term maintainability becomes very real.

Another common path is a dual-runtime bridge between Vue and React. That can work for a while, but the bundle weight, debugging complexity, and mental overhead all go up. More importantly, that is not a true migration. It is a long-term coexistence strategy.

So the real problem is not whether cross-framework conversion is possible. It is whether it can be done in a way that is stable, controllable, and maintainable over time.

That is the core problem VuReact is trying to solve.

How VuReact Approaches It

VuReact is built around semantic-aware compilation, not string replacement.

The difference matters:

  • String replacement only cares whether two snippets look similar.
  • Semantic-aware compilation first understands what the source code means, then decides how that meaning should be expressed in React.

From the compiler pipeline perspective, VuReact is a full flow: parsing, semantic analysis, transformation, and code generation.

You write Vue concepts such as template syntax, <script setup>, the Composition API, and TypeScript types. The output is standard React code that is readable, predictable, and easy to maintain.

The value proposition can be summarized in five points:

  1. Semantic awareness: it understands templates, directives, lifecycle patterns, and type shapes instead of blindly replacing syntax.
  2. Incremental migration: you can start with a single module and expand step by step.
  3. Convention-driven output: it follows a compilation specification, so the generated result is predictable and CI-friendly.
  4. Full feature alignment: Vue core concepts are mapped into React implementation paths, including scoped style handling during compilation.
  5. Smart dependency collection: reactive dependencies are traced and analyzed automatically to generate precise React Hook dependency arrays.

Most importantly, the output is pure React. It does not depend on a Vue runtime, and it does not hide Vue inside a React container. That is what makes it suitable for long-term maintenance, not just demos.

What the Translation Actually Looks Like

The easiest way to understand VuReact is to look at the actual semantic mappings.

The point is not whether the output "looks like React." The real question is whether the core Vue semantics are faithfully mapped into a standard React implementation.

The code below is intentionally simplified to focus on the conversion logic.

1. Default Slot -> props.children

Vue:

<!-- Child.vue -->
<template>
  <section class="card">
    <slot></slot>
  </section>
</template>

<!-- Parent usage -->
<Child>
  <p>Order summary</p>
</Child>
Enter fullscreen mode Exit fullscreen mode

VuReact output:

// Child.tsx
type ChildProps = {
  children?: React.ReactNode;
};

function Child(props: ChildProps) {
  return <section className="card">{props.children}</section>;
}

export default memo(Child);

// Parent usage
<Child>
  <p>Order summary</p>
</Child>;
Enter fullscreen mode Exit fullscreen mode

This is the kind of mapping that feels natural when you inspect it side by side. Vue's default slot is not guessed. It is explicitly compiled into React's children, which is exactly the idiomatic way to model content composition in React.

2. Scoped Slot -> Render Prop

Vue:

<!-- List.vue -->
<template>
  <ul>
    <li v-for="(item, i) in props.items" :key="item.id">
      <slot :item="item" :index="i"></slot>
    </li>
  </ul>
</template>

<!-- Parent usage -->
<List :items="users">
  <template v-slot="slotProps">
    <div>{{ slotProps.index + 1 }}. {{ slotProps.item.name }}</div>
  </template>
</List>
Enter fullscreen mode Exit fullscreen mode

VuReact output:

// List.tsx
type ListProps = {
  items: any[];
  children?: (slotProps: { item: any; index: number }) => React.ReactNode;
};

function List(props: ListProps) {
  return (
    <ul>
      {props.items.map((item, index) => (
        <li key={item.id}>{props.children?.({ item, index })}</li>
      ))}
    </ul>
  );
}

export default memo(List);

// Parent usage
<List
  items={users}
  children={(slotProps) => (
    <div>
      {slotProps.index + 1}. {slotProps.item.name}
    </div>
  )}
/>
Enter fullscreen mode Exit fullscreen mode

This is one of the strongest parts of VuReact. A scoped slot is really a mechanism for passing data back from child to parent. VuReact does not flatten that idea away. It compiles it into a typed render prop, preserving the original data-flow semantics.

3. computed() and defineEmits() -> React Hooks and Callbacks

Vue:

<script setup lang="ts">
const emit = defineEmits<{
  (e: 'update:name', value: string): void;
}>();

const state = reactive({ count: 1, price: 99 });
const totalPrice = computed(() => state.count * state.price);

const submit = () => {
  emit('update:name', 'next');
};
</script>
Enter fullscreen mode Exit fullscreen mode

VuReact output:

import { useCallback } from 'react';
import { useComputed, useReactive } from '@vureact/runtime-core';

const state = useReactive({ count: 1, price: 99 });
const totalPrice = useComputed(() => state.count * state.price);

type CompProps = {
  onUpdateName?: (value: string) => void;
};

const submit = useCallback(() => {
  props.onUpdateName?.('next');
}, [props.onUpdateName]);
Enter fullscreen mode Exit fullscreen mode

This block contains two important translations.

First, computed becomes useComputed, which preserves the dependency-tracking and memoization semantics.

Second, defineEmits with update:name becomes the React-style callback prop onUpdateName. That solves both sides of the problem: how reactivity lands in React, and how component communication lands in React.

4. <style scoped> -> Scoped CSS via data-css-{hash}

Vue:

<template>
  <div class="card">
    <p class="content">Content</p>
  </div>
</template>

<style scoped>
.card {
  border: 1px solid #e5e5e5;
}
.content {
  font-size: 12px;
}
</style>
Enter fullscreen mode Exit fullscreen mode

VuReact output:

import './card-abc1234.css';

<div className="card" data-css-abc1234>
  <p className="content" data-css-abc1234>
    Content
  </p>
</div>
Enter fullscreen mode Exit fullscreen mode
/* card-abc1234.css */
.card[data-css-abc1234] {
  border: 1px solid #e5e5e5;
}

.content[data-css-abc1234] {
  font-size: 12px;
}
Enter fullscreen mode Exit fullscreen mode

This is where many migration tools start to lose precision. VuReact compiles scoped styles into CSS files with scope markers and injects the matching data-css-{hash} attributes at the right nodes. In other words, the style isolation you relied on in Vue still holds in React.

When you put these mappings together, the pattern becomes clear: VuReact is not trying to make React look like Vue. It is doing semantic-equivalent delivery across the template, script, and style layers. That is the real reason it can work in production-grade projects.

Why This Matters in the AI Era

People often ask: if AI is so powerful now, why do we still need a compiler?

My answer is simple: AI is for exploration, and the compiler is for certainty.

In migration work, uncertainty is the enemy. Pure AI rewriting can be fast, but the output may shift with the prompt. A pure rules engine is stable, but not flexible enough. A semantic compiler gives us the middle ground: deterministic output with room for higher-level automation.

That means AI and VuReact are not competing tools. They are complementary.

VuReact gives you a reliable base. AI can then help with refactoring, naming cleanup, structural extraction, and test generation on top of that base.

For teams migrating a real codebase, that is much more realistic than asking AI to replace everything in one pass.

Real Projects, Not Just a Concept

VuReact is already running in two real backend-oriented projects:

  • A standard TypeScript + Vue + Vite + Vue Router admin backend.
  • A mixed Vue/React project that uses Ant Design and Zustand in a collaborative workflow.

These are not demo-only toy examples. They are online, inspectable projects with real implementation details.

If you want to see how the migration strategy plays out in a real project, I also recommend checking the follow-up case studies linked from the project site.

How to Get Started

You can start with a very lightweight path.

First, install the compiler in an existing Vue 3 project or in a demo repo:

npm install -D @vureact/compiler-core
Enter fullscreen mode Exit fullscreen mode

Then read the quick start guide and run through the smallest possible example. The goal of that first pass is not speed. It is to build confidence around what gets converted cleanly and what needs a convention.

Next, pick a small component with a clear boundary and verify three things:

  • Is the generated code readable?
  • Is the conversion cost acceptable?
  • Does the team feel comfortable maintaining the result?

If you are a tech lead, this is easier to manage because the migration scope is explicit, the progress is measurable, and the return on effort is visible.

If you are a front-end developer, it is easier to get moving because you can keep writing with familiar Vue mental models while producing standard React.

GitHub: https://github.com/vureact-js/core

Closing Thoughts

I have always believed that the end goal of cross-framework development is not to make you memorize more syntax. It is to let you express ideas in a familiar way and deliver them safely into the target ecosystem.

That is what VuReact is built for: taking Vue semantics and generating maintainable React.

If you want to take the next step, I would suggest this order:

  1. Read the semantic mapping guide.
  2. Try the online demos to validate the actual experience.
  3. Inspect the GitHub repo and documentation for the implementation details.

In the AI agent era, the strongest combination is not "AI alone." It is "compiler certainty + AI flexibility." When those two work together, teams can move faster with less risk.

Official site: vureact.top | GitHub | CRM demo | Customer support demo

Top comments (0)