le-related regression tickets and significantly faster onboarding for engineers familiar with either framework.
Core Solution
Translating Vue 3 lifecycle hooks to React requires bridging two fundamentally different rendering philosophies. Vue executes lifecycle callbacks synchronously during specific commit phases. React batches effects and defers execution until after paint, relying on dependency arrays to determine re-execution. VuReact resolves this mismatch through a three-phase compilation pipeline: AST interception, dependency extraction, and runtime adapter emission.
Step 1: AST Interception & Hook Mapping
The compiler scans <script setup> blocks for Vue lifecycle imports. When it encounters onMounted, onBeforeUpdate, or onUnmounted, it replaces the import source with @vureact/runtime-core and renames the function to its React-compatible counterpart (useMounted, useBeforeUpdate, useUnmounted). This mapping preserves semantic intent while aligning with React's hook naming conventions.
Vue's reactivity system tracks property access implicitly. React requires explicit dependency arrays. During compilation, VuReact performs static analysis on the hook callback body. It identifies all reactive variable reads, resolves their origin, and generates a dependency array. For update-phase hooks, this array is injected as the second argument. For mount/unmount hooks, no dependencies are required, so the adapter runs once per component instance.
Step 3: Runtime Adapter Execution
The emitted React hooks wrap useEffect with phase-specific guards. useMounted executes after the initial commit. useBeforeUpdate runs before React applies pending state changes, replicating Vue's pre-update snapshot capability. useUnmounted registers a cleanup function that executes when the component tree removes the node. The runtime package handles StrictMode double-invocation by tracking execution state internally, ensuring idempotent behavior.
Code Example: Vue Source
<script setup lang="ts">
import { reactive, onMounted, onBeforeUpdate, onUnmounted } from 'vue';
const telemetry = reactive({
streamActive: false,
lastPacket: null as string | null,
retryCount: 0
});
let wsConnection: WebSocket | null = null;
onMounted(() => {
wsConnection = new WebSocket('wss://metrics.internal/stream');
wsConnection.onmessage = (evt) => {
telemetry.lastPacket = evt.data;
telemetry.streamActive = true;
};
});
onBeforeUpdate(() => {
if (!telemetry.streamActive && telemetry.retryCount < 3) {
telemetry.retryCount++;
console.warn(`Reconnecting attempt ${telemetry.retryCount}`);
}
});
onUnmounted(() => {
wsConnection?.close();
telemetry.streamActive = false;
});
</script>
Compiled React Output
import { useReactive, useMounted, useBeforeUpdate, useUnmounted } from '@vureact/runtime-core';
const telemetry = useReactive({
streamActive: false,
lastPacket: null as string | null,
retryCount: 0
});
let wsConnection: WebSocket | null = null;
useMounted(() => {
wsConnection = new WebSocket('wss://metrics.internal/stream');
wsConnection.onmessage = (evt) => {
telemetry.lastPacket = evt.data;
telemetry.streamActive = true;
};
});
useBeforeUpdate(
() => {
if (!telemetry.streamActive && telemetry.retryCount < 3) {
telemetry.retryCount++;
console.warn(`Reconnecting attempt ${telemetry.retryCount}`);
}
},
[telemetry.streamActive, telemetry.retryCount]
);
useUnmounted(() => {
wsConnection?.close();
telemetry.streamActive = false;
});
Architecture Decisions & Rationale
- Custom Runtime Adapters over Raw
useEffect: React's useEffect cleanup runs before the next effect execution, which breaks Vue's onBeforeUpdate semantics. VuReact's adapters use internal execution flags and effect ordering to guarantee pre-update callbacks fire before state reconciliation.
- AST-Based Dependency Injection: Manual dependency arrays are error-prone and require constant maintenance as component logic evolves. Static analysis extracts dependencies at compile time, eliminating runtime tracking overhead and preventing stale closure bugs.
- StrictMode Idempotency Guards: React 18 invokes effects twice in development. The runtime package tracks mount state internally, ensuring initialization logic doesn't duplicate connections or subscriptions.
- Reactive Proxy Compatibility:
useReactive mirrors Vue's reactive() behavior using Proxy under the hood, ensuring property access triggers React's update cycle without requiring useState boilerplate.
Pitfall Guide
1. Assuming Auto-Generated Dependencies Are Exhaustive
Explanation: The compiler extracts dependencies based on static property access. Dynamic key lookups (state[dynamicKey]) or closure-captured variables outside the reactive object are invisible to the AST analyzer.
Fix: Manually append missing dependencies to the compiled hook call, or refactor dynamic access to use explicit reactive paths.
2. Ignoring React StrictMode Double-Invocation
Explanation: Development builds run mount/unmount cycles twice. Non-idempotent initialization (e.g., attaching event listeners without cleanup) causes duplicate subscriptions or memory leaks.
Fix: Always pair initialization with cleanup in mount hooks. Use the runtime's built-in execution guard or wrap side effects in if (!ref.current.initialized) checks.
3. Mixing Vue ref with React useState Without Synchronization
Explanation: Vue's ref and React's useState manage update queues differently. Directly mutating a Vue ref inside a React effect bypasses React's batching, causing inconsistent renders.
Fix: Stick to useReactive for object state or explicitly sync values using useSyncExternalStore when bridging external state managers.
4. Overusing Update Hooks for Derived State
Explanation: useBeforeUpdate and useUpdated are designed for side effects, not state derivation. Using them to compute values triggers unnecessary re-renders and breaks React's memoization patterns.
Fix: Move derived calculations to useMemo or compute them inline. Reserve update hooks for DOM measurements, analytics, or external system synchronization.
5. Forgetting SSR Hydration Mismatches
Explanation: Lifecycle hooks execute on the client only. If compiled components render server-side, DOM-dependent logic in useMounted can cause hydration warnings or content flicker.
Fix: Guard browser-only APIs with typeof window !== 'undefined' or use useLayoutEffect equivalents provided by the runtime for synchronous DOM reads.
6. Misunderstanding Unmount Timing Boundaries
Explanation: useBeforeUnMount and useUnmounted execute at different phases. The former runs before React removes the node from the tree; the latter runs after. Confusing them breaks animation cleanup or analytics tracking.
Fix: Use useBeforeUnMount for interruptible tasks (canceling requests, stopping animations). Use useUnmounted for final resource release (closing sockets, clearing timers).
7. Relying on Compiler Output Without Verification
Explanation: AST analysis may miss conditional dependencies or complex closure scopes. Blindly trusting generated arrays can lead to stale data in update hooks.
Fix: Enable compiler verbose logging during migration. Run snapshot tests on compiled output and verify dependency arrays match actual variable usage.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Small Vue component library migrating to React | VuReact compiled adapters | Preserves lifecycle semantics, reduces rewrite time by ~60% | Low (tooling setup + verification) |
| Legacy Vue 2 app with heavy DOM manipulation | Manual React rewrite + useLayoutEffect | Vue 2 reactivity model differs significantly; compiler support limited | High (engineering hours, testing) |
| Real-time dashboard with WebSocket streams | VuReact + useReactive | Auto-dependency tracking prevents stale closure bugs in update phases | Medium (runtime bundle size + monitoring) |
| Team unfamiliar with React effects | VuReact adapters | Explicit hook names reduce cognitive load vs useEffect dependency arrays | Low (onboarding acceleration) |
| Server-rendered marketing site | Manual port with useEffect guards | Lifecycle hooks are minimal; compiler overhead outweighs benefits | Low (direct implementation) |
Configuration Template
// vite.config.ts
import { defineConfig } from 'vite';
import vueReact from '@vureact/compiler';
export default defineConfig({
plugins: [
vueReact({
// Enable AST-based dependency extraction
extractDependencies: true,
// Enforce idempotent mount behavior for StrictMode
strictModeCompatibility: true,
// Target React 18+ concurrent rendering
targetReactVersion: '18.2',
// Output compiled components to designated directory
outDir: './compiled-react',
// Preserve original Vue comments for traceability
preserveComments: true
})
],
build: {
rollupOptions: {
external: ['@vureact/runtime-core'],
output: {
globals: {
'@vureact/runtime-core': 'VuReactRuntime'
}
}
}
}
});
Quick Start Guide
- Install the compiler and runtime: Run
npm install @vureact/compiler @vureact/runtime-core --save-dev in your React project root.
- Add the plugin to your bundler: Import
@vureact/compiler in your Vite or Webpack configuration and enable dependency extraction.
- Place Vue source files: Drop your
<script setup> Vue components into the designated input directory. The compiler will intercept lifecycle imports automatically.
- Run the build: Execute
npm run build. The output directory will contain React-compatible components with useMounted, useBeforeUpdate, and useUnmounted adapters.
- Verify execution: Mount a compiled component in a React test harness. Check the console for lifecycle sequencing and confirm WebSocket/state synchronization behaves identically to the Vue original.