DEV Community

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

Posted on • Originally published at vureact.top

How does VuReact compile Vue 3's withDefaults 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 withDefaults 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 withDefaults.
  3. withDefaults() must be assigned to a variable.
  4. The first argument must be a defineProps() call, and the second argument must be an inline object literal.

Compilation Mapping

Vue withDefaults(defineProps<T>(), defaults)useMemo default value merge

withDefaults is a Vue 3 <script setup> utility for providing compile-time default values for props declared by defineProps. Vue's compiler generates default value logic to ensure props not passed by the parent component receive their default values. VuReact compiles this into useMemo, which merges the incoming props with the defaults during component initialization, producing a read-only props object that always contains the full set of default values.

  • Vue
<script setup lang="ts">
interface Props {
  msg?: string;
  count?: number;
  labels: string[];
}

const props = withDefaults(defineProps<Props>(), {
  msg: 'hello',
  count: 42,
  labels: () => ['one', 'two'],
});
</script>
Enter fullscreen mode Exit fullscreen mode
  • Compiled React
import { useMemo, memo } from 'react';

interface Props {
  msg?: string;
  count?: number;
  labels: string[];
}

export type ICompProps = Props;

const Input = memo((vrProps: ICompProps) => {
  /* from withDefaults */
  const props = useMemo<Readonly<Props>>(() => ({
    ...vrProps,
    msg: vrProps.msg ?? 'hello',
    count: vrProps.count ?? 42,
    labels: vrProps.labels ?? ['one', 'two'],
  }), [vrProps]);
});
Enter fullscreen mode Exit fullscreen mode

As the example shows, Vue's withDefaults is compiled into a combination of React's useMemo and the nullish coalescing operator ??. This can be broken down into three parts:

  1. Type preservation — The Props interface is kept as-is, and the optional/required constraints are not altered by the defaults. msg? and count? remain optional;
  2. Default value mergeuseMemo spreads ...vrProps to retain all passed values, then fills in defaults for each field using the ?? nullish coalescing operator;
  3. Read-only guaranteeuseMemo<Readonly<Props>> ensures the returned props object is read-only, consistent with the runtime immutability of Vue's withDefaults.

Core behavior: VuReact ensures that props.xxx accessed inside the component always returns the complete property set with defaults applied — exactly like Vue's development experience.


Primitive type defaults → ?? nullish coalescing

For primitive types such as string and number, VuReact directly uses the ?? nullish coalescing operator:

  • Vue
<script setup lang="ts">
const props = withDefaults(defineProps<Props>(), {
  msg: 'hello',
  count: 42,
});
</script>
Enter fullscreen mode Exit fullscreen mode
  • Compiled React
const props = useMemo(() => ({
  ...vrProps,
  msg: vrProps.msg ?? 'hello',
  count: vrProps.count ?? 42,
}), [vrProps]);
Enter fullscreen mode Exit fullscreen mode

Option mapping rules:

  1. default: 'hello' — implemented via the ?? nullish coalescing operator, only takes effect when the parent does not pass the prop (i.e., undefined);
  2. default: 42 — primitive default values are used directly as literal values on the right side of ??;
  3. Type preservation — the optionality of msg? and count? in Props is unaffected by the default values.

Reference type defaults → factory function invocation

For reference types such as arrays and objects, Vue's withDefaults requires factory functions (e.g., () => ['one', 'two']) to avoid multiple instances sharing the same reference. VuReact follows the same convention, invoking the factory function directly on the right side of ??, ensuring a new reference instance is created on every render:

  • Vue
<script setup lang="ts">
interface Props {
  labels: string[];
}

const props = withDefaults(defineProps<Props>(), {
  labels: () => ['one', 'two'],
});
</script>
Enter fullscreen mode Exit fullscreen mode
  • Compiled React
const props = useMemo<Readonly<Props>>(() => ({
  ...vrProps,
  labels: vrProps.labels ?? ['one', 'two'],
}), [vrProps]);
Enter fullscreen mode Exit fullscreen mode

VuReact guarantees that the factory function on the right side of ?? returns a new instance each time it is called, avoiding side-effect pollution caused by shared references.

Compilation rules:

  1. Factory function invocation — reference type defaults are called as factory functions to ensure independence;
  2. Array defaults['one', 'two'] creates a new array on every render;
  3. Object defaults — object literals follow the same pattern, preventing reference sharing across component instances.

