ification streams without rewriting animation logic or sacrificing performance. The compiled output behaves identically to the source, but lives natively within React's component tree.
Core Solution
Compilation Pipeline Architecture
The transformation from Vue <TransitionGroup> to React relies on a three-phase pipeline: AST parsing, prop normalization, and runtime adapter mapping.
- AST Parsing: The compiler scans the Vue template and identifies
<TransitionGroup> nodes. It extracts the name, tag, move-class, duration, and event listeners (@enter, @leave, @before-enter, etc.).
- Prop Normalization: Vue's kebab-case props and event prefixes are converted to React's camelCase convention.
move-class becomes moveClass, @enter becomes onEnter, and :duration becomes duration={}.
- Runtime Adapter Mapping: The compiler replaces the Vue component with an import from
@vureact/runtime-core. The adapter acts as a bridge, translating Vue's transition class hooks into React's effect-driven class application.
New Code Example: Task Queue Migration
Consider a task management interface where items slide in, fade out, and smoothly reorder when dragged.
Original Vue Source
<template>
<TransitionGroup
name="task-stack"
tag="section"
move-class="task-shift"
:duration="400"
@enter="handleEnter"
@leave="handleLeave"
>
<article
v-for="task in taskQueue"
:key="task.uid"
class="task-card"
>
<h3>{{ task.title }}</h3>
<span class="priority">{{ task.priority }}</span>
</article>
</TransitionGroup>
</template>
Compiled React Output
import { TransitionGroup } from '@vureact/runtime-core';
export function TaskQueue({ taskQueue, handleEnter, handleLeave }) {
return (
<TransitionGroup
name="task-stack"
tag="section"
moveClass="task-shift"
duration={400}
onEnter={handleEnter}
onLeave={handleLeave}
>
{taskQueue.map((task) => (
<article key={task.uid} className="task-card">
<h3>{task.title}</h3>
<span className="priority">{task.priority}</span>
</article>
))}
</TransitionGroup>
);
}
Architecture Decisions & Rationale
Why map v-for to .map()?
React's reconciliation algorithm requires explicit key tracking during iteration. The compiler converts Vue's template directive into JSX .map() calls, ensuring each element receives a stable key prop. This preserves React's diffing efficiency while maintaining the exact DOM structure Vue would produce.
Why preserve the tag prop?
The tag attribute defines the wrapper element that contains the animated children. In React, this is implemented as a lightweight wrapper component that renders the specified HTML element (e.g., section, div, ul). Keeping tag explicit prevents unnecessary DOM nesting and ensures CSS selectors targeting the container remain valid post-migration.
How are move transitions handled?
Vue's <TransitionGroup> uses the FLIP technique for reordering. The runtime adapter captures the initial bounding box (First), waits for React to update the DOM, captures the final bounding box (Last), calculates the inverse transform (Invert), and applies a CSS transition to animate to the final position (Play). The moveClass prop injects the necessary transition properties. This approach avoids layout thrashing and maintains 60fps performance even with large lists.
Why use a runtime adapter instead of pure CSS?
Vue's transition system relies on precise class injection timing (v-enter-from, v-enter-active, v-leave-to, etc.). React's concurrent rendering and strict mode can cause double-invocations that break pure CSS class toggling. The adapter synchronizes class application with React's commit phase, ensuring hooks fire exactly once per lifecycle event and preventing animation state corruption.
Pitfall Guide
1. Unstable Keys Causing Animation Desync
Explanation: React's reconciliation depends on stable keys. If key values change between renders (e.g., using array indices or generated IDs), the adapter cannot track element positions, causing enter/leave animations to skip or duplicate.
Fix: Always use deterministic, persistent identifiers (UUIDs, database IDs, or hashed content). Never use index in dynamic lists with transitions.
2. Missing position: absolute on Leave Transitions
Explanation: When an item leaves, it must be removed from the document flow so remaining items can shift into place. Without absolute positioning, the leaving element occupies space, blocking FLIP calculations for siblings.
Fix: Add .task-stack-leave-active { position: absolute; } to your CSS. Ensure the parent container has position: relative to contain the absolute child.
3. CSS Specificity Conflicts with Auto-Generated Classes
Explanation: The adapter applies classes like .task-stack-enter-from and .task-stack-move. If your global CSS or UI framework uses higher specificity, these classes may be overridden, breaking animations.
Fix: Scope transition CSS to the component or use CSS modules. Avoid !important. Verify computed styles in DevTools to ensure transition classes are applied.
4. Ignoring React Strict Mode Double-Invocations
Explanation: React Strict Mode mounts components twice in development. If your onEnter or onLeave hooks mutate state or trigger side effects, they may fire twice, causing animation loops or state corruption.
Fix: Make hooks idempotent. Use refs to track animation state, or wrap side effects in useEffect cleanup functions. Test in production mode to verify behavior.
5. Overusing tag on Inline Elements
Explanation: Setting tag="span" or tag="a" on a <TransitionGroup> containing block-level children breaks layout flow and prevents proper height/width calculations during transitions.
Fix: Use block-level containers (div, section, ul) for list transitions. If inline layout is required, apply display: flex or display: grid to the container and ensure children are properly sized.
Explanation: FLIP animations rely on transform and opacity for GPU acceleration. If moveClass only defines margin or top/left changes, the browser falls back to CPU layout recalculations, causing jank.
Fix: Define moveClass with transition: transform 0.4s ease, opacity 0.4s ease;. Avoid animating layout-triggering properties like width, height, or margin.
7. Mixing JS Hooks with CSS animationend
Explanation: Calling onEnter while also listening to animationend in the same component creates race conditions. The adapter expects hooks to manage lifecycle state, not CSS events.
Fix: Choose one approach. Use adapter hooks for state updates and cleanup. Remove manual addEventListener('animationend') calls to prevent duplicate triggers.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Legacy Vue app with heavy list animations | VuReact Compiler + Runtime Adapter | Preserves exact animation semantics, reduces rewrite time by 70%+ | Low (tooling setup) |
| New React project starting from scratch | Framer Motion or React Spring | Native React ecosystem, better tree-shaking, active community | Medium (learning curve) |
| Micro-interactions on static lists | Pure CSS + React state | Zero runtime overhead, simple to maintain | Low |
| Complex staggered choreography | GSAP + React refs | Frame-accurate control, timeline sequencing | High (implementation time) |
Configuration Template
// vite.config.ts
import { defineConfig } from 'vite';
import vueReact from '@vureact/vite-plugin';
export default defineConfig({
plugins: [
vueReact({
runtime: '@vureact/runtime-core',
transform: {
transitionGroup: {
preserveHooks: true,
enableFLIP: true,
strictModeSafe: true,
},
},
css: {
prefix: 'vureact',
scope: 'module',
},
}),
],
build: {
rollupOptions: {
external: ['@vureact/runtime-core'],
},
},
});
Quick Start Guide
- Install the toolchain: Run
npm install @vureact/vite-plugin @vureact/runtime-core in your project root.
- Configure the plugin: Add the VueReact Vite plugin to your
vite.config.ts using the template above. Enable preserveHooks and enableFLIP for full transition support.
- Migrate a component: Replace a Vue
<TransitionGroup> block with the compiled JSX output. Ensure key props use stable IDs and moveClass includes transform transitions.
- Validate in dev: Start the dev server. Verify enter/leave animations trigger correctly and list reordering uses smooth FLIP transitions. Check DevTools for class application timing.
- Build for production: Run
npm run build. Confirm the runtime adapter is bundled correctly and animation performance remains consistent under concurrent rendering conditions.