e. The compiler does not simply rename functions; it transforms the underlying state management strategy to align with React's hook lifecycle while preserving Vue's mutation semantics.
Step-by-Step Implementation
- Initialize the compilation pipeline: Configure the build tool to process
.vue or .ts files containing Vue Composition API imports.
- Define state with Vue primitives: Write components using
reactive() or shallowReactive() as you normally would in a Vue codebase.
- Compile to React: Run the VuReact compiler to generate equivalent React components with
useReactive() and useShallowReactive() hooks.
- Validate runtime behavior: Ensure the compiled hooks trigger React updates correctly and maintain TypeScript type safety.
New Code Examples
The following examples demonstrate how VuReact transforms Vue state declarations into React hooks. Notice the different domain models, interface structures, and variable naming conventions.
Vue Source
<script lang="ts" setup>
import { reactive, shallowReactive } from 'vue';
interface DeploymentConfig {
environment: string;
replicas: number;
features: Record<string, boolean>;
}
interface CacheLayer {
ttl: number;
strategy: 'lru' | 'fifo';
metadata: { lastSync: string; version: string };
}
const workspace = reactive<DeploymentConfig>({
environment: 'staging',
replicas: 3,
features: { darkMode: true, notifications: false },
});
const sessionCache = shallowReactive<CacheLayer>({
ttl: 3600,
strategy: 'lru',
metadata: { lastSync: new Date().toISOString(), version: '2.1.0' },
});
</script>
Compiled React Output
import { useReactive, useShallowReactive } from '@vureact/runtime-core';
interface DeploymentConfig {
environment: string;
replicas: number;
features: Record<string, boolean>;
}
interface CacheLayer {
ttl: number;
strategy: 'lru' | 'fifo';
metadata: { lastSync: string; version: string };
}
const workspace = useReactive<DeploymentConfig>({
environment: 'staging',
replicas: 3,
features: { darkMode: true, notifications: false },
});
const sessionCache = useShallowReactive<CacheLayer>({
ttl: 3600,
strategy: 'lru',
metadata: { lastSync: new Date().toISOString(), version: '2.1.0' },
});
Architecture Decisions and Rationale
VuReact's runtime hooks are built on a Proxy subscription model that integrates with React's rendering cycle without breaking its core principles. Here's why the architecture is structured this way:
- Proxy Interception Layer: The hooks wrap the initial state object in a JavaScript
Proxy. Property reads register dependencies, and property writes trigger a scheduled React update. This mirrors Vue's reactivity system but routes updates through React's useSyncExternalStore or a controlled forceUpdate mechanism.
- TypeScript Preservation: The compiler extracts generic type parameters and interface definitions directly from the Vue AST. Since React hooks accept generic arguments, the type information flows through unchanged. This eliminates the need for manual type casting or wrapper interfaces.
- Shallow vs Deep Tracking:
useReactive() recursively proxies nested objects and arrays, enabling deep mutation tracking. useShallowReactive() only proxies the top-level object, leaving nested structures as plain references. This distinction is critical for performance: deep proxying adds overhead proportional to object depth, while shallow tracking minimizes memory allocation and dependency graph complexity.
- React Lifecycle Compatibility: Unlike Vue's synchronous update batching, React schedules updates asynchronously. The runtime hooks queue state changes and flush them during React's render phase, preventing infinite update loops and ensuring compatibility with concurrent features like Suspense and transitions.
Pitfall Guide
Migrating reactivity patterns requires awareness of framework-specific behaviors. The following pitfalls are commonly encountered when working with compiled Vue-to-React state hooks.
1. Expecting Deep Updates in Shallow Reactive Objects
Explanation: Developers often mutate nested properties inside useShallowReactive() expecting automatic re-renders. Shallow tracking only observes top-level reference changes.
Fix: Replace the entire object when modifying nested data, or switch to useReactive() if deep tracking is required.
// β Incorrect: nested mutation won't trigger update
sessionCache.metadata.lastSync = new Date().toISOString();
// β
Correct: replace top-level reference
sessionCache.metadata = { ...sessionCache.metadata, lastSync: new Date().toISOString() };
2. Destructuring Reactive State Before Rendering
Explanation: Extracting properties from a reactive object breaks the Proxy chain. The destructured values become plain primitives or references, losing automatic update subscriptions.
Fix: Access properties directly through the reactive object, or use computed selectors that maintain dependency tracking.
// β Breaks reactivity
const { environment, replicas } = workspace;
// β
Maintains reactivity
const env = workspace.environment;
const rep = workspace.replicas;
3. Mixing Vue ref and reactive Patterns Incorrectly
Explanation: Vue's ref() wraps primitives in an object with a .value property. The compiler maps this to a different hook (useRefState or similar). Confusing the two leads to type mismatches and undefined behavior.
Fix: Use reactive() for objects/arrays and ref() for primitives. Verify the compiled hook matches the source primitive type.
4. Assuming React useEffect Cleanup Works Identically
Explanation: Vue's watch and watchEffect automatically clean up dependencies. React's useEffect requires explicit cleanup functions. Compiled code may not automatically translate Vue watchers to React effects.
Fix: Manually implement cleanup logic in useEffect or use VuReact's compiled watcher equivalents that handle dependency disposal.
5. Overusing Deep Reactivity for Large Static Datasets
Explanation: Proxying thousands of nested objects consumes significant memory and CPU cycles during property access. This is unnecessary for read-only or infrequently updated data.
Fix: Use shallowReactive() for large datasets, or freeze immutable structures with Object.freeze() before passing them to the hook.
6. Ignoring Generic Constraints During Compilation
Explanation: TypeScript interfaces with complex generics or conditional types may not map cleanly if the compiler encounters unsupported utility types.
Fix: Simplify generic constraints to basic interfaces or Record types. Validate compiled output with tsc --noEmit to catch type drift early.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Small form state with frequent nested edits | useReactive() | Deep tracking simplifies mutation logic and reduces boilerplate | Low CPU, moderate memory |
| Large configuration objects or API payloads | useShallowReactive() | Shallow tracking minimizes proxy overhead and dependency graph size | Low memory, high performance |
| Cross-framework logic sharing | VuReact compiled hooks | Maintains identical state semantics across Vue and React codebases | Zero rewrite cost, consistent DX |
| Performance-critical list rendering | Manual useState + immutable updates | Bypasses proxy interception for maximum render control | Higher developer overhead, optimal runtime speed |
| Third-party library integration | useShallowReactive() | Prevents accidental proxying of external objects that may break library internals | Safe interop, minimal risk |
Configuration Template
Use this configuration to initialize the compilation pipeline and runtime dependencies.
// package.json dependencies
{
"dependencies": {
"@vureact/runtime-core": "^1.4.0",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@vureact/compiler": "^1.4.0",
"typescript": "^5.3.0"
}
}
// vite.config.ts
import { defineConfig } from 'vite';
import vureact from '@vureact/compiler/vite';
export default defineConfig({
plugins: [
vureact({
target: 'react',
preserveTypes: true,
shallowOptimization: true,
}),
],
build: {
rollupOptions: {
external: ['react', 'react-dom'],
},
},
});
// tsconfig.json
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"jsx": "react-jsx",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"paths": {
"@vureact/runtime-core": ["./node_modules/@vureact/runtime-core/dist/index.d.ts"]
}
},
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"]
}
Quick Start Guide
- Install dependencies: Run
npm install @vureact/compiler @vureact/runtime-core react react-dom to add the compiler and runtime packages.
- Configure the build tool: Add the VuReact plugin to your Vite, Webpack, or Rollup configuration as shown in the template above.
- Write Vue state logic: Create a
.vue or .ts file using reactive() or shallowReactive() with your preferred TypeScript interfaces.
- Compile and verify: Execute the build command. The compiler will generate equivalent React components with
useReactive() and useShallowReactive() hooks. Run tsc --noEmit to confirm type safety.
- Integrate into React app: Import the compiled components into your React project. The hooks will automatically synchronize with React's rendering cycle while preserving Vue's mutation ergonomics.