Technologies supporting Reactive Props Destructure
※ This blog has been translated into English by ChatGPT from the original article at https://zenn.dev/comm_vue_nuxt/articles/reactive-props-destructure.
Vue 3.5.0 has been released
Recently, Vue 3.5.0 (Tengen Toppa Gurren Lagann) has been released.
This release includes various changes such as optimizations in the Reactivity System, new composable functions useTemplateRef
and useId
, and improvements to Custom Elements. For more details, please refer to the official blog post above or the summary article from the same publication here.
Topics of the Release
In Vue 3.5, the feature Reactive Props Destructure has become stable. This article will explain the overview, background, implementation, and technologies supporting Reactive Props Destructure.
What is Reactive Props Destructure? (Review)
Reactive Props Destructure is a feature that maintains reactivity when destructuring props defined with defineProps. This allows for improvements in developer experience (DX), particularly making it concise to set default values without using withDefault
.
const { count = 0, msg = "hello" } = defineProps<{
count?: number;
message?: string;
}>();
// Changes in count trigger properly
const double = computed(() => count * 2);
::::details Previous Way of Writing
const props = withDefaults(
defineProps<{
count?: number;
msg?: string;
}>(),
{
count: 0,
msg: "hello",
}
);
const double = computed(() => props.count * 2);
::::
Implementation Background of Reactive Props Destructure
About RFC
Reactive Props Destructure originated from an RFC.
Though, this was proposed by Evan You, the creator of Vue.js, and it is derivative of another RFC called Reactivity Transform that Evan You had previously proposed.
Reactivity Transform was proposed as a compiler implementation to improve developer experience (DX) related to reactivity, and it included features such as (a few examples):
- Omission of
.value
with$ref
- Conversion of existing reactive variables with
$
- props destructure
- Maintenance of reactivity across boundaries with
$$
Yes, props destructure was one of these features proposed as part of Reactivity Transform.
Discontinuation of Reactivity Transform
Reactivity Transform was being implemented as experimental, but eventually it was discontinued. The reasons for discontinuation are summarized in the comments of the same RFC as follows:
In a nutshell, the reasons for discontinuation were:
- Difficult distinction between reactive and non-reactive variables when
.value
is omitted - Cost of context shift between different mental models
- External functions expected to work with ref still need
.value
, adding mental burden
Although Reactivity Transform has been "continuously available in the Vue Macros library", it was deprecated in 3.3 and completely removed in 3.4 from vuejs/core.
RFC for Reactive Props Destructure
Initially proposed as part of Reactivity Transform, Reactive Props Destructure later became its own independent RFC and was proposed in 2023/4.
This was part of the Reactivity Transform proposal and now split into a separate proposal of its own.
As mentioned in the RFC, the motivations were mainly:
- Concise syntax for default values and aliases
- Consistency with implicit props access in templates
Further details will be discussed later, but the compiled output is generally described in the RFC as follows:
Compilation Rules
The compilation logic is straightforward - the above example is compiled like the following:
Input
const { count } = defineProps(["count"]); watchEffect(() => { console.log(count); });
Output
const __props = defineProps(["count"]); watchEffect(() => { console.log(__props.count); });
As a user, you will handle destructured properties when writing source code, but the compiler will convert the code to access these variables by tracing them from the conventional props
object, maintaining reactivity.
The drawbacks of this feature are also documented in the same comment. Some of the points summarized are:
- There is a risk of accidentally passing destructured properties to a function, causing reactivity loss.
- It is not explicitly clear that they are props (making it harder to distinguish from other variables).
- Confusion for beginners due to compiler magic.
For more detailed information, please refer to the RFC, where significant drawbacks like those mentioned above are listed, along with approaches to dealing with these drawbacks.
Regarding the approach to dealing with these drawbacks, it roughly states, "Well, wasn't it already like this before? Is it really a big problem?"
How does Reactive Props Destructure work
Since only a part of the information is written in the RFC, let's take a closer look at how it actually works. While we will delve into the implementation details later, it is important to note that Reactive Props Destructure is an implementation of the compiler. For a deeper understanding of what the compiler is, please refer to this article from the same publication.
When we say "observe the operation," we are actually referring to "observing how it gets compiled."
※ Regarding all output code:
- The code has been formatted.
- The compiler is set to Development Mode.
In Production Mode, some options related to props (validator, required) are removed, making it difficult to understand. Therefore, we are looking at the output in Development Mode. - The compiler version used is Vue 3.5.0. :::
Basic Operation (Before Reactive Props Destructure)
First, let's look at the basic operation when not using Props Destructure. Let's also observe how the defined props are used in the template.
Input
<script setup lang="ts">
import { computed } from "vue";
const props = defineProps<{ count: number }>();
const double = computed(() => props.count * 2);
</script>
<template>
<div>{{ props.count }}{{ count }}{{ double }}</div>
</template>
Output
/* Analyzed bindings: {
"computed": "setup-const",
"props": "setup-reactive-const",
"double": "setup-ref",
"count": "props"
} */
import { defineComponent as _defineComponent } from "vue";
import { computed } from "vue";
const __sfc__ = /*#__PURE__*/ _defineComponent({
__name: "App",
props: {
count: { type: Number, required: true },
},
setup(__props, { expose: __expose }) {
__expose();
const props = __props;
const double = computed(() => props.count * 2);
const __returned__ = { props, double };
Object.defineProperty(__returned__, "__isScriptSetup", {
enumerable: false,
value: true,
});
return __returned__;
},
});
import {
toDisplayString as _toDisplayString,
openBlock as _openBlock,
createElementBlock as _createElementBlock,
} from "vue";
function render(_ctx, _cache, $props, $setup, $data, $options) {
return (
_openBlock(),
_createElementBlock(
"div",
null,
_toDisplayString($setup.props.count) +
_toDisplayString($props.count) +
_toDisplayString($setup.double),
1 /* TEXT */
)
);
}
__sfc__.render = render;
__sfc__.__file = "src/App.vue";
export default __sfc__;
Naming of props Object
Firstly, the props
received as the first argument in the setup function is fixed to the name __props
, and the user-defined name for the props object binds to this.
For example, when writing const props = defineProps();
, it becomes const props = __props;
.
Props Definition
In Vue.js, props are defined in the component's props option. In this case, it is the part where:
const __sfc__ = /*#__PURE__*/ _defineComponent({
props: {
count: { type: Number, required: true },
},
});
The part of the Input saying defineProps<{ count: number }>()
is converted by the compiler to props: { count: { type: Number, required: true } }
.
If you were to write defineProps<{ count?: string }>()
, it would become props: { count: { type: String, required: false } }
.
Also, it's worth noting that the Vue.js compiler determines whether a prop is required based on optional parameter syntax, not on the type information specified. Therefore, even if you write defineProps<{ count: string | undefined }>()
, the required
field will not be set to false
.
References
Next, let's look at the references in the template. In this example's Input:
<template>
<div>{{ props.count }}{{ count }}{{ double }}</div>
</template>
props
, count
, and double
are referenced in the template.
In Vue.js, variables defined as props can be directly referenced in the template (like {{ count }}
), and variables named as props objects in the script setup can also be referenced (like {{ props.count }}
).
Looking at the compiled result, there is metadata at the top:
/* Analyzed bindings: {
"computed": "setup-const",
"props": "setup-reactive-const",
"double": "setup-ref",
"count": "props"
} */
Focusing on props
, double
, and count
, they are each bound to setup-reactive-const
, setup-ref
, and props
.
This metadata helps resolve the references of variables in the template.
The actual resolution can be seen in:
function render(_ctx, _cache, $props, $setup, $data, $options) {
return (
_openBlock(),
_createElementBlock(
"div",
null,
_toDisplayString($setup.props.count) +
_toDisplayString($props.count) +
_toDisplayString($setup.double),
1 /* TEXT */
)
);
}
In the template, variables with setup-xxx
bindings are compiled to be referenced from $setup
, while props are referenced from $props
.
Basic Operation of Reactive Props Destructure
Now, let's take a look at the operation of Reactive Props Destructure, which is the main topic of this discussion.
First, let's start with a simple example.
Input
<script setup lang="ts">
import { computed } from "vue";
const { count } = defineProps<{ count: number }>();
const double = computed(() => count * 2);
</script>
<template>
<div>{{ count }}{{ double }}</div>
</template>
Output
/* Analyzed bindings: {
"computed": "setup-const",
"double": "setup-ref",
"count": "props"
} */
import { defineComponent as _defineComponent } from "vue";
import { computed } from "vue";
const __sfc__ = /*#__PURE__*/ _defineComponent({
__name: "App",
props: {
count: { type: Number, required: true },
},
setup(__props, { expose: __expose }) {
__expose();
const double = computed(() => __props.count * 2);
const __returned__ = { double };
Object.defineProperty(__returned__, "__isScriptSetup", {
enumerable: false,
value: true,
});
return __returned__;
},
});
import {
toDisplayString as _toDisplayString,
openBlock as _openBlock,
createElementBlock as _createElementBlock,
} from "vue";
function render(_ctx, _cache, $props, $setup, $data, $options) {
return (
_openBlock(),
_createElementBlock(
"div",
null,
_toDisplayString($props.count) + _toDisplayString($setup.double),
1 /* TEXT */
)
);
}
__sfc__.render = render;
__sfc__.__file = "src/App.vue";
export default __sfc__;
Since we are destructuring, we have removed {{ props.count }}
from the previous template as there is no variable named props
.
One thing to note is the part of the compiled code:
const double = computed(() => __props.count * 2);
This is mentioned in the RFC as well.
Although the user's code is const double = computed(() => count * 2);
, the metadata reveals that count
is a props
, so it is compiled as __props.count
.
This behavior is similar to how count
is used in the template.
The part of the RFC mentioning "consistency with implicit props access in the template" can also be observed.
It seems that the scoping is properly managed:
import { computed } from "vue";
const { count } = defineProps<{ count: number }>();
const double = computed(() => count * 2);
{
const count = 1;
console.log(count);
}
If written like this, the generated code will be:
const double = computed(() => __props.count * 2);
{
const count = 1;
console.log(count);
}
Setting Default Values
Next is setting default values. You can probably already guess the output code.
Input
<script setup lang="ts">
import { computed } from "vue";
const { count = 0 } = defineProps<{ count?: number }>();
const double = computed(() => count * 2);
</script>
<template>
<div>{{ count }}{{ double }}</div>
</template>
Output
/* Analyzed bindings: {
"computed": "setup-const",
"double": "setup-ref",
"count": "props"
} */
import { defineComponent as _defineComponent } from "vue";
import { computed } from "vue";
const __sfc__ = /*#__PURE__*/ _defineComponent({
__name: "App",
props: {
count: { type: Number, required: false, default: 0 },
},
setup(__props, { expose: __expose }) {
__expose();
const double = computed(() => __props.count * 2);
const __returned__ = { double };
Object.defineProperty(__returned__, "__isScriptSetup", {
enumerable: false,
value: true,
});
return __returned__;
},
});
import {
toDisplayString as _toDisplayString,
openBlock as _openBlock,
createElementBlock as _createElementBlock,
} from "vue";
function render(_ctx, _cache, $props, $setup, $data, $options) {
return (
_openBlock(),
_createElementBlock(
"div",
null,
_toDisplayString($props.count) + _toDisplayString($setup.double),
1 /* TEXT */
)
);
}
__sfc__.render = render;
__sfc__.__file = "src/App.vue";
export default __sfc__;
We were able to observe count: { type: Number, required: false, default: 0 },
.
Props Alias
In Reactive Props Destructure, similar to regular JavaScript destructuring, you can assign aliases to variable names. Let's see how it compiles, except for the actual output code.
Input
<script setup lang="ts">
import { computed } from "vue";
const { count: renamedPropsCount } = defineProps<{ count: number }>();
const double = computed(() => renamedPropsCount * 2);
</script>
<template>
<div>{{ count }}{{ renamedPropsCount }}{{ double }}</div>
</template>
Output
/* Analyzed bindings: {
"renamedPropsCount": "props-aliased",
"__propsAliases": {
"renamedPropsCount": "count"
},
"computed": "setup-const",
"double": "setup-ref",
"count": "props"
} */
import { defineComponent as _defineComponent } from "vue";
import { computed } from "vue";
const __sfc__ = /*#__PURE__*/ _defineComponent({
__name: "App",
props: {
count: { type: Number, required: true },
},
setup(__props, { expose: __expose }) {
__expose();
const double = computed(() => __props.count * 2);
const __returned__ = { double };
Object.defineProperty(__returned__, "__isScriptSetup", {
enumerable: false,
value: true,
});
return __returned__;
},
});
import {
toDisplayString as _toDisplayString,
openBlock as _openBlock,
createElementBlock as _createElementBlock,
} from "vue";
function render(_ctx, _cache, $props, $setup, $data, $options) {
return (
_openBlock(),
_createElementBlock(
"div",
null,
_toDisplayString($props.count) +
_toDisplayString($props["count"]) +
_toDisplayString($setup.double),
1 /* TEXT */
)
);
}
__sfc__.render = render;
__sfc__.__file = "src/App.vue";
export default __sfc__;
The important part is the analysis at the top:
/* Analyzed bindings: {
"renamedPropsCount": "props-aliased",
"__propsAliases": {
"renamedPropsCount": "count"
},
"computed": "setup-const",
"double": "setup-ref",
"count": "props"
} */
It includes the information about the alias.
The __propsAliases
contains the mapping of aliases to original variable names, and renamedPropsCount
is also analyzed as props-aliased
.
Based on this information, the compiler compiles renamedPropsCount
as __props.count
.
const double = computed(() => __props.count * 2);
_toDisplayString($props.count) +
_toDisplayString($props["count"]) +
_toDisplayString($setup.double);
How is Reactive Props Destructure implemented
So far, we have seen how the code written by the user for Reactive Props Destructure is converted into runtime code. Now, let's delve into how these are actually implemented by looking at the source code of vuejs/core.
All the source code and links mentioned from this point onwards are based on v3.5.0.
Background Information
As a refresher, vuejs/core is a repository containing the implementation of Vue.js (v3.0 and later), with several packages under the packages
directory. We will be focusing on the compiler-sfc
package in this case, where the compiler for Single File Components is implemented.
https://github.com/vuejs/core/tree/6402b984087dd48f1a11f444a225d4ac6b2b7b9e/packages/compiler-sfc
In most cases, it is used by bundler tools' plugins or loaders (such as vite-plugin-vue for Vite).
The implementation does not have a single large function called compile
, but instead, there are separate functions for parsing the entire Single File Component, like parse
, and compiling each block, such as compileScript
, compileTemplate
, compileStyle
.
The result of parse
is a structured object named SFCDescriptor
, and each block's compile function takes this SFCDescriptor
(or information obtained from the SFCDescriptor) as an argument.
Parsing and Compiling the Script Block
Since we are mainly focusing on the compiler related to the script block this time, we will explore compileScript
and related files in the script/
directory.
Under the script/
directory, various processes are divided into separate files, and these are called from compileScript
. In this case, we will mainly follow these 3 processes:
The compileScript.ts
file serves as the entry point from where these processes are called.
The compileScript
function is a large function that receives the SFCDescriptor and compiles the code. It extracts information from the Descriptor for <script>
and <script setup>
and processes them accordingly.
Analyzing Metadata
Starting with the analysis of metadata, we can see an object holding the bindings information in the code:
Later on, several binding pieces of information are ultimately stored in ctx.bindingMetadata
.
This ctx.bindingMetadata
seems to store the results from analyzeScriptBindings
, and it appears that all the binding metadata is aggregated in this object.
This object is also used as a result of this function, indicating its importance in the overall process.
By compiling an SFC like the one below and outputting ctx.bindingMetadata
, we obtained the following results:
<script setup lang="ts">
import { computed } from "vue";
const { count: renamedProps = 0 } = defineProps<{ count?: number }>();
const double = computed(() => renamedProps * 2);
</script>
<template>
<div>{{ renamedProps }}{{ count }}{{ double }}</div>
</template>
The output was:
{
renamedProps: 'props-aliased',
__propsAliases: { renamedProps: 'count' },
computed: 'setup-const',
double: 'setup-ref',
count: 'props'
}
It is also good to keep in mind that the following types of bindings are enumerated as prior knowledge.
Now let's take a look at how ctx.bindingMetadata is actually generated. When you look at the big picture, compileScript processes in steps 1.1 to 11.
- Some parts omitted
. . .
Let's examine the parts related to bindingMetadata from within these. Furthermore, this time, we will only focus on the parts related to defineProps and Reactive Props Destructure.
1.1 Walk Import Declarations of <script>
This is the parsing of the import statements in the <script>
tag.
Since import statements also have identifier bindings, they are parsed and added to bindingMetadata
.
It is important to note here that these are handled within <script>
, and <script setup>
is handled differently.
The AST is manipulated to find the import statements.
Once found, the information is registered using a function called registerUserImport
.
At this point, it is not yet added to bindingMetadata
, but registered in an object called ctx.userImports
.
This ctx.userImports
will later be integrated into bindingMetadata
.
1.2 Walk Import Declarations of <script setup>
This is the <script setup>
version of the previous process.
The process of generating bindingMetadata
is the same, but unlike the normal script, the import statements are hoisted. (since they cannot be written inside the setup function)
2.1 Process Normal <script>
Body
This is quite lengthy, but the part to focus on is the walkDeclaration
called in the latter half.
Walk Declaration
Let's take a look at walkDeclaration
a little ahead of time. (Although it does not actually relate to 2.1, you will see it frequently from now on)
When there are declarations of variables, functions, or classes, it adds the information to bindingMetadata
based on them.
It makes detailed determinations, such as whether the defined variable is const
, whether the initial value is a call to ref
, computed
, shallowRef
, customRef
, or toRef
.
Additionally, a crucial point here is that it also determines if the initial value is a call to a compiler macro such as defineProps
, defineEmits
, or withDefaults
.
At this stage, it seems to not yet be marked as BindingTypes.PROS
.
(If it were defineProps
, it would become BindingTypes.SETUP_REACTIVE_CONST
)
2.2 Process <script setup>
Body
Next is the process for <script setup>
.
Immediately, we see some intriguing functions like processDefineProps
, processDefineEmits
, and processDefineOptions
.
Let's look at the part that handles variable declarations.
At this point, you can see that defineProps
and defineEmits
are being processed.
This time, we will focus on defineProps
.
Therefore, let's read the function processDefineProps
.
Reading Define Props
processDefineProps
is implemented in script/defineProps.ts.
To start off, if node
is not a call to defineProps
, it will go to processWithDefaults
.
Let's take a look at processWithDefaults
.
Similarly, if it is not a call to withDefault
, it returns false and ends.
No further processing is done.
If it is a call to withDefault
, it handles the call by assuming that the first argument being passed is defineProps
, and processes it through processDefineProps
.
Now, let's go back to processDefineProps
.
defineProps
has two APIs.
One that passes the definition of Runtime Props as the first argument, and one that specifies the type in the generics.
First, the former object is stored in ctx
.
If such objects exist, their keys are taken out and registered in bindingMetadata
as BindingTypes.PROPS
.
Next, while checking for overlaps with the former, the definition (type argument) of the API in the latter case is also stored in ctx
.
Next, if declId
is an object pattern, processPropsDestructure
is called as Props Destructure.
processPropsDestructure
is implemented in script/definePropsDestructure.ts.
Let's take a look at processPropsDestructure
.processPropsDestructure
is responsible for parsing the definitions of props and does not handle the transformation of prop references (e.g., rewriting count
to __props.count
).
It proceeds through declId.properties
, and if there are default values, it adds them to the prop definitions.
The processing of transforming references will be read later. For now, we end here.
Let's go back to processDefineProps
.
Though we go back, there is nothing particularly done after this, so processDefineProps
ends here.
After this, we only need to generate code based on ctx.propsRuntimeDecl
or ctx.propsTypeDecl
completed here.
The generation process is not written here, so we will look at it later.
2.2 Continuing the process of <script setup>
body
We've returned to compileScript
.
After handling defineProps
and defineEmits
, we look for and walkDeclaration
through variable, function, and class declarations just as we did in section 2-1.
The walkDeclaration
behaves as previously explained.
In addition to those, there are some special handlings required in <script setup>
.
They are unrelated to the main theme of Reactive Props Destructure, so I won't go into detail.
- Marks as an asynchronous component when using top-level await
https://github.com/vuejs/core/blob/6402b984087dd48f1a11f444a225d4ac6b2b7b9e/packages/compiler-sfc/src/compileScript.ts#L628-L649 - Throws error if there are export statements
https://github.com/vuejs/core/blob/6402b984087dd48f1a11f444a225d4ac6b2b7b9e/packages/compiler-sfc/src/compileScript.ts#L656-L667
That's it for section 2's processing.
3 props destructure transform
Next, let's proceed to section 3. This is the crux of our discussion.
As can be seen from the name of the function being executed, this is finally the essence of Reactive Props Destructure.
Reading propsDestructure
Let's dive in. This function transformDestructuredProps
is implemented in script/definePropsDestructure.ts.
In this article as well,
It appears to handle scope control properly,
As mentioned, scope management is necessary.
First is the place to store scope information.
We can also find functions that manipulate the scope stack.
The first half includes other local functions, but we can skip over those for now and read them as needed.
The main processing is below this point.
The AST for <script setup>
is retrieved from ctx
, and we walk through the tree.
By using the walk
function provided by the estree-walker library to traverse the AST and create the final output,
Before starting this walk, we execute a function called walkScope
once to register binding information in the current scope (root).
We look through node.body
(Statement[]
) to find and register places that can generate identifiers.
Specifically, it involves variable, function, and class declarations.
This way, binding information is registered to the scope set as currentScope
.
The walkScope
function is called within the walk
function.
Now, let's look at what is being done within the walk
function.
The leave
hook doesn't do much, so let's understand that part first.
The important part is that when the AST Node type matches /Function(?:Expression|Declaration)$|Method$/
or is a BlockStatement
, it executes popScope
.
That's all we need to keep in mind.
Now for the main enter
hook.
function scopes
When it matches /Function(?:Expression|Declaration)$|Method$/
.
It performs pushScope
while also walking arguments to register bindings.
catch param
Next is catch parameters. This part can easily be overlooked,
try {
} catch (e) {}
It refers to e
in catch (e)
. Don't forget to register this as well.
non-function block scopes
Next are block scopes.
These are blocks other than functions, such as the BlockStatement passed to if, for, while, try, etc.
Here, we look around the body statements to find variable or function declarations and register bindings.
identifier
By now, binding registration is complete, and finally, when entering an Identifier, it rewrites the id based on the binding information.
For example, count
is converted to __props.count
at this point.
It checks the scope value and executes rewriteId
if not a local variable.
In rewriteId
, it not only rewrites simple identifiers (e.g., x --> __props.x
) but also handles object shorthand (e.g., { prop } -> { prop: __props.prop }
).
This concludes the processing for identifier rewriting in Reactive Props Destructure.
By now, you should have a good understanding of the Reactive Props Destructure processing.
Next is generating the code based on the information obtained so far.
We're almost there, but first, there's something to do with bindingMetadata's registration, so let's take a look at that next.
6. Analyze binding metadata
You may have forgotten what has been done so far, but we're finally back.
We have proceeded through compileScript from sections 1-1, 1-2, 2-1, 2-2, and 3, and in section 3, we looked into the Reactive Props Destructure processing.
Let's continue.
We'll skip sections 4 and 5 for now and move on to analyzing binding metadata in section 6.
First, analyzeScriptBindings
is executed on scriptAst
(the AST of the non-setup script).
analyzeScriptBindings
is implemented in script/analyzeScriptBindings.ts.
What this does is look for export default
and analyze bindings from the Options API.
For example, the props
option.
If a key named props
exists in the object exported by export default
, it is registered as BindingTypes.PROPS
.
It also analyzes and registers inject, computed, methods, setup, data, and more, into bindingMetadata.
Now, let's see the continuation of section 6 processing after analyzeScriptBindings
.
The continuation is straightforward: it consolidates ctx.userImports
, scriptBindings
, setupBindings
, and other collected information into ctx.bindingMetadata
.
With this, the bindingMetadata is complete! Good job!
Next, let's look into code generation!
8. Finalize setup()
Argument Signature
This section is where we finalize the signature for the generated setup
function code.
The reason why the first argument props
becomes __props
happens here.
10. Finalize Default Export
Here, we assemble the final code to be output.
In particular, we want to focus on the following part:
Here, it generates the code defining the props
and adds it to runtimeOptions
(a variable that stores the code to be output as component options).
Let's take a look at genRuntimeProps
.
This function is defined in script/defineProps.ts.
If it is passed as a runtime object, it returns it as a string as is.
If it’s destructured, it extracts the default values and outputs the code that merges them at runtime using a function called mergeDefaults
.
Next, we deal with the case where props
is defined with type arguments.
The function extractRuntimeProps
generates a runtime object (as a string) based on the type information.
It converts the type information into prop definition data using a function called resolveRuntimePropsFromType
.
Surprisingly, this process is relatively simple, and in the end, it generates an object containing key names, types, and required information.
As mentioned in this article,
it determines
required
by examining whether parameters are optional
based on the type information.
Let’s delve into how this definition data is created.
First, it resolves the type information using a function called resolveTypeElements
.
Since Vue.js 3.3.0, types imported through macros like defineProps
can be used.
https://blog.vuejs.org/posts/vue-3-3#imported-and-complex-types-support-in-macros
Therefore, it not only parses the type literal passed as type arguments but also resolves type definitions imported from external files.
This is generally handled in the following processes.
To resolve them, it requires settings like alias in tsconfig
, thus loading TypeScript and using its API to read tsconfig
.
Using the resolved type information, it infers the types specified for props.
This function is inferRuntimeType
.
This too is quite complex, but it infers based on the type node’s type.
Although we won't go into the details of these inference methods, it painstakingly writes branches, uses resolveTypeReference
at key points, and recursively calls inferRuntimeType
.
As a result, this process enables the generation of the necessary information for the runtime Props definition object from the type information given in the type arguments. The rest is generation.
We return to this point:
Now, it’s just a matter of looping through each prop definition and generating code using a function called genRuntimePropFromType
.
Handling Identifiers in Templates
That was quite lengthy, but we’ve now traced the implementation of Reactive Props Destructure with defineProps
.
Simultaneously, with the binding information registered in bindingMetadata
, the compileTemplate
can refer to this information to generate the correct reference code for identifiers used in the template.
Since the purpose here is to understand Reactive Props Destructure, we will not delve into the template compiler, but the relevant code is found around here:
The template compiler has an interface called a transformer, where arbitrary transform processes can be executed.
Specifically, this is a transformer called transformExpression
, which transforms expressions appearing in the template.
Particularly for CallExpression, MemberExpression, and IdentifierExpression, it needs to trace the correct reference source and rewrite the code accordingly.
That process is the aforementioned rewriteIdentifier
.
Based on the registered BindingTypes
, it adds the correct prefix.
For example, {{ renamedCount }}
-> {{ $props['renamedCount'] }}
Summary of Implementation Methods
We have now seen the parsing of bindingMetadata
, handling of defineProps
and withDefault
, tracking references to destructured props, and compiling template based on bindingMetadata
.
This gives us a full picture of the process.
Well done!
However, the title here wasn't "How to Implement Props Destructure!", it was "The Technology Behind Reactive Props Destructure".
The compiler is, of course, one part of it, but there's another crucial aspect.
That is "support from language tools".
Support from Language Tools
In the RFC, some shortcomings of Props Destructure were highlighted, such as:
- There’s a possibility of losing reactivity by mistakenly passing destructured props to functions.
- It may not be explicitly clear that it's props (making it harder to distinguish from other variables).
- Confusion among beginners due to compiler magic.
Though the overall conclusion was "Well, these have been the same issues before, so it's not a major problem,"
the Reactive Props Destructure was created to improve DX (Developer Experience).
If there are parts that can be improved, we should improve them.
Thus, the Vue Team decided to solve these issues with support from language tools.
Specifically, they implemented inlay hints to make it obvious that something is a prop.
This was mentioned in the release blog.
https://blog.vuejs.org/posts/vue-3-5#reactive-props-destructure
For those who prefer to better distinguish destructured props from normal variables, @vue/language-tools 2.1 has shipped an opt-in setting to enable inlay hints for them:
This makes an effort to maintain the clarity of props while still benefiting from concise props definitions such as default values and aliases.
This feature is implemented as a plugin in vuejs/language-tools.
If you take a peek at the findDestructuredProps
function, you can observe an implementation similar to the transformDestructuredProps
we saw in compiler-sfc.
Just like transformDestructuredProps
, it analyzes scopes and bindings to search identifiers.
In Summary, How to Approach Reactive Props Destructure
This article turned out to be quite long, but we covered the background, implementation methods, and the support from language tools related to the Reactive Props Destructure, which became stable in Vue 3.5.0.
As evident from the discussions in the RFC, there are mixed opinions about this Reactive Props Destructure, and many people were concerned about its shortcomings.
In fact, the Vue.js Team has an internal discussion board that only team members can view/post on, where discussions are held.
Reactive Props Destructure was a topic of a lot of discussions there as well.
Personally, I believe Props Destructure is a great feature.
To evaluate it as "great", there are several key points, which I will summarize below to conclude this article:
- Vue.js has a consistent philosophy of "improving DX through compilers and language tools".
This is a consistent thought process. I would recommend checking out my previous slide regarding this. What is Vue.js? Hmm… It’s just a language lol - Consider where the best compromise point is with existing APIs.
In Vue.js, the runtime code to define props is solely theprops
option.
For more on this, check out the article related to compiler macros on the same publication. - Understand the pros and cons, and consider how to face the cons.
This goes without saying.
Read the RFC for motivations and drawbacks, and how to face those drawbacks. https://github.com/vuejs/rfcs/discussions/502#discussion-5140019 Focus on the "Motivation" and "Drawbacks" sections. Also, consider the "support from language tools" mentioned here as an additional context to the drawbacks section. - Try to understand how it works.
This is not mandatory, but understanding the implementation can help you grasp the concepts and key points.
Refer to this article for that understanding. Once understanding the mechanism, focus momentarily on the interface when coding, then recall the mechanism when slightly puzzled.
Being able to switch between these can be highly advantageous.
That's it.