You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Implementation of the setup function as a composable. The target API is shown below.
<!-- Child.vue -->
<script setup>
// The body of a composable
import { ref } from 'vue'
// Define props normally. Prop desctructure is still managed by the compiler.
// Ref props are unwraped by default
const props = defineProps({
prop1: String,
prop2: String,
prop3: {
type: Ref,
unwrap: false
}
})
// Created per instance
const v = ref(0)
// Hoisted out of setup
#define function public() {
// No access to variables declared in script setup,
// same as defineProps
}
// Exposed; return value of the composable
return {
v,
public
}
</script>
<!-- Parent.vue -->
<script setup>
import Child from './Child.vue'
import { ref, useExposed } from 'vue'
const ref1 = ref('str-1')
const ref2 = ref('str-2')
const ref3 = ref('str-3')
// Exposed refs unwrapped by default
const exposed = useExposed('child') // Call Child's setup composable
/**
* Notes:
* 1. If called, useExposed must be called before Child mounts.
* 2. useExposed cannot be called multiple times for the same instance.
* 3. Variables passed as props need to be defined by the time useExposed is called.
*/
</script>
<template render>
<!-- Refs are not automatically unwrapped -->
<Child v-exposed="child" :prop1="ref1.value" :prop2="ref2" :prop3="ref3"/>
</template>
Introduction
The Composition API provides useful features whose benefits become more evident when extensively working with composables [1]. However, the typical pattern followed with Composition API seems to deviate when used along component templates in Single File Component (SFC). Handling ref objects, for instance, tends to be very different when dealing with components than when using composables.
I'll (try to briefly) explore two specific new features that might be useful for Vue's SFCs. It will then be found that the reasoning followed for those features may be extended, leading to a new way of thinking of the component setup function where it is modeled and implemented as a composable itself.
Two new features
Hoist variables out of setup into module scope from <script setup>.
When using <script setup>, all variables all defined per instance inside the setup function [2]. However, if the component uses several functions or constants that are not required in the template, it may be appropriate to hoist them to the model scope to be reused by different component instances [3].
Currently, there are two ways of achieving this: 1) extract variables into a .js file and import them, and 2) extract variables into a regular <script> tag. However, both alternatives may introduce the separation of concerns problem that the Composition API aimed to solve from Options API (especially if extracted variables are tightly coupled to the component itself, e.g., if they're not used anywhere else).
This could be solved by marking variable declarations with a #define compiler directive so that the compiler moves those variables to the module scope. The #define keyword is intentionally chosen to maintain other compiler macro's naming convention and alude to the fact that variables normally defined inside <script setup> are not available inside functions, object initializers, etc., defined with the #define directive [4].
<script setup>
// Hoist out of setup#define functionfn() {// No access to variables declared in <script setup>}
</script>
Although the main use case would be extracting functions, the #define directive would also made it possible to easily create shared state between component instances.
This feature further improves <script setup> ergonomics for interacting with regular <script> and reduces the instances where regular <script> is required.
Pass ref objects as props.
It is my impression that being able to directly pass ref objects is a fundamental feature of the Composition API, but this is not possible through props in SFC templates because refs are automatically unwrapped [5,6].
In short, I believe directly passing ref objects as props to components would be useful in the same way directly passing refs as arguments to composables is useful [7,8].
To satisfy this need, the following API is proposed.
<script setup>
constv=ref('8')
</script>
<template render>
<!-- Refs are not automatically unwrapped -->
<Child:prop="v"/>
</template>
Apart from possible implementation complexities and/or constraints, I'd suppose another reason ref automatic shallow unwrapping is performed is to prevent child components' direct access to the ref's object instance, which might also be why component models are implemented with a modelValue-update:modelValue pair; this, also aligned with Vue's recommendation of not mutating objects passed through props. However, for objects, I don't think unexpected mutations should be a concern in the same way they are typically not a concern when passing ref objects to a composable (or passing any object to any function, for that matter). Following this analogy with parameters passed to composables, it'd make sense for primitive props to be readonly while object properties are naturally writable. In any case, Vue's guide does mention that child components mutating parent's state is fine if the parent and child components are tightly coupled by design. However, I'd say a great example of "tightly coupled by design" is components using models (with v-model and defineModel), yet child components don't have access to the model's ref object, making the (internal) implementation more elaborate. Therefore, it seems that the main reason for unwrapping refs is not found in preventing child component acess to parent's state [9,10].
On the other hand, I think there are benefits to not automatically unwrapping refs in the template:
Consistency. Ref notation becomes consistent between SFC templates and script/composables; for instance, writing SFC template becomes more similar to how components are written with render functions [11].
Explicitness. Ref behavior in SFC templates becomes more predictable and explicit.
Additionally, and although not directly related to ref usage in SFC templates, I think the reasons why the Reactivity Transform was dropped align in part with the beforementioned benefits [12].
Nevertheless, the concern here is only with refs passed through props. Perhaps it is possible and appropriate to only prevent unwrapping in props while still shallow unwrapping other template expressions.
Further exploring setup function as a composable
As previously mentioned, an useful analogy of passing props to components is passing arguments to a composable. Regarding the #define directive feature, a similarity with composables can also be found in that composables may also define module scoped variables for global use. Thus, it might be worth exploring what can be achieved by thinking of the setup function as a regular composable [13].
A possible next step is to consider the component's exposed data as the returned value of a composable. Then, this composable should be callable in the parent's setup. If the composable could be executed synchronously within the parent's setup, the API to access a components exposed data could be greatly improved [14].
A conceptual concern with this approach might be that the setup function is too tightly coupled with the rendering mechanism. However, from a high level perspective, a setup function simply seems to execute code to prepare a component's state and data for the template. So it might be possible to partially detach the setup function from the rendering mechanism so that it is able to be called inside other setup functions [15].
The setup function not only needs to provide exposed data, but also data to be made available for the template (when using regular <scritp>). Then, it'd be useful to depict the setup function as returning an object with render and expose keys:
When using <script setup>, the compiler should still insert a compiled render function for the render property [16].
Then, a new useExposed composable could be introduced for a parent component to execute the setup function and only return the expose data.
In order to implement such API, I found there are at least three major concerns that need to be addressed.
1. Prop tracking
First, how would props be tracked? The premise here is that reactivity will be handled in the same way that composables do; a composable only receives refs or getters, and they are tracked as required according to their use. This also implies that for most cases, it would still not be required to explicitly write .value when passing refs through props, but now the ref object itself will be passed to the child component.
From a DX point of view, an important concern is props object unwrapping. A prop could still be a regular or reactive value, so a way for the user to comfortably read from either is required. This problem may also be relevant to the exposed data; altough exposed data may be more predictable (precise knowledge of which properties are refs), full .value notation for not-deeply-nested refs is still cumbersome (exposed.ref.value). Then, a mechanism to shallow unwrap refs is still required.
This could be solved with a proxy whose only task is shallow unwrapping refs in props and exposed data. Destructuring of props and exposed data should still be handled by the compiler.
Furthermore, since it is only required to unwrap not deeply nested refs, a regular object with custom accessors could be used instead of a proxy. Usage of proxies would be significantly reduced (again, just the same way proxies are not extensively required when working with composables).
I have, for other use cases, developed an API that creates objects that are able to shallow unwrap refs. For reference, this API's main features are shown below.
// Define regular or reactive propertiesconstextendedA=extendedReactive({prop1: 1,prop2: ref(2)})// Reactive properties are unwrapped by defaultconsole.log(extendedA.prop1)// 1console.log(extendedA.prop2)// 2console.log(isRef(extended.prop2))// falseconsole.log(isRef(extended.refs.prop2))// true// Configure propertiesconstextendedB=extendedReactive(withDescriptor=>({prop1: 1,prop2: withDescriptor({value: 2,enumerable: false,writable: false}),prop3: withDescriptor({value: ref(3),unwrap: false,readonly: true})}))
3. Component unmounting
3.1. Re-execution of setup function
When a component is unmounted and re-mounted with v-if, its setup function is always run before the component is mounted [2]. The things to take into consideration are the props and the returned (exposed) value: When the component re-mounts, the values of the passed props may be different from those first sent; likewise, different props may produce a different state of the exposed data.
To address this, the useExposed composable needs to observe whether the component is mounted to be able to send the updated props to the setup function. Then, props should be provided as a getter, not a plain object. This can be automatically handled by the compiler, but if not using SFC, props should always be wrapped with a getter function.
Regarding the exposed data, the returned object could be easily updated. But there's one concern with auto unwrapped refs. An advantage of using useTemplateRef is that it is required to read a "root" ref before accessing any other exposed data (i.e., when reading any property exposed.value.property the exposed ref is always tracked). This is useful for scenarios where an exposed unwrapped ref is tracked by an effect; when the component unmounts, the exposed data becomes null, making the exposed ref inaccesible, but since the "root" ref gets updated, the effect will properly update its dependencies as well. Wether using a proxy or an object to shallow unwrap refs, this same task can be accomplished. Embedded are links to a pair of demos that show this behavior with both defineExpose and a simplified approach of how useExtended could be implemented.
It can also be mentioned that a recommendation for users would be not to directly expose ref objects since new ones are created every time the setup function is re-executed. Shallow unwrapping may even be enforced for exposed data, i.e., not allowing to send plain refs in the root of the exposed object. This would also emulate the behavior of useTemplateRef but without having to write .value. Were an use case required to expose ref objects, it might be useful to also expose a way for the user to easily cleanup effects tracking exposed refs (such as an effect scope).
3.2. Alternative
Finally, it could also be explored not to re-execute the setup function when the component is re-mounted, that is, once a component is mounted, its setup function won't execute again. Above it was mentioned that the purpose of re-executing the setup function is retrieving new props and computing new exposed data. However, if props and exposed data that are expected to change are properly handled with the Reactivity API, it would not be required to execute the whole function before every re-mount. This implies props not sent as refs will not be updated, but that's how composables typically operate: data that will change or needs to be tracked must be passed as refs/getters.
If the setup function is executed once, it becomes easier to handle the exposed data and risks of tracking refs that are no longer managed by the component (because they were replaced by new ones on the setup function's re-execution) are reduced.
I think the main concern with this alternative is that the setup function side effects would stay in place. However, the unnecesary computations of side effects can easily (and efficiently) be avoided by pausing the child component setup's effect scope while it is unmounted. By doing so, the side effects staying in place becomes (in some cases) a benefit since they do not have to be set up again anymore. The component being unmounted would now only imply its DOM tree is unmounted while the logical part stays in place, but paused to optimize performance.
It also might be possible to have both solutions and let the user decide which to use, similar to v-if and v-show. Pausing the setup's scope would be preferred for components that are frequently toggled.
Conclusion
Taking all previous discussions into account, the target API is the one presented in the summary. It seems to me that the reached API better mirrors the Composition API pattern used outside SFCs.
I just wanted to explore if directly passing refs as props was possible and ended having all these other ideas, so I though I'd share them for discussion. If you find issues or errors in my reasoning, please point them out. If you agree with something, or find it appealing, please share your thoughts. I'll appreciate your feedback.
Reference
[1] The primary advantage of Composition API is that it enables clean, efficient logic reuse in the form of Composable functions.
[2] The code inside [<script setup>] is compiled as the content of the component's setup() function. This means that unlike normal <script>, which only executes once when the component is first imported, code inside <script setup> will execute every time an instance of the component is created.
[3] [Normal <script> may be nedeed to] run side effects or create objects that should only execute once.
[4] The options passed to defineProps and defineEmits will be hoisted out of setup into module scope. Therefore, the options cannot reference local variables declared in setup scope. Doing so will result in a compile error. However, it can reference imported bindings since they are in the module scope as well.
[5] refs returned from setup are automatically shallow unwrapped when accessed in the template so you do not need to use .value when accessing them.
[6] Similar to values returned from a setup() function, refs are automatically unwrapped when referenced in templates
[7] [Section of the guide showing the usefulness of directly passing refs to composables]
[8] [Guide on composables accepting refs or getters arguments even if reactivity is optional]
[9] All props form a one-way-down binding between the child property and the parent one: when the parent property updates, it will flow down to the child, but not the other way around. This prevents child components from accidentally mutating the parent's state, which can make your app's data flow harder to understand.
[10] When objects and arrays are passed as props, while the child component cannot mutate the prop binding, it will be able to mutate the object or array's nested properties. This is because in JavaScript objects and arrays are passed by reference, and it is unreasonably expensive for Vue to prevent such mutations. The main drawback of such mutations is that it allows the child component to affect parent state in a way that isn't obvious to the parent component, potentially making it more difficult to reason about the data flow in the future. As a best practice, you should avoid such mutations unless the parent and child are tightly coupled by design. In most cases, the child should emit an event to let the parent perform the mutation.
[11] [Setup function] usage with render functions.
[12] Losing .value makes it harder to tell what is being tracked and which line is triggering a reactive effect. [...] Since there will still be external functions that expect to work with raw refs, the conversion between Reactive Variables and raw refs is inevitable.
[13] In the context of Vue applications, a "composable" is a function that leverages Vue's Composition API to encapsulate and reuse stateful logic.
[14] The recommended convention is for composables to always return a plain, non-reactive object containing multiple refs.
[15] The cooler part about composables though, is that you can also nest them: one composable function can call one or more other composable functions. This enables us to compose complex logic using small, isolated units, similar to how we compose an entire application using components.
[16] [<script setup> provides] better runtime performance (the template is compiled into a render function in the same scope, without an intermediate proxy)
reacted with thumbs up emoji reacted with thumbs down emoji reacted with laugh emoji reacted with hooray emoji reacted with confused emoji reacted with heart emoji reacted with rocket emoji reacted with eyes emoji
-
Summary
Implementation of the
setup
function as a composable. The target API is shown below.Introduction
The Composition API provides useful features whose benefits become more evident when extensively working with composables [1]. However, the typical pattern followed with Composition API seems to deviate when used along component templates in Single File Component (SFC). Handling ref objects, for instance, tends to be very different when dealing with components than when using composables.
I'll (try to briefly) explore two specific new features that might be useful for Vue's SFCs. It will then be found that the reasoning followed for those features may be extended, leading to a new way of thinking of the component setup function where it is modeled and implemented as a composable itself.
Two new features
Hoist variables out of setup into module scope from
<script setup>
.When using
<script setup>
, all variables all defined per instance inside the setup function [2]. However, if the component uses several functions or constants that are not required in the template, it may be appropriate to hoist them to the model scope to be reused by different component instances [3].Currently, there are two ways of achieving this: 1) extract variables into a
.js
file and import them, and 2) extract variables into a regular<script>
tag. However, both alternatives may introduce the separation of concerns problem that the Composition API aimed to solve from Options API (especially if extracted variables are tightly coupled to the component itself, e.g., if they're not used anywhere else).This could be solved by marking variable declarations with a
#define
compiler directive so that the compiler moves those variables to the module scope. The#define
keyword is intentionally chosen to maintain other compiler macro's naming convention and alude to the fact that variables normally defined inside<script setup>
are not available inside functions, object initializers, etc., defined with the#define
directive [4].Although the main use case would be extracting functions, the
#define
directive would also made it possible to easily create shared state between component instances.This feature further improves
<script setup>
ergonomics for interacting with regular<script>
and reduces the instances where regular<script>
is required.Pass ref objects as props.
It is my impression that being able to directly pass ref objects is a fundamental feature of the Composition API, but this is not possible through props in SFC templates because refs are automatically unwrapped [5,6].
In short, I believe directly passing ref objects as props to components would be useful in the same way directly passing refs as arguments to composables is useful [7,8].
To satisfy this need, the following API is proposed.
Apart from possible implementation complexities and/or constraints, I'd suppose another reason ref automatic shallow unwrapping is performed is to prevent child components' direct access to the ref's object instance, which might also be why component models are implemented with a
modelValue
-update:modelValue
pair; this, also aligned with Vue's recommendation of not mutating objects passed through props. However, for objects, I don't think unexpected mutations should be a concern in the same way they are typically not a concern when passing ref objects to a composable (or passing any object to any function, for that matter). Following this analogy with parameters passed to composables, it'd make sense for primitive props to be readonly while object properties are naturally writable. In any case, Vue's guide does mention that child components mutating parent's state is fine if the parent and child components are tightly coupled by design. However, I'd say a great example of "tightly coupled by design" is components using models (withv-model
anddefineModel
), yet child components don't have access to the model's ref object, making the (internal) implementation more elaborate. Therefore, it seems that the main reason for unwrapping refs is not found in preventing child component acess to parent's state [9,10].On the other hand, I think there are benefits to not automatically unwrapping refs in the template:
Additionally, and although not directly related to ref usage in SFC templates, I think the reasons why the Reactivity Transform was dropped align in part with the beforementioned benefits [12].
Nevertheless, the concern here is only with refs passed through props. Perhaps it is possible and appropriate to only prevent unwrapping in props while still shallow unwrapping other template expressions.
Further exploring setup function as a composable
As previously mentioned, an useful analogy of passing props to components is passing arguments to a composable. Regarding the
#define
directive feature, a similarity with composables can also be found in that composables may also define module scoped variables for global use. Thus, it might be worth exploring what can be achieved by thinking of the setup function as a regular composable [13].A possible next step is to consider the component's exposed data as the returned value of a composable. Then, this composable should be callable in the parent's setup. If the composable could be executed synchronously within the parent's setup, the API to access a components exposed data could be greatly improved [14].
A conceptual concern with this approach might be that the setup function is too tightly coupled with the rendering mechanism. However, from a high level perspective, a setup function simply seems to execute code to prepare a component's state and data for the template. So it might be possible to partially detach the setup function from the rendering mechanism so that it is able to be called inside other setup functions [15].
The setup function not only needs to provide exposed data, but also data to be made available for the template (when using regular
<scritp>
). Then, it'd be useful to depict thesetup
function as returning an object withrender
andexpose
keys:When using
<script setup>
, the compiler should still insert a compiled render function for therender
property [16].Then, a new
useExposed
composable could be introduced for a parent component to execute the setup function and only return theexpose
data.In order to implement such API, I found there are at least three major concerns that need to be addressed.
1. Prop tracking
First, how would props be tracked? The premise here is that reactivity will be handled in the same way that composables do; a composable only receives refs or getters, and they are tracked as required according to their use. This also implies that for most cases, it would still not be required to explicitly write
.value
when passing refs through props, but now the ref object itself will be passed to the child component.2. Props (and exposed data) ref shallow-unwrapping
From a DX point of view, an important concern is
props
object unwrapping. A prop could still be a regular or reactive value, so a way for the user to comfortably read from either is required. This problem may also be relevant to the exposed data; altough exposed data may be more predictable (precise knowledge of which properties are refs), full.value
notation for not-deeply-nested refs is still cumbersome (exposed.ref.value
). Then, a mechanism to shallow unwrap refs is still required.This could be solved with a proxy whose only task is shallow unwrapping refs in props and exposed data. Destructuring of props and exposed data should still be handled by the compiler.
Furthermore, since it is only required to unwrap not deeply nested refs, a regular object with custom accessors could be used instead of a proxy. Usage of proxies would be significantly reduced (again, just the same way proxies are not extensively required when working with composables).
I have, for other use cases, developed an API that creates objects that are able to shallow unwrap refs. For reference, this API's main features are shown below.
3. Component unmounting
3.1. Re-execution of
setup
functionWhen a component is unmounted and re-mounted with
v-if
, itssetup
function is always run before the component is mounted [2]. The things to take into consideration are the props and the returned (exposed) value: When the component re-mounts, the values of the passed props may be different from those first sent; likewise, different props may produce a different state of the exposed data.To address this, the
useExposed
composable needs to observe whether the component is mounted to be able to send the updated props to thesetup
function. Then, props should be provided as a getter, not a plain object. This can be automatically handled by the compiler, but if not using SFC, props should always be wrapped with a getter function.Regarding the exposed data, the returned object could be easily updated. But there's one concern with auto unwrapped refs. An advantage of using
useTemplateRef
is that it is required to read a "root" ref before accessing any other exposed data (i.e., when reading any propertyexposed.value.property
theexposed
ref is always tracked). This is useful for scenarios where an exposed unwrapped ref is tracked by an effect; when the component unmounts, the exposed data becomes null, making the exposed ref inaccesible, but since the "root" ref gets updated, the effect will properly update its dependencies as well. Wether using a proxy or an object to shallow unwrap refs, this same task can be accomplished. Embedded are links to a pair of demos that show this behavior with bothdefineExpose
and a simplified approach of howuseExtended
could be implemented.It can also be mentioned that a recommendation for users would be not to directly expose ref objects since new ones are created every time the
setup
function is re-executed. Shallow unwrapping may even be enforced for exposed data, i.e., not allowing to send plain refs in the root of the exposed object. This would also emulate the behavior ofuseTemplateRef
but without having to write.value
. Were an use case required to expose ref objects, it might be useful to also expose a way for the user to easily cleanup effects tracking exposed refs (such as an effect scope).3.2. Alternative
Finally, it could also be explored not to re-execute the
setup
function when the component is re-mounted, that is, once a component is mounted, itssetup
function won't execute again. Above it was mentioned that the purpose of re-executing thesetup
function is retrieving new props and computing new exposed data. However, if props and exposed data that are expected to change are properly handled with the Reactivity API, it would not be required to execute the whole function before every re-mount. This implies props not sent as refs will not be updated, but that's how composables typically operate: data that will change or needs to be tracked must be passed as refs/getters.If the
setup
function is executed once, it becomes easier to handle the exposed data and risks of tracking refs that are no longer managed by the component (because they were replaced by new ones on thesetup
function's re-execution) are reduced.I think the main concern with this alternative is that the
setup
function side effects would stay in place. However, the unnecesary computations of side effects can easily (and efficiently) be avoided by pausing the child component setup's effect scope while it is unmounted. By doing so, the side effects staying in place becomes (in some cases) a benefit since they do not have to be set up again anymore. The component being unmounted would now only imply its DOM tree is unmounted while the logical part stays in place, but paused to optimize performance.It also might be possible to have both solutions and let the user decide which to use, similar to
v-if
andv-show
. Pausing the setup's scope would be preferred for components that are frequently toggled.Conclusion
Taking all previous discussions into account, the target API is the one presented in the summary. It seems to me that the reached API better mirrors the Composition API pattern used outside SFCs.
I just wanted to explore if directly passing refs as props was possible and ended having all these other ideas, so I though I'd share them for discussion. If you find issues or errors in my reasoning, please point them out. If you agree with something, or find it appealing, please share your thoughts. I'll appreciate your feedback.
Reference
Beta Was this translation helpful? Give feedback.
All reactions