Unsupported withDefaults patterns

VuReact explicitly does not support the following withDefaults usages and will report an error at compile time.

1. Not assigned to a variable

withDefaults() must be assigned to a variable (e.g., const props = withDefaults(...)); standalone expression calls are not supported:

<script setup lang="ts">
// Not supported
withDefaults(defineProps<Props>(), { msg: 'hello' });
</script>
Enter fullscreen mode Exit fullscreen mode
<script setup lang="ts">
// Supported form
const props = withDefaults(defineProps<Props>(), { msg: 'hello' });
</script>
Enter fullscreen mode Exit fullscreen mode

2. First argument is not a defineProps() call

The first argument of withDefaults() must be a defineProps() call expression; other expressions are not supported:

<script setup lang="ts">
// Not supported
const props = withDefaults({ msg: 'hello' });
</script>
Enter fullscreen mode Exit fullscreen mode
<script setup lang="ts">
// Supported form
const props = withDefaults(defineProps<Props>(), { msg: 'hello' });
</script>
Enter fullscreen mode Exit fullscreen mode

3. Second argument is not an object literal

The second argument of withDefaults() must be an inline object literal; variable references or other expressions are not supported:

<script setup lang="ts">
// Not supported
const defaults = { msg: 'hello' };
const props = withDefaults(defineProps<Props>(), defaults);
</script>
Enter fullscreen mode Exit fullscreen mode
<script setup lang="ts">
// Supported form
const props = withDefaults(defineProps<Props>(), { msg: 'hello' });
</script>
Enter fullscreen mode Exit fullscreen mode

Compilation strategy summary

VuReact's withDefaults compilation strategy demonstrates a complete default value conversion capability:

  1. Vue macro decomposition — breaks withDefaults into type preservation, default value merge, and read-only guarantee;
  2. Type safety — the Props interface is kept as-is, and optionality is not altered by defaults;
  3. Merge strategy — uses useMemo to merge the original props with defaults, ensuring performance optimization;
  4. Reference independence — reference type defaults use factory functions to guarantee independent instances on each render.

Core features:

  1. Automatic merge — combines ...vrProps with ?? nullish coalescing for default value merging;
  2. Read-only guaranteeuseMemo<Readonly<Props>> ensures props are immutable;
  3. Type preservation — the interface definition is kept as-is without affecting external type constraints;
  4. Performance optimizationuseMemo caches the merged result to reduce unnecessary computations.

Caveats:

  1. Must be assigned to a variablewithDefaults() must be used with const props = ...;
  2. Only supports defineProps() — the first argument must be a defineProps() call;
  3. Only supports object literals — the second argument must be an inline object literal;
  4. Reference types use factories — arrays, objects, and other reference types must use factory functions.

VuReact's compilation strategy ensures a smooth migration from Vue to React. Developers do not need to manually implement default value merging logic. The compiled code preserves Vue's withDefaults semantics and type inference capabilities while following React's useMemo performance optimization pattern, keeping the migrated application fully capable of handling default values.

Full example

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

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

interface Props {
  msg?: string;
  count?: number;
  labels: string[];
}

const props = withDefaults(defineProps<Props>(), {
  msg: 'hello',
  count: 42,
  labels: () => ['one', 'two'],
});
</script>

<template>
  <div>{{ props.msg }} {{ props.count }}</div>
  <ul>
    <li v-for="value in props.labels" :key="value">{{ value }}</li>
  </ul>
</template>
Enter fullscreen mode Exit fullscreen mode
  • Compiled React (output.tsx):
import { useMemo, memo } from 'react';

interface Props {
  msg?: string;
  count?: number;
  labels: string[];
}

export type ICompProps = Props;

const CompWithDefaults = memo((vrProps: ICompProps) => {
  /* from withDefaults */
  const props = useMemo<Readonly<Props>>(() => ({
    ...vrProps,
    msg: vrProps.msg ?? 'hello',
    count: vrProps.count ?? 42,
    labels: vrProps.labels ?? ['one', 'two'],
  }), [vrProps]);

  return (
    <>
      <div>{props.msg}{props.count}</div>
      <ul>
        {props.labels.map(value => <li key={value}>{value}</li>)}
      </ul>
    </>
  );
});

export default CompWithDefaults;
Enter fullscreen mode Exit fullscreen mode

Related Links

Top comments (0)