DEV Community

Cover image for How does VuReact compile Vue 3's defineModel to React?
Ryan John
Ryan John

Posted on • Originally published at vureact.top

How does VuReact compile Vue 3's defineModel to React?

VuReact is a compiler toolchain for migrating from Vue to React — and for writing React with Vue 3 syntax.

In this article, we will look at how Vue 3's defineModel macro is compiled into React.

Before We Start

To keep the examples easy to read, this article follows a few simple conventions:

  1. All Vue and React snippets focus on core logic only, with full component wrappers and unrelated configuration omitted.
  2. The discussion assumes you are already familiar with the API shape and core behavior of Vue 3's defineModel.
  3. VuReact only supports the type, default, required options and a custom prop name.
  4. Array destructuring of the return value is not supported.

Compilation Mapping

Vue defineModeluseVRef + useUpdated for auto-sync

defineModel is a Vue 3 <script setup> macro that simplifies v-model two-way binding declarations. Under the hood, Vue's compiler generates a ref along with a modelValue prop and an update:modelValue event. VuReact compiles this into useVRef (which turns a prop value into a reactive ref) paired with useUpdated (which triggers the parent's onUpdate:xxx callback when the value changes).

  • Vue
// Declares a "state" prop consumed by the parent via v-model:state
const state = defineModel<string>('state');

// Declares a "modelValue" prop (default: 'xxx') consumed via v-model
const modelValue = defineModel({ default: 'xxx' });
Enter fullscreen mode Exit fullscreen mode
  • Compiled React
import { useVRef, useUpdated } from '@vureact/runtime-core';

type IChildProps = {
  state?: string;
  modelValue?: string;
} & {
  onUpdateState?: (arg: string) => void;
  onUpdateModelValue?: (arg: string) => void;
};

// "state" prop consumed by parent via onUpdateState
const state = useVRef<string>(props.state);

// "modelValue" prop (default: 'xxx') consumed via onUpdateModelValue
const modelValue = useVRef<string>(props.modelValue ?? 'xxx');

// Automatically notifies parent on value change
useUpdated(() => {
  props.onUpdateState?.(state.value);
}, [state.value]);

useUpdated(() => {
  props.onUpdateModelValue?.(modelValue.value);
}, [modelValue.value]);
Enter fullscreen mode Exit fullscreen mode

As the example shows, Vue's defineModel is decomposed into three parts:

  1. Prop type declaration — non-event fields in IChildProps (state?, modelValue?);
  2. Event callback declarationonUpdateXxx fields in IChildProps;
  3. Runtime reactivityuseVRef wraps the initial prop value into a reactive ref, and useUpdated watches the ref and automatically invokes the parent-provided callback.

With VuReact, directly mutating the ref's .value inside the component triggers the parent's update — exactly like Vue's development experience.


defineModel(name, options) → typed prop declaration with default

defineModel accepts a name to specify the prop name and limited options (type, default, required). VuReact converts these options into corresponding TypeScript type constraints and default-value handling.

  • Vue
const count = defineModel<number>('count', {
  type: Number,
  default: 0,
  required: true,
});
Enter fullscreen mode Exit fullscreen mode
  • Compiled React
type IChildProps = {
  count: number;  // required: true → non-optional type
} & {
  onUpdateCount?: (arg: number) => void;
};

const count = useVRef<number>(props.count ?? 0); // default: 0
Enter fullscreen mode Exit fullscreen mode

Option mapping rules:

  1. required: true makes count required (count: number) instead of optional (count?: number).
  2. default: 0 is implemented via the ?? nullish coalescing operator, falling back to 0 when the parent does not pass the prop.
  3. type: Number influences the generic type parameter (<number>) in the generated TypeScript definition.

Direct .value assignment → auto-sync with parent

In Vue, defineModel returns a ref; you read and write its .value. VuReact preserves this .value access pattern in the compiled React code.

  • Vue
const state = defineModel<string>('state');

const update = () => {
  state.value = 'hello'; // direct assignment
};
Enter fullscreen mode Exit fullscreen mode
  • Compiled React
const state = useVRef<string>(props.state);

const update = useCallback(() => {
  state.value = 'hello'; // triggers props.onUpdateState automatically
}, [state.value]);
Enter fullscreen mode Exit fullscreen mode

Compilation rules:

  1. useCallback wrapping — functions that touch state.value are wrapped with useCallback.
  2. Correct dependency trackingstate.value is added to the dependency array.
  3. Parent syncstate.value = 'hello' simultaneously updates the component's local state and pushes the new value to the parent.

v-model in templates → React controlled components

Vue templates use v-model to bind a defineModel ref; VuReact compiles this into the standard React controlled-component pattern (value + onChange).

  • Vue
<template>
  <input v-model="modelValue" />
  <div>Parent bound v-model is: {{ count }}</div>
  <button @click="update">Increment</button>
</template>
Enter fullscreen mode Exit fullscreen mode
  • Compiled React
<input
  value={modelValue}
  onChange={(e) => {
    modelValue = e.target.value;
  }}
/>
<div>Parent bound v-model is:{count.value}</div>
<button onClick={update}>Increment</button>
Enter fullscreen mode Exit fullscreen mode

v-model conversion rules:

  1. value binding — the ref's value is bound to the value attribute.
  2. onChange handler — the ref is reassigned directly in the onChange callback.
  3. Automatic sync — the reassignment triggers useUpdated, which pushes the new value to the parent.

Unsupported defineModel patterns

VuReact explicitly does not support the following defineModel usages and will skip them with an error.

1. Array destructuring of the return value

<script setup lang="ts">
// Not supported (Vue 3.4+ experimental feature)
const [arg1, arg2] = defineModel();
</script>
Enter fullscreen mode Exit fullscreen mode

Vue 3.4+ supports destructuring the return value into [model, modifiers] to access modifier status. VuReact does not support this syntax yet. Use the standard form instead:

<script setup lang="ts">
// Supported form
const model = defineModel();
</script>
Enter fullscreen mode Exit fullscreen mode

2. get / set / validator options

<script setup lang="ts">
// Not supported
const modelValue = defineModel({
  get() {},
  set() {},
  validator() {},
});
</script>
Enter fullscreen mode Exit fullscreen mode

Vue's defineModel supports custom get/set accessors and a validator function. VuReact does not support these options yet. Use useVRef directly to implement custom logic if needed.


Full example

Below is a complete single-file component using defineModel and its compiled React output.

  • Vue (input.vue):
<script setup lang="ts">
// @vr-name: Child

const state = defineModel<string>('state');
const modelValue = defineModel({ default: 'xxx' });
const count = defineModel<number>('count', {
  type: Number,
  default: 0,
  required: true,
});

const update = () => {
  state.value = 'hello';
  count.value++;
};
</script>

<template>
  <input v-model="modelValue" />
  <div>Parent bound v-model is: {{ count }}</div>
  <button @click="update">Increment</button>
</template>
Enter fullscreen mode Exit fullscreen mode
  • Compiled React (output.tsx):
import { useCallback, memo } from 'react';
import { useVRef, useUpdated } from '@vureact/runtime-core';

export type IChildProps = {
  state?: string;
  modelValue?: string;
  count: number;
} & {
  onUpdateState?: (arg: string) => void;
  onUpdateModelValue?: (arg: string) => void;
  onUpdateCount?: (arg: number) => void;
};

const Child = memo((props: IChildProps) => {
  const state = useVRef<string>(props.state);
  const modelValue = useVRef<string>(props.modelValue ?? 'xxx');
  const count = useVRef<number>(props.count ?? 0);

  const update = useCallback(() => {
    state.value = 'hello';
    count.value++;
  }, [state.value, count.value]);

  useUpdated(() => {
    props.onUpdateState?.(state.value);
  }, [state.value]);

  useUpdated(() => {
    props.onUpdateModelValue?.(modelValue.value);
  }, [modelValue.value]);

  useUpdated(() => {
    props.onUpdateCount?.(count.value);
  }, [count.value]);

  return (
    <>
      <input
        value={modelValue}
        onChange={(e) => {
          modelValue = e.target.value;
        }}
      />
      <div>Parent bound v-model is:{count.value}</div>
      <button onClick={update}>Increment</button>
    </>
  );
});

export default Child;
Enter fullscreen mode Exit fullscreen mode

Related Links

Top comments (0)