ensures that migration does not introduce subtle reconciliation regressions.
Core Solution
The transpilation pipeline operates in three distinct phases: static literal detection, reactive dependency graph construction, and conditional hook injection. Each phase targets a specific category of top-level declarations found in Vue 3 <script setup> blocks.
Phase 1: Primitive Hoisting
When the compiler encounters a top-level const initialized with a primitive literal (string, number, boolean, null, undefined), it lifts the declaration outside the generated React component function. This prevents the JavaScript engine from allocating a new binding on every render cycle.
Vue Source:
<script setup lang="ts">
const apiEndpoint = '/v2/analytics';
const maxRetries = 3;
const debugMode = false;
</script>
Compiled React Output:
const apiEndpoint = '/v2/analytics';
const maxRetries = 3;
const debugMode = false;
export const AnalyticsPanel = memo(() => {
return <div>Dashboard Content</div>;
});
Why this works: React re-executes component functions during reconciliation. Hoisting primitives removes them from the execution scope entirely, eliminating allocation overhead and ensuring consistent memory references across renders.
Phase 2: Reactive Dependency Graph Construction
For top-level variables that derive values from reactive sources (ref, reactive, computed), the compiler performs control-flow analysis to map every reactive read. It constructs a dependency graph by tracking property access paths and proxy triggers.
Vue Source:
<script setup lang="ts">
const sessionToken = ref('');
const uiState = reactive({ theme: 'dark', sidebarOpen: true });
const derivedConfig = {
authHeader: `Bearer ${sessionToken.value}`,
toggleSidebar: () => {
uiState.sidebarOpen = !uiState.sidebarOpen;
},
};
const pureConstants = {
version: '2.1.0',
fallbackTheme: 'light',
};
Compiled React Output:
const sessionToken = useVRef('');
const uiState = useReactive({ theme: 'dark', sidebarOpen: true });
const derivedConfig = useMemo(
() => ({
authHeader: `Bearer ${sessionToken.value}`,
toggleSidebar: () => {
uiState.sidebarOpen = !uiState.sidebarOpen;
},
}),
[sessionToken.value, uiState.sidebarOpen],
);
const pureConstants = {
version: '2.1.0',
fallbackTheme: 'light',
};
Why this works: The compiler identifies that derivedConfig reads sessionToken.value and mutates uiState.sidebarOpen. It automatically generates a useMemo wrapper with a precise dependency array. pureConstants contains no reactive reads, so it remains a plain object. This prevents unnecessary hook invocations while guaranteeing that derived state updates when its dependencies change.
Phase 3: Runtime Proxy Mapping
Vue's reactivity relies on Proxy-based tracking. React lacks native proxy support in its rendering cycle, so the compiler maps Vue's ref and reactive to runtime equivalents (useVRef, useReactive). These helpers maintain Vue's getter/setter semantics while integrating with React's render queue.
Architecture Rationale:
- Hoisting over inline initialization: Reduces GC pressure and prevents referential inequality in
React.memo or shouldComponentUpdate checks.
- Automatic dependency arrays: Eliminates stale closure bugs caused by manual
useMemo maintenance.
- Selective memoization: Avoids hook overhead for static data, preserving React's lightweight render path.
- Runtime abstraction: Decouples Vue's reactivity model from React's lifecycle, enabling semantic parity without rewriting business logic.
Pitfall Guide
1. Over-Memoization of Static Literals
Explanation: Wrapping primitive values or pure objects in useMemo adds unnecessary hook execution overhead and increases bundle size.
Fix: Rely on the compiler's static analysis. Only wrap expressions that read from reactive proxies or perform expensive computations.
2. Stale Closure Capture in Memoized Callbacks
Explanation: Functions inside memoized objects may capture outdated state if the dependency array omits a reactive read.
Fix: Ensure every reactive property accessed inside a callback is included in the dependency array. The compiler handles this automatically, but manual overrides require strict auditing.
3. Nested Reactive Mutations Bypassing React's Queue
Explanation: Directly mutating nested reactive properties inside memoized callbacks can trigger Vue's proxy system without notifying React's render cycle.
Fix: Use explicit state setters or trigger re-renders via useVRef updates. Avoid deep mutations in memoized contexts.
4. Circular Dependency Graphs
Explanation: Top-level variables referencing each other during initialization can break the compiler's dependency resolution, causing infinite loops or missing updates.
Fix: Flatten declarations, use lazy initialization (() => value), or split interdependent logic into separate hooks.
5. Runtime Helper Import Mismatch
Explanation: Forgetting to import useVRef or useReactive from the official runtime package results in undefined errors at runtime.
Fix: Configure bundler aliases or use the compiler's auto-import feature. Verify runtime package version matches the compiler version.
6. Ignoring Event Handler Stability
Explanation: Memoizing objects containing inline functions still causes child re-renders if the function reference changes on every render.
Fix: Let the compiler wrap stable callbacks in useCallback, or explicitly mark them as constant outside the component scope.
7. SSR Hydration Mismatches
Explanation: Hoisted constants that rely on client-only APIs (e.g., window, localStorage) cause hydration warnings when rendered on the server.
Fix: Guard client-specific logic with environment checks (typeof window !== 'undefined') or defer initialization to useEffect.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Small utility module with static config | Compiler hoisting only | Zero runtime overhead, minimal bundle size | Low |
| Complex reactive dashboard with derived state | Automated useMemo generation | Guarantees referential stability, reduces manual dependency tracking | Medium |
| Legacy Vue migration with tight deadlines | Full VuReact transpilation pipeline | Eliminates syntax translation friction, preserves reactivity semantics | High initial, low long-term |
| Performance-critical list rendering | Manual useCallback + compiler memoization | Fine-grained control over child re-renders, optimal reconciliation | Medium |
Configuration Template
// vureact.config.ts
import { defineConfig } from '@vureact/compiler';
export default defineConfig({
compiler: {
hoistPrimitives: true,
autoMemoizeReactive: true,
dependencyAnalysis: 'strict',
runtimeHelpers: {
ref: 'useVRef',
reactive: 'useReactive',
},
},
output: {
format: 'esm',
target: 'react-18',
memoizationStrategy: 'automatic',
},
ssr: {
hydrateGuard: true,
clientOnlyCheck: 'typeof window !== undefined',
},
});
Quick Start Guide
- Install the compiler and runtime packages:
npm install @vureact/compiler @vureact/runtime
- Create
vureact.config.ts in your project root using the template above
- Run the transpilation command:
npx vureact compile ./src/vue --out ./src/react
- Verify output with React DevTools: confirm hoisted constants and
useMemo wrappers
- Replace Vue imports with compiled React components and run integration tests