tifies all reactive property accesses, and generates a dependency array. Reactive references are converted to useVRef(), which provides the .value accessor pattern required by the compiled code.
Vue Source:
import { ref, watchEffect } from 'vue';
const theme = ref('dark');
const isLoaded = ref(false);
watchEffect(() => {
if (isLoaded.value) {
document.documentElement.setAttribute('data-theme', theme.value);
}
});
Compiled React Output:
import { useVRef, useWatchEffect } from '@vureact/runtime-core';
const theme = useVRef('dark');
const isLoaded = useVRef(false);
useWatchEffect(() => {
if (isLoaded.value) {
document.documentElement.setAttribute('data-theme', theme.value);
}
}, [isLoaded.value, theme.value]);
Architecture Rationale:
useVRef vs. useState: Using useVRef preserves the .value syntax, making the compiled code visually consistent with the source. This reduces cognitive friction during code reviews and debugging.
- Static Dependency Extraction: The compiler analyzes the AST of the callback to find all reactive reads. In the example,
isLoaded.value and theme.value are detected. The generated array [isLoaded.value, theme.value] ensures the effect re-runs whenever either value changes, matching Vue's behavior.
- Optional Chaining in Deps: If the source uses optional chaining (e.g.,
obj?.prop), the compiler includes the optional access in the dependency array to capture changes safely.
2. Flush Mode Mapping
Vue's flush option controls when the effect runs relative to DOM updates. The compiler maps these modes to distinct React hooks to preserve timing semantics.
Vue Source:
import { ref, watchEffect } from 'vue';
const container = ref(null);
const dimensions = ref({ width: 0, height: 0 });
// Post-flush: Runs after DOM updates
watchEffect(() => {
if (container.value) {
dimensions.value = container.value.getBoundingClientRect();
}
}, { flush: 'post' });
// Sync-flush: Runs synchronously
watchEffect(() => {
console.log('Sync check:', container.value);
}, { flush: 'sync' });
Compiled React Output:
import { useVRef, useWatchPostEffect, useWatchSyncEffect } from '@vureact/runtime-core';
const container = useVRef(null);
const dimensions = useVRef({ width: 0, height: 0 });
useWatchPostEffect(() => {
if (container.value) {
dimensions.value = container.value.getBoundingClientRect();
}
}, [container.value, container.value?.getBoundingClientRect]);
useWatchSyncEffect(() => {
console.log('Sync check:', container.value);
}, [container.value]);
Architecture Rationale:
useWatchPostEffect: Maps to flush: 'post'. This hook typically wraps useLayoutEffect or a similar mechanism to ensure the effect runs after DOM mutations, preventing layout thrashing and ensuring accurate measurements.
useWatchSyncEffect: Maps to flush: 'sync'. This hook executes synchronously during render, matching Vue's synchronous flush behavior. It is critical for logic that must run immediately upon reactive changes without waiting for the commit phase.
- Dependency Granularity: The compiler captures method calls like
getBoundingClientRect in the dependency array if they are accessed, ensuring the effect responds to changes in the element reference.
Pitfall Guide
Migrating reactive logic requires attention to semantic details. The following pitfalls highlight common issues and their resolutions.
-
Missing Runtime Imports
- Explanation: The compiled code relies on
@vureact/runtime-core. If this package is not installed or imports are stripped, the application will crash at runtime.
- Fix: Ensure
@vureact/runtime-core is listed in dependencies and that the build pipeline does not tree-shake these imports aggressively. Verify imports in the compiled output.
-
Cleanup Function Semantics
- Explanation: Vue's
watchEffect allows returning a cleanup function that runs before the next execution. React's useEffect cleanup runs on unmount or dependency change. If the runtime adapter does not bridge this correctly, cleanup logic may execute at the wrong time or not at all.
- Fix: Use the provided runtime hooks which handle cleanup mapping. Do not manually rewrite cleanup logic unless necessary. Test cleanup behavior in scenarios involving rapid state changes and component unmounting.
-
Conditional Tracking Gaps
- Explanation: Vue tracks dependencies dynamically; if a branch is not taken, the dependency is not registered. Static analysis may over-approximate dependencies, including reads that are conditionally guarded. This can cause effects to run more frequently than in Vue.
- Fix: Accept that over-declaration is safer than under-declaration. If performance is impacted, refactor the source code to reduce conditional complexity or use manual dependency arrays in the React output for critical paths.
-
Flush Mode Drift
- Explanation:
flush: 'sync' in Vue is truly synchronous. In React, achieving synchronous execution requires specific hook implementations. If the runtime uses useEffect for sync effects, timing will drift, potentially causing visual glitches or state inconsistencies.
- Fix: Verify that
useWatchSyncEffect uses a synchronous mechanism like useLayoutEffect or direct execution. Audit the runtime source code to confirm timing guarantees.
-
Dynamic Property Access
- Explanation: Accessing properties via variables (e.g.,
state[key]) can be difficult for static analysis to track accurately. The compiler may miss these reads or generate overly broad dependencies.
- Fix: For dynamic access patterns, consider using explicit dependency arrays in the compiled output or refactoring the source to use static property access where possible.
-
Ref Object Mutability
- Explanation:
useVRef returns a reactive object. Direct mutation of the ref object itself (rather than .value) can break reactivity tracking.
- Fix: Always mutate
.value. Avoid reassigning the ref variable. Ensure ESLint rules or linters catch direct ref mutations.
-
Bundle Size Impact
- Explanation: Importing the runtime adapter adds to the bundle size. While minimal, this can be a concern for performance-critical applications.
- Fix: Use tree-shaking to include only the hooks used. The runtime is designed to be modular; unused hooks like
useWatchPostEffect can be excluded if not present in the codebase.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| DOM Measurement | useWatchPostEffect | Ensures effect runs after DOM updates, providing accurate dimensions. | Low |
| Synchronous State Sync | useWatchSyncEffect | Matches Vue's sync flush, critical for immediate state consistency. | Low |
| General Side Effects | useWatchEffect | Standard mapping for auto-tracking with minimal overhead. | Low |
| Complex Conditional Logic | Manual Dependency Array | Static analysis may over-declare; manual control optimizes performance. | Medium |
| Dynamic Property Access | Refactor Source or Manual Deps | Improves tracking accuracy and reduces runtime overhead. | Medium |
Configuration Template
Use this configuration to enable the compiler plugin in a Vite-based project. Adjust paths and options as needed.
// vite.config.ts
import { defineConfig } from 'vite';
import vueReact from '@vureact/compiler';
export default defineConfig({
plugins: [
vueReact({
// Enable static analysis for dependency generation
analyzeDeps: true,
// Map flush modes to specific hooks
flushMapping: {
post: 'useWatchPostEffect',
sync: 'useWatchSyncEffect',
},
// Configure ref transformation
refTransform: {
import: 'useVRef',
source: '@vureact/runtime-core',
},
}),
],
});
Quick Start Guide
-
Install Dependencies:
npm install @vureact/compiler @vureact/runtime-core
-
Configure Build Tool:
Add the VueReact compiler plugin to your build configuration (e.g., vite.config.ts or webpack.config.js). Ensure the plugin processes Vue SFCs or JSX files as required.
-
Run Compilation:
Execute your build command. The compiler will transform Vue components, replacing watchEffect with the appropriate React hooks and generating dependency arrays.
-
Verify Output:
Inspect the compiled React code. Check that imports are correct, dependencies are generated, and flush modes are mapped. Run tests to ensure behavioral parity.
-
Deploy:
Once verified, deploy the migrated codebase. Monitor performance and error rates to ensure the migration maintains stability.