apping, and type alignment. Teams that adopt this strategy can migrate thousands of components without rewriting rendering logic from scratch, while maintaining strict XSS defenses and layout consistency across the entire application.
Core Solution
The translation of Vue’s content directives into React’s JSX ecosystem relies on a structured compilation pipeline. Rather than treating template conversion as a text transformation, a robust toolchain parses the source template into an Abstract Syntax Tree (AST), identifies directive nodes, applies semantic mapping rules, and generates type-safe React code. Below is the step-by-step technical implementation.
Step 1: AST Parsing & Directive Identification
The compiler first parses the Vue Single File Component (SFC) template. It traverses the DOM node tree and flags elements containing v-html or v-text attributes. Each flagged node is isolated for transformation, preserving surrounding markup and event bindings.
Step 2: Semantic Mapping & Security Wrapping
Once identified, the compiler applies framework-specific mapping rules:
v-html maps to React’s dangerouslySetInnerHTML property. The compiler wraps the bound expression in an object literal { __html: expression } and injects a compile-time sanitization hook if a security policy is configured.
v-text maps to JSX interpolation {expression}. The compiler strips the directive attribute and replaces the element’s children with the interpolated value, relying on React’s built-in HTML escaping.
Step 3: JSX Generation & Type Alignment
The transformed AST is serialized into TypeScript React (TSX) code. Variable names are normalized to follow React conventions, and TypeScript interfaces are generated to reflect the expected data types for dynamic content.
New Code Examples
Vue Source Template
<template>
<section class="content-wrapper">
<article v-html="rawMarkup"></article>
<aside v-text="plainPayload"></aside>
</section>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const rawMarkup = ref('<strong>System Alert:</strong> Maintenance scheduled.')
const plainPayload = ref('User session expires in 15 minutes.')
</script>
Compiled React TSX Output
import React from 'react';
interface ContentSectionProps {
rawMarkup: string;
plainPayload: string;
}
export const ContentSection: React.FC<ContentSectionProps> = ({ rawMarkup, plainPayload }) => {
return (
<section className="content-wrapper">
<article dangerouslySetInnerHTML={{ __html: rawMarkup }} />
<aside>{plainPayload}</aside>
</section>
);
};
Architecture Decisions & Rationale
Why AST-based transformation over regex?
Regular expressions cannot reliably parse nested templates, handle conditional rendering (v-if/v-for), or preserve attribute bindings. AST parsing guarantees structural integrity, allowing the compiler to safely traverse, modify, and regenerate nodes without corrupting the component hierarchy.
Why map v-html to dangerouslySetInnerHTML?
React deliberately names this property to signal security risk. The compiler adopts this exact mapping to preserve Vue’s HTML parsing semantics while forcing developers to acknowledge the XSS implications. This alignment ensures that migrated components behave identically to their Vue counterparts, but within React’s explicit security model.
Why map v-text to JSX interpolation?
Vue’s v-text replaces inner content with plain text and automatically escapes HTML entities. React’s JSX interpolation {value} performs identical escaping at runtime. Mapping directly to interpolation eliminates unnecessary wrapper functions and leverages React’s native rendering pipeline for optimal performance.
Why inject sanitization hooks at compile time?
Manual sanitization is error-prone and inconsistent across teams. By injecting a configurable sanitization step during compilation, the toolchain ensures that all v-html bindings pass through a standardized filter (e.g., DOMPurify or a custom policy) before reaching the DOM. This shifts security from a runtime concern to a build-time guarantee.
Pitfall Guide
Migrating content rendering directives introduces subtle but critical failure points. Below are the most common mistakes observed in production migrations, along with proven fixes.
1. Unsanitized HTML Injection
Explanation: Developers map v-html directly to dangerouslySetInnerHTML without applying input sanitization. If the bound string contains malicious scripts, they execute immediately upon rendering.
Fix: Configure the compiler to wrap all v-html bindings with a sanitization utility. Implement a strict allowlist policy for tags and attributes. Validate third-party content sources before compilation.
2. Whitespace & Newline Collapse
Explanation: Vue’s template compiler normalizes whitespace differently than React’s JSX parser. Manual conversions often result in collapsed spaces or unexpected line breaks, breaking UI layouts.
Fix: Preserve whitespace semantics by mapping v-text to {String(value).trim()} when layout consistency is critical. Use CSS white-space: pre-wrap on container elements to maintain formatting across frameworks.
3. Hydration Mismatch on Server-Side Rendering
Explanation: When migrating SSR applications, dangerouslySetInnerHTML can cause hydration mismatches if the server and client generate different HTML strings due to timing or environment differences.
Fix: Ensure deterministic content generation on both server and client. Use suppressHydrationWarning only as a last resort for intentionally dynamic content. Validate that sanitization pipelines produce identical output across environments.
4. Over-Escaping Plain Text
Explanation: Developers sometimes wrap v-text equivalents in additional escaping functions, double-encoding HTML entities and rendering visible & or < strings in the UI.
Fix: Rely exclusively on React’s built-in JSX interpolation for plain text. Remove redundant encodeURIComponent or escapeHtml calls. Trust the framework’s native escaping mechanism.
5. Event Delegation Loss on Injected Nodes
Explanation: HTML injected via dangerouslySetInnerHTML bypasses React’s synthetic event system. Click handlers or focus events attached to injected elements will not trigger React’s event delegation.
Fix: Attach event listeners to the parent container and use event delegation. Alternatively, parse the injected HTML string, extract interactive elements, and render them as native React components instead of raw HTML.
6. TypeScript Type Drift
Explanation: Migrated components often lose type safety when dynamic content bindings are converted to any or string without proper interface definitions.
Fix: Generate explicit TypeScript interfaces for all dynamic props. Use React.ReactNode for complex content and string for plain text. Enforce strict type checking in the CI pipeline to catch drift early.
7. Ignoring Framework-Specific Rendering Optimizations
Explanation: Vue’s reactivity system automatically tracks directive dependencies. React requires explicit memoization to prevent unnecessary re-renders when content strings change frequently.
Fix: Wrap dynamic content components with React.memo when props are stable. Use useMemo for expensive sanitization operations. Profile render cycles to identify unnecessary DOM updates.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
Legacy Vue app with heavy v-html usage | Compiler-driven translation + DOMPurify pipeline | Preserves semantics while enforcing security boundaries | Low (automated) |
| New React feature requiring dynamic HTML | Manual JSX with explicit sanitization | Full control over security policy and component architecture | Medium (development time) |
| Marketing site with static HTML snippets | Pre-rendered components or CMS integration | Eliminates runtime injection risks entirely | Low (infrastructure) |
| Enterprise app with strict compliance requirements | Custom AST transformer + policy engine | Guarantees auditability and consistent enforcement | High (initial setup) |
Configuration Template
// vureact.config.ts
import { defineConfig } from 'vureact/compiler';
export default defineConfig({
input: './src/views/**/*.vue',
output: './src/migrated/**/*.tsx',
directives: {
vHtml: {
strategy: 'dangerouslySetInnerHTML',
sanitization: {
enabled: true,
policy: 'strict',
allowedTags: ['strong', 'em', 'br', 'p', 'ul', 'li', 'a'],
allowedAttrs: ['href', 'target', 'rel'],
},
},
vText: {
strategy: 'jsxInterpolation',
whitespace: 'preserve',
trim: false,
},
},
typescript: {
generateInterfaces: true,
strictNullChecks: true,
propNaming: 'camelCase',
},
ssr: {
hydrationValidation: true,
deterministicOutput: true,
},
});
Quick Start Guide
- Install the toolchain: Run
npm install vureact @vureact/compiler in your project root.
- Initialize configuration: Execute
npx vureact init to generate a baseline vureact.config.ts file.
- Run the migration: Execute
npx vureact migrate --watch to transform Vue SFCs into React TSX components in real-time.
- Validate output: Review the generated TSX files, run TypeScript compilation checks, and execute component tests to verify rendering parity.
- Integrate into CI: Add the migration command to your pipeline with
--dry-run and --strict flags to prevent unvalidated directive conversions from reaching production.