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:
- All Vue and React snippets focus on core logic only, with full component wrappers and unrelated configuration omitted.
- The discussion assumes you are already familiar with the API shape and core behavior of Vue 3's
withDefaults. -
withDefaults()must be assigned to a variable. - 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>
- 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]);
});
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:
-
Type preservation — The
Propsinterface is kept as-is, and the optional/required constraints are not altered by the defaults.msg?andcount?remain optional; -
Default value merge —
useMemospreads...vrPropsto retain all passed values, then fills in defaults for each field using the??nullish coalescing operator; -
Read-only guarantee —
useMemo<Readonly<Props>>ensures the returned props object is read-only, consistent with the runtime immutability of Vue'swithDefaults.
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>
- Compiled React
const props = useMemo(() => ({
...vrProps,
msg: vrProps.msg ?? 'hello',
count: vrProps.count ?? 42,
}), [vrProps]);
Option mapping rules:
-
default: 'hello'— implemented via the??nullish coalescing operator, only takes effect when the parent does not pass the prop (i.e.,undefined); -
default: 42— primitive default values are used directly as literal values on the right side of??; -
Type preservation — the optionality of
msg?andcount?inPropsis 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>
- Compiled React
const props = useMemo<Readonly<Props>>(() => ({
...vrProps,
labels: vrProps.labels ?? ['one', 'two'],
}), [vrProps]);
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:
- Factory function invocation — reference type defaults are called as factory functions to ensure independence;
-
Array defaults —
['one', 'two']creates a new array on every render; - 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>
<script setup lang="ts">
// Supported form
const props = withDefaults(defineProps<Props>(), { msg: 'hello' });
</script>
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>
<script setup lang="ts">
// Supported form
const props = withDefaults(defineProps<Props>(), { msg: 'hello' });
</script>
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>
<script setup lang="ts">
// Supported form
const props = withDefaults(defineProps<Props>(), { msg: 'hello' });
</script>
Compilation strategy summary
VuReact's withDefaults compilation strategy demonstrates a complete default value conversion capability:
-
Vue macro decomposition — breaks
withDefaultsinto type preservation, default value merge, and read-only guarantee; -
Type safety — the
Propsinterface is kept as-is, and optionality is not altered by defaults; -
Merge strategy — uses
useMemoto merge the original props with defaults, ensuring performance optimization; - Reference independence — reference type defaults use factory functions to guarantee independent instances on each render.
Core features:
-
Automatic merge — combines
...vrPropswith??nullish coalescing for default value merging; -
Read-only guarantee —
useMemo<Readonly<Props>>ensures props are immutable; - Type preservation — the interface definition is kept as-is without affecting external type constraints;
-
Performance optimization —
useMemocaches the merged result to reduce unnecessary computations.
Caveats:
-
Must be assigned to a variable —
withDefaults()must be used withconst props = ...; -
Only supports
defineProps()— the first argument must be adefineProps()call; - Only supports object literals — the second argument must be an inline object literal;
- 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>
- 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;
Top comments (0)