rcepting useAttrs() calls and replacing them with direct props references. The compiler analyzes the surrounding context to determine whether TypeScript types are present, whether explicit interfaces exist, and whether the file is typed or untyped. Each scenario triggers a specific transformation rule.
Step 1: Intercept and Replace the API Call
The compiler scans for useAttrs() invocations within <script setup> blocks. When found, it replaces the call with a reference to the component's props parameter. If the component does not already declare props, the compiler injects a parameter with a fallback type.
Vue Input:
<script setup lang="ts">
import { useAttrs } from 'vue';
const layoutAttrs = useAttrs();
</script>
Compiled React Output:
import type { ReactNode } from 'react';
interface LayoutProps {
children?: ReactNode;
}
export function LayoutContainer(props: Record<string, unknown>) {
const layoutAttrs = props as Record<string, unknown>;
// ...
}
Rationale: React components receive all incoming data through the first parameter. Mapping useAttrs() directly to props aligns with React's execution model. The Record<string, unknown> fallback ensures type safety when no explicit interface is provided, preventing any leakage while maintaining flexibility.
Step 2: Preserve and Augment TypeScript Interfaces
When explicit type definitions accompany useAttrs(), the compiler preserves them and applies type assertions to the props reference. This prevents type erosion during migration and allows developers to continue using structured attribute shapes.
Vue Input:
<script setup lang="ts">
interface CardMeta {
variant?: 'primary' | 'secondary';
elevation?: number;
[key: string]: unknown;
}
const { variant, elevation } = useAttrs() as CardMeta;
const fullMeta: CardMeta = useAttrs();
</script>
Compiled React Output:
interface CardMeta {
variant?: 'primary' | 'secondary';
elevation?: number;
[key: string]: unknown;
}
export function InfoCard(props: Record<string, unknown>) {
const { variant, elevation } = props as CardMeta;
const fullMeta = props as CardMeta;
// ...
}
Rationale: Vue's as casting and explicit type annotations are semantically equivalent to React's type assertions. The compiler retains the interface definition and applies it to the props reference. This approach avoids generating new type files and keeps the migration self-contained within the component scope.
Step 3: Handle Plain JavaScript Environments
In untyped codebases, the compiler strips type assertions entirely. The transformation becomes a direct variable assignment, reducing cognitive load and preventing unnecessary type noise.
Vue Input:
<script setup>
const themeAttrs = useAttrs();
</script>
Compiled React Output:
export function ThemeWrapper(props) {
const themeAttrs = props;
// ...
}
Rationale: JavaScript projects prioritize runtime simplicity over static analysis. Removing type assertions keeps the compiled output clean and aligns with standard React patterns. The compiler detects the absence of lang="ts" or lang="typescript" and switches to a lightweight transformation mode.
Architecture Decisions
The compiler avoids generating proxy wrappers or reactive attribute objects because React's reconciliation model does not require them. Vue's useAttrs() returns a reactive proxy to support template compilation and automatic updates. React handles updates through explicit state changes and prop reference comparisons. By mapping directly to props, the compiler eliminates unnecessary abstraction layers and ensures the migrated component behaves identically to a native React implementation.
Type preservation is handled through AST node cloning rather than string replacement. This guarantees that interface declarations, generic constraints, and mapped types are correctly positioned in the output file. The compiler also validates that props is declared in the function signature before injecting type assertions, preventing reference errors in edge cases.
Pitfall Guide
1. Blind Casting to Record<string, unknown>
Explanation: Developers often replace useAttrs() with props as Record<string, unknown> without considering existing type definitions. This discards explicit interfaces and forces downstream code to use index signatures.
Fix: Preserve original interfaces when present. Use the compiler's type-aware transformation to maintain structural typing. Only fall back to Record<string, unknown> when no explicit shape is defined.
2. Ignoring React's Prop Spreading Behavior
Explanation: Vue automatically merges class and style attributes from parent to child. React requires explicit spreading or utility functions like clsx or tailwind-merge. Migrated components may lose styling inheritance if developers assume automatic merging.
Fix: Audit class and style handling post-migration. Replace implicit Vue merging with explicit React prop spreading or a dedicated class merging utility.
3. Type Inference Breakage in Generic Components
Explanation: When useAttrs() is used inside generic Vue components, the compiler may fail to propagate generic constraints to the React props parameter, resulting in lost type safety.
Fix: Ensure the compiler extracts generic parameters from the Vue signature and applies them to the React function declaration. Example: export function DataPanel<T extends Record<string, unknown>>(props: T)
4. Assuming React Props Are Reactive Proxies
Explanation: Vue's useAttrs() returns a reactive object that updates automatically when parent attributes change. React props are plain objects that trigger re-renders only when the parent passes a new reference.
Fix: Refactor any code relying on proxy reactivity to use React's state or effect hooks. Treat props as immutable snapshots per render cycle.
5. Mixing Declared Props with Fallthrough Attributes
Explanation: In Vue, defineProps and useAttrs() are strictly separated. In React, both live in the same props object. Developers may accidentally destructure declared props alongside fallthrough attributes, causing naming collisions or unexpected overrides.
Fix: Explicitly separate concerns during migration. Destructure declared props first, then assign remaining attributes to a dedicated variable. Example: const { id, title, ...restAttrs } = props;
6. Overlooking Event Handler Naming Conventions
Explanation: Vue uses kebab-case for event listeners (@click, @update:modelValue), while React expects camelCase (onClick, onUpdateModelValue). The compiler maps useAttrs() to props, but event handlers may retain Vue naming if not normalized.
Fix: Run a secondary AST pass that converts Vue event keys to React conventions before attribute assignment. Ensure onX patterns follow React's synthetic event system.
7. Failing to Handle v-bind Propagation
Explanation: Vue's v-bind="$attrs" forwards all fallthrough attributes. In React, this translates to {...props}. Developers sometimes manually spread individual keys, breaking the fallthrough contract.
Fix: Replace v-bind="$attrs" with {...props} in JSX. Verify that no declared props are accidentally overridden by the spread operation.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Large Vue codebase with strict TS | Compiler-assisted migration with type preservation | Maintains type safety, reduces manual refactoring time | Low upfront, high long-term ROI |
| Small utility components | Manual refactor with explicit prop typing | Simpler than configuring compiler for isolated files | Medium upfront, low maintenance |
| Legacy JS codebase | Compiler mapping with untyped fallback | Avoids type noise, aligns with existing patterns | Low upfront, moderate runtime debugging |
Components with heavy v-bind forwarding | Compiler + explicit spread normalization | Preserves fallthrough behavior without proxy overhead | Low upfront, high reliability |
| Mixed framework team | Standardize on React props model post-migration | Reduces cognitive load, enforces consistent patterns | Medium training cost, long-term clarity |
Configuration Template
// vureact.config.ts
import { defineConfig } from '@vureact/core';
export default defineConfig({
compiler: {
target: 'react',
version: '18.x',
strictTypes: true,
preserveInterfaces: true,
normalizeEvents: true,
fallbackPropsType: 'Record<string, unknown>'
},
transform: {
useAttrs: {
strategy: 'direct-reference',
typeAssertion: 'preserve-or-infer',
jsMode: 'untyped-reference'
},
vBind: {
strategy: 'spread-normalization',
excludeDeclared: true
}
},
output: {
format: 'tsx',
importHelpers: false,
stripVueImports: true
}
});
Quick Start Guide
- Install the compiler package and initialize configuration:
npm install @vureact/core && npx vureact init
- Place your Vue 3 components in a designated source directory (e.g.,
src/vue-components/)
- Run the transformation:
npx vureact transform ./src/vue-components --out ./src/react-components
- Verify the output: Check that
useAttrs() calls are replaced with props references and type assertions are preserved
- Integrate into your build pipeline: Add the transform command to your pre-build script or CI workflow for continuous migration