encies often omitted or incorrect. | Auto-generated dependency arrays based on prop usage. |
| Boilerplate | High; repetitive prop drilling and interface definitions. | Minimal; compiler generates interfaces and wrappers. |
Why This Matters: The compiler doesn't just rename strings; it reconstructs the data flow. By converting v-model directives into controlled prop patterns and generating stable useCallback wrappers, VuReact ensures the React output adheres to best practices that developers might overlook during manual translation.
Core Solution
The compilation process operates in three distinct phases: interface generation, call-site transformation, and v-model normalization.
Vue's defineEmits() macro is analyzed to extract event names and payload signatures. The compiler generates a React interface where each event becomes an optional callback prop. Event names are normalized to React conventions: kebab-case becomes camelCase, and the on prefix is applied.
Vue Source:
<script setup lang="ts">
interface OrderPayload {
orderId: string;
total: number;
}
const emit = defineEmits<{
(e: 'submit-order', payload: OrderPayload): void;
(e: 'change:status', newStatus: 'pending' | 'processing'): void;
}>();
</script>
Compiled React Output:
interface OrderFormProps {
onSubmitOrder?: (payload: OrderPayload) => void;
onChangeStatus?: (newStatus: 'pending' | 'processing') => void;
}
// The compiler generates the interface and ensures types match exactly.
// Note: 'change:status' maps to 'onChangeStatus', preserving the v-model pattern.
2. Emit Call-Site Optimization
Calls to emit() are replaced with optional chaining on the generated props. The compiler wraps these invocations in useCallback to maintain referential stability, automatically inferring the dependency array from the props used.
Vue Source:
const handleCheckout = () => {
emit('submit-order', { orderId: 'ORD-99', total: 150 });
};
const updateWorkflow = (status: 'pending' | 'processing') => {
emit('change:status', status);
};
Compiled React Output:
const handleCheckout = useCallback(() => {
props.onSubmitOrder?.({ orderId: 'ORD-99', total: 150 });
}, [props.onSubmitOrder]);
const updateWorkflow = useCallback((status: 'pending' | 'processing') => {
props.onChangeStatus?.(status);
}, [props.onChangeStatus]);
Rationale:
- Optional Chaining: React props are optional by default. The
?. operator prevents runtime errors if the parent does not provide a callback.
- Dependency Arrays: The compiler extracts
props.onSubmitOrder and props.onChangeStatus into the dependency array. This ensures the callback updates if the parent passes a new function reference, preventing stale closures.
3. v-model Normalization
Vue's v-model:prop syntax is syntactic sugar for passing a value and listening to an update:prop event. VuReact detects this pattern and compiles it into a controlled component pattern in React.
Vue Parent Usage:
<template>
<OrderForm v-model:status="currentStatus" />
</template>
<script setup>
const currentStatus = ref('pending');
</script>
Compiled React Output:
const Dashboard = memo(() => {
// useVRef bridges Vue's reactivity model to React's state management
const currentStatus = useVRef('pending');
return (
<OrderForm
status={currentStatus.value}
onChangeStatus={(val) => (currentStatus.value = val)}
/>
);
});
Rationale:
- Controlled Props: The compiler splits
v-model:status into status (prop) and onChangeStatus (callback).
- State Bridge:
useVRef is utilized to maintain the reactive binding. This allows the compiled code to handle updates without rewriting the state logic, ensuring the parent component remains synchronized with the child.
Pitfall Guide
1. Missing Optional Chaining in Manual Rewrites
Explanation: Developers often write props.onSubmitOrder(payload) without checking if the prop exists. In React, if the parent omits the callback, this throws a TypeError.
Fix: Always use optional chaining (props.onSubmitOrder?.(payload)). The compiler generates this automatically; manual migrations must enforce it.
2. Incorrect v-model Naming Mapping
Explanation: update:name must map to onUpdateName, not onName. Failing to include the Update prefix breaks the two-way binding contract and causes the parent to miss updates.
Fix: Adhere to the naming convention: update:xxx β onUpdateXxx. This preserves the semantic link between the prop and the callback.
3. Stale Closures in useCallback
Explanation: When manually wrapping emit calls, developers may omit props from the dependency array. This causes the callback to capture an old version of the prop, leading to missed updates or incorrect behavior.
Fix: Ensure every prop used inside the callback is listed in the dependency array. The compiler handles this by scanning the emit arguments.
4. Type Drift in Payloads
Explanation: During manual migration, payload types are sometimes simplified or altered, causing mismatches between the child's expectation and the parent's handler.
Fix: Copy the payload interface verbatim. The compiler preserves the exact TypeScript shape from defineEmits, ensuring zero type drift.
5. v-model Collision with Existing Props
Explanation: If a component already has a prop named onUpdateName and also uses v-model:name, a naming collision occurs.
Fix: The compiler detects collisions and may rename the generated callback or require explicit disambiguation. In manual migrations, audit prop names to avoid conflicts with onUpdateXxx patterns.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Large Legacy Vue App | Full VuReact Compilation | Automates complex v-model and event mapping; reduces migration time by ~70%. | Low initial setup; high long-term savings. |
| New React Feature | Manual Implementation | No Vue code exists; standard React patterns are sufficient. | Zero compilation cost; standard dev time. |
| Hybrid Migration | Incremental Compilation | Compile components one-by-one; allows gradual rollout and testing. | Moderate overhead; lower risk. |
| Custom Event Logic | Post-Compilation Edit | Compiler handles standard cases; manual tweaks needed for complex side effects. | Low cost; maintains type safety. |
Configuration Template
Use this configuration to customize event mapping rules and output formatting in your project.
// vureact.config.ts
import { defineConfig } from 'vureact';
export default defineConfig({
compiler: {
// Enable strict type checking for emit payloads
strictTypes: true,
// Customize event name mapping if needed
naming: {
// Default: update:xxx -> onUpdateXxx
// Override for specific cases
overrides: {
'custom:update': 'onCustomUpdate',
},
},
// Generate useCallback wrappers for all emit calls
optimizeCallbacks: true,
// Output directory for compiled React code
outDir: './src/react-compiled',
},
});
Quick Start Guide
- Install VuReact: Run
npm install vureact --save-dev to add the compiler to your project.
- Configure: Create
vureact.config.ts with the template above and adjust paths as needed.
- Compile: Execute
npx vureact compile ./src/vue-components to process your Vue files.
- Verify: Review the generated React components in the output directory. Check interfaces and callback usage.
- Integrate: Import the compiled components into your React application and run your test suite to validate behavior.