patible with Vue's getter/setter semantics.
reactive β useReactive: Wraps an object in a proxy-like structure that tracks nested mutations.
watch β useWatch: The core adapter. The compiler analyzes the watch source, determines if it's a getter, ref, or array, and injects the necessary tracking logic.
2. New Code Examples
The following examples demonstrate how VuReact compiles Vue 3 patterns into React. Note the preservation of callback signatures and options.
Scenario A: Async Side-Effect with Cleanup
In Vue, handling async operations within a watcher requires careful cleanup to avoid race conditions. VuReact preserves the onCleanup argument, allowing the callback to remain unchanged.
Vue 3 Source:
import { ref, watch } from 'vue';
const orderId = ref(100);
const orderDetails = ref(null);
watch(
orderId,
async (newId, oldId, onCleanup) => {
let isCancelled = false;
onCleanup(() => {
isCancelled = true;
});
try {
const result = await fetchOrderDetails(newId);
if (!isCancelled) {
orderDetails.value = result;
}
} catch (err) {
if (!isCancelled) {
console.error('Failed to load order:', err);
}
}
},
{ immediate: true }
);
Compiled React Output:
import { useVRef, useWatch } from '@vureact/runtime-core';
const orderId = useVRef(100);
const orderDetails = useVRef(null);
useWatch(
orderId,
async (newId, oldId, onCleanup) => {
let isCancelled = false;
onCleanup(() => {
isCancelled = true;
});
try {
const result = await fetchOrderDetails(newId);
if (!isCancelled) {
orderDetails.value = result;
}
} catch (err) {
if (!isCancelled) {
console.error('Failed to load order:', err);
}
}
},
{ immediate: true }
);
Architecture Decision:
VuReact maps onCleanup directly rather than converting it to a return function. This preserves the original callback structure, reducing the diff size during migration. The runtime adapter internally manages the cleanup lifecycle, ensuring that Vue's cleanup semantics are honored within React's effect execution model.
Scenario B: Deep Watching and Multi-Source Aggregation
Complex watchers often observe nested properties or aggregate multiple sources. VuReact handles these cases by generating precise dependency tracking without requiring manual array management.
Vue 3 Source:
import { reactive, watch } from 'vue';
const appConfig = reactive({
theme: { primaryColor: '#333', mode: 'dark' },
features: { notifications: true, analytics: false }
});
// Deep watch on nested object
watch(
() => appConfig.theme,
(newTheme) => {
applyTheme(newTheme);
},
{ deep: true }
);
// Multi-source watch
watch(
[appConfig.features.notifications, () => appConfig.theme.mode],
([notificationsEnabled, mode]) => {
updateNotificationSettings(notificationsEnabled, mode);
}
);
Compiled React Output:
import { useReactive, useWatch } from '@vureact/runtime-core';
const appConfig = useReactive({
theme: { primaryColor: '#333', mode: 'dark' },
features: { notifications: true, analytics: false }
});
useWatch(
() => appConfig.theme,
(newTheme) => {
applyTheme(newTheme);
},
{ deep: true }
);
useWatch(
[appConfig.features.notifications, () => appConfig.theme.mode],
([notificationsEnabled, mode]) => {
updateNotificationSettings(notificationsEnabled, mode);
}
);
Rationale:
- Deep Watching: The compiler detects the
{ deep: true } option and configures the runtime hook to perform deep equality checks or proxy-based tracking, depending on the source type. This avoids the boilerplate of manual deep comparisons in React.
- Multi-Source Arrays: VuReact preserves the array syntax. The compiler analyzes each element in the array to determine if it's a direct reference or a getter function. It generates a combined dependency list that triggers the callback only when relevant sources change, mimicking Vue's aggregation behavior.
Pitfall Guide
Even with automated compilation, certain patterns require attention to ensure production stability. Below are common pitfalls and their resolutions.
1. The Native Ref Trap
Explanation: Developers may accidentally use React's native useRef for values that need to be watched. Native refs do not trigger re-renders or notify watchers when their .current property changes.
Fix: Always use useVRef for any state that is passed to useWatch. The compiler will flag mismatches if the source is not a VuReact ref or reactive object.
2. Async Race Conditions Without Cleanup
Explanation: While useWatch supports onCleanup, developers migrating code might omit the cleanup logic if the original Vue code was simple. In React, rapid state changes can lead to multiple concurrent async calls.
Fix: Ensure all async watchers implement onCleanup to cancel pending requests or set a cancellation flag. This is critical for maintaining data integrity.
Explanation: Using { deep: true } on large objects can introduce performance overhead due to deep traversal during change detection.
Fix: Profile deep watches in production. If performance degrades, refactor the watcher to observe specific nested properties using getter functions (e.g., () => state.user.profile.settings) rather than the entire object.
4. Multi-Source Array Index Drift
Explanation: In multi-source watchers, the callback receives an array of new values. If the order of sources in the array changes, the callback arguments shift, potentially causing logic errors.
Fix: Destructure the callback arguments carefully and add comments documenting the expected order. Consider using named variables in the array to improve readability:
useWatch(
[notifications, themeMode],
([newNotifs, newMode]) => { /* ... */ }
);
Explanation: The { immediate: true } option causes the watcher to run during component initialization. If the side effect relies on DOM elements or external resources that aren't ready, it may fail.
Fix: Guard immediate watchers with checks for resource availability, or defer non-critical side effects to a separate useEffect that runs after mount.
6. Stale Closures in Getters
Explanation: When using getter functions in multi-source arrays (e.g., () => state.value), ensure the getter captures the latest state. If the state is wrapped in a ref, the getter must access the .value property.
Fix: Verify that getters correctly dereference refs. VuReact's useReactive objects handle this automatically, but mixed patterns require careful review.
7. Cleanup Function Side Effects
Explanation: The function passed to onCleanup should be free of side effects that impact application state. It is intended for resource disposal, not state updates.
Fix: Restrict cleanup functions to aborting requests, removing event listeners, or clearing timers. Avoid updating refs or triggering other watchers within the cleanup callback.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Migrating Legacy Vue Component | useWatch | Preserves existing logic; zero rewrite required. | Low dev cost; high migration speed. |
| New React Feature Development | useEffect | Native ecosystem; better tooling support for greenfield. | Standard React development cost. |
| Complex Multi-Source Logic | useWatch | Simplifies array aggregation; avoids manual dependency merging. | Low complexity; maintains readability. |
| High-Frequency Data Updates | useWatch with deep: false | Minimizes overhead; precise tracking reduces unnecessary renders. | Performance optimization required. |
| Integration with React Libraries | useWatch | Compatible with React ecosystem; hooks interop is seamless. | No additional integration cost. |
Configuration Template
To enable VuReact compilation, configure your bundler plugin. This example uses Vite, but the pattern applies to other build tools.
// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import vureact from '@vureact/vite-plugin';
export default defineConfig({
plugins: [
vureact({
// Optional: Enable verbose logging for compilation analysis
debug: false,
// Optional: Customize runtime import path
runtimeImport: '@vureact/runtime-core',
}),
react(),
],
resolve: {
alias: {
// Ensure Vue aliases are removed or redirected if necessary
vue: '@vureact/runtime-core',
},
},
});
Quick Start Guide
-
Install Dependencies:
npm install @vureact/runtime-core @vureact/vite-plugin
-
Add Plugin to Config:
Include the vureact plugin in your vite.config.ts as shown in the Configuration Template.
-
Write Vue Code:
Create a component using Vue 3 syntax with watch, ref, and reactive.
// MyComponent.vue
<script setup>
import { ref, watch } from 'vue';
const count = ref(0);
watch(count, (val) => console.log('Count changed:', val));
</script>
-
Build and Run:
Execute your build command. The compiler will transform the Vue code into React hooks.
npm run dev
-
Verify Output:
Inspect the compiled component. You should see useVRef and useWatch imports from @vureact/runtime-core, with the original logic preserved.
// Compiled MyComponent.tsx
import { useVRef, useWatch } from '@vureact/runtime-core';
const count = useVRef(0);
useWatch(count, (val) => console.log('Count changed:', val));
By following this workflow, you can leverage VuReact to bridge Vue 3's reactivity model to React, ensuring that side-effect logic remains robust, maintainable, and performant throughout the migration process.