ence errors in React.
Core Solution
VuReact's compilation of useTemplateRef involves three distinct transformation phases: API substitution, access pattern rewriting, and template binding conversion. The following implementation details demonstrate how the compiler achieves this mapping.
Step 1: API Substitution and Type Preservation
The compiler identifies imports of useTemplateRef from Vue and replaces them with useRef from React. Crucially, it extracts the generic type parameter and the string identifier, mapping them to React's ref initialization.
Vue Source:
<script setup lang="ts">
import { useTemplateRef } from 'vue';
// Vue declaration with generic type and string identifier
const videoPlayer = useTemplateRef<HTMLVideoElement>('videoPlayer');
</script>
Compiled React Output:
import { useRef } from 'react';
// React declaration with preserved generic type and null initialization
const videoPlayer = useRef<HTMLVideoElement | null>(null);
Rationale: useRef is the closest semantic equivalent in React for holding mutable DOM references. The compiler preserves the generic type HTMLVideoElement to ensure that TypeScript continues to provide accurate autocompletion and type checking. The string identifier 'videoPlayer' is discarded in React, as refs are bound directly to variables, but the variable name is retained for consistency.
Step 2: Access Pattern Rewriting
Vue's reactivity system allows direct access to the DOM node via .value. React requires accessing .current. The compiler performs a static analysis of all ref usages and rewrites the property access.
Vue Source:
<script setup lang="ts">
import { onMounted, useTemplateRef } from 'vue';
const videoPlayer = useTemplateRef<HTMLVideoElement>('videoPlayer');
onMounted(() => {
// Vue access pattern
if (videoPlayer.value) {
videoPlayer.value.play();
console.log(videoPlayer.value.duration);
}
});
</script>
Compiled React Output:
import { useRef } from 'react';
import { useMounted } from '@vureact/runtime-core';
const videoPlayer = useRef<HTMLVideoElement | null>(null);
useMounted(() => {
// React access pattern with optional chaining
if (videoPlayer.current) {
videoPlayer.current.play();
console.log(videoPlayer.current.duration);
}
});
Rationale: The compiler rewrites .value to .current. It also introduces optional chaining (?.) where appropriate to handle the nullable nature of React refs. The onMounted hook is mapped to useMounted, a runtime helper provided by @vureact/runtime-core. This helper abstracts React's useEffect to match Vue's lifecycle semantics, ensuring the callback executes after the DOM node is available.
Step 3: Template Binding Conversion
In Vue templates, refs are bound using a string attribute. In React JSX, refs are bound using an object reference. The compiler transforms the template syntax accordingly.
Vue Source:
<template>
<video ref="videoPlayer" src="/assets/intro.mp4" />
</template>
Compiled React Output:
return (
<video ref={videoPlayer} src="/assets/intro.mp4" />
);
Rationale: The compiler replaces the string-based ref attribute with a JSX expression binding the ref variable. This maintains the connection between the template element and the ref object, allowing React to populate the .current property upon rendering.
Pitfall Guide
Migrating refs requires attention to execution timing, null safety, and type constraints. The following pitfalls are common during manual migration and are mitigated by VuReact's compilation strategy.
-
Premature DOM Access
- Explanation: Attempting to access a ref before the component mounts results in
null. In Vue, onMounted guarantees the DOM is ready. In React, accessing .current during render or in effects without proper dependencies can yield stale or null values.
- Fix: Use
useMounted (or useEffect with empty dependencies) to defer access until after the initial render. VuReact automatically maps onMounted to useMounted.
-
Missing Null Checks
- Explanation: React refs are nullable (
T | null). Accessing properties on .current without a null check can cause runtime errors.
- Fix: Always use optional chaining (
?.) or explicit null checks before accessing ref properties. The compiler enforces this pattern in the output.
-
Type Drift in Generics
- Explanation: Manually rewriting refs often leads to losing generic type parameters, resulting in
any types and loss of IDE support.
- Fix: Ensure the ref declaration includes the correct generic type (e.g.,
useRef<HTMLDivElement | null>(null)). VuReact preserves generics from the original useTemplateRef call.
-
Ref Name Mismatch
- Explanation: In Vue, the string in
ref="name" must match the identifier in useTemplateRef('name'). Manual migration can introduce typos, breaking the binding.
- Fix: Rely on the compiler to map the string identifier to the variable name automatically. If editing manually, verify that the JSX
ref attribute matches the variable name exactly.
-
Overusing Refs for State
- Explanation: Refs are intended for DOM access or mutable values that don't trigger re-renders. Using refs to store UI state leads to stale UI and breaks React's reactivity model.
- Fix: Use
useState for reactive state. Refs should only be used for DOM nodes or values that need to persist across renders without causing updates.
-
Stale Closures in Effects
- Explanation: Accessing refs inside effects without including them in the dependency array can lead to stale references if the ref changes.
- Fix: Include the ref in the dependency array if the effect depends on it. However, since refs are mutable, React's linter may warn about this. Use
useMounted for one-time setup to avoid dependency issues.
-
Ignoring Runtime Dependencies
- Explanation: The compiled output relies on
@vureact/runtime-core for helpers like useMounted. Forgetting to install this package causes import errors.
- Fix: Ensure
@vureact/runtime-core is added to the project dependencies. The compiler generates imports for this package automatically.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Large Vue 3 Codebase | Use VuReact Compiler | Automates ref migration, preserves types, reduces manual errors. | Lowers migration cost significantly; requires initial setup. |
| Small Component Migration | Manual Rewrite | Simpler for isolated components; avoids adding compiler dependencies. | Higher per-component effort; risk of type drift. |
| Strict Type Safety Required | VuReact with Type Preservation | Ensures generics are retained; prevents runtime type errors. | Minimal overhead; improves long-term maintainability. |
| Legacy React Integration | Manual Ref Binding | Better control over React-specific patterns; avoids runtime helpers. | Higher development time; requires deep React expertise. |
Configuration Template
The following template demonstrates a standard VuReact configuration for a Vite-based project. This setup ensures that Vue 3 files are compiled to React with proper ref handling.
// vite.config.ts
import { defineConfig } from 'vite';
import vureact from '@vureact/compiler/vite';
export default defineConfig({
plugins: [
vureact({
// Enable semantic compilation for useTemplateRef
semanticMapping: {
useTemplateRef: true,
preserveTypes: true,
},
// Runtime configuration
runtime: {
core: '@vureact/runtime-core',
helpers: ['useMounted'],
},
}),
],
// TypeScript configuration to support compiled output
esbuild: {
jsx: 'automatic',
},
});
Quick Start Guide
-
Initialize Project: Create a new React project or navigate to an existing one.
npm create vite@latest my-react-app -- --template react-ts
cd my-react-app
-
Install VuReact: Add the compiler and runtime packages.
npm install @vureact/compiler @vureact/runtime-core
-
Configure Build: Update vite.config.ts with the VuReact plugin as shown in the Configuration Template.
-
Add Vue Source: Place Vue 3 components with useTemplateRef in the src directory. The compiler will automatically process them during the build.
-
Verify Output: Run the development server and inspect the compiled React components to ensure refs are correctly mapped and types are preserved.
npm run dev