Support `MaybeRefOrGetter` as the type for passing props
What problem does this feature solve?
Getters are extremely useful to avoid the overhead of computed properties and to pass data that should be re-evaluated when accessed. So something like () => new Date()
vs new Date()
. #7997 is great and provides some nice tools for handling getters and normalizing values specifically the MaybeRefOrGetter
type and the toValue
and toRef
utilities.
Props basically have the type MaybeRef
because you can bind either a static value or a ref :value="new Date()"
vs :value="myDateRef"
. However you cannot pass a getter. Currently in order to do so you'd have to make the type for your prop Date | (() => Date)
and then normalize the value using toRef
or toValue
inside your component. Then you could pass :value="myDateGetter"
.
It would be a nice developer experience to allow all props to be MaybeRefOrGetter
and automatically normalize the value. So you could bind myDateGetter
while the prop type would simply Date
.
What does the proposed API look like?
DateLabel.vue
<template>
<span>{{ value.toLocaleString() }}</span>
</template>
<script lang="ts" setup>
defineProps<{
value: Date
}>()
</script>
When using the DateLabel
component all three of these would pass type validation and render the date label.
<template>
<DateLabel :value="dateConst" />
<DateLabel :value="dateRef" />
<DateLabel :value="dateGetter" />
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import DateLabel from './DateLabel.vue'
const dateConst = new Date()
const dateRef = ref(new Date())
const dateGetter = () => new Date()
</script>
<DateLabel :value="dateGetter()" />
@Shyam-Chen I could be wrong but I believe there is a distinct difference between that and what I'm asking for. A getter normalized to a ref using toRef
calls the getter itself each time .value
is accessed. Which would make this work differently in a few ways
- If you never used the value the getter would never be called (possibly because another prop or user interaction is needed)
- When you access
.value
on a getter that is normalized to a ref usingtoRef
the getter is called each time. Which means in my example above if the label was "live" the outcome would be very different. In this example if you passed the getter() => new Date()
the date could update every second. If you called the getter when assigning the prop like:date="dateGetter()"
it would not.
<template>
<span>{{ value.toLocaleString() }}</span>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
const { date } = defineProps<{
date: Date
}>()
const label = ref(date.toLocalString())
setTimeout(() => label.value = date.value.toLocalString(), 1000)
</script>
Use computed
?
<script lang="ts" setup>
import { ref, computed } from 'vue';
import DateLabel from './DateLabel.vue';
const dateConst = new Date();
const dateRef = ref(new Date());
const dateGetter = computed(() => new Date());
</script>
<template>
<DateLabel :value="dateConst" />
<DateLabel :value="dateRef" />
<DateLabel :value="dateGetter" />
</template>
DateLabel.vue
:
<script lang="ts" setup>
const props = defineProps<{
value: Date;
bar?: number;
}>();
</script>
<template>
<div>{{ value.toLocaleString() }}</div>
</template>
@Shyam-Chen There are a few key differences between computed and getters which are outlined quite well in https://github.com/vuejs/core/pull/7997. Both methods you've suggested are possible and useful. However they are functionally and performantly differently than being able to pass a getter that is then normalized in the receiving component.
Props basically have the type MaybeRef because you can bind either a static value or a ref
This may be a misunderstanding. You cannot pass a ref, props do not have a MaybeRef
type. The ref will be unwrapped in the parent's render function before passing it to the child component (when using templates). What happens, essentially, is this:
// code generated by the SFC compiler
render: () => h(DateLabel, { date: unref(dateRef) })
So the child never receives the ref, it always receives the value - a Date
. So a prop defined as type Date
can only receive a Date
, not a Ref<Date>
. Unwrapping happens in the parent's render function/template, not in the child's props. (In a manually written render function, you would have to do the unwrapping yourself).
To do a similar thing with a getter, you would do what @Shyam-Chen initially proposed - you would evaluate the getter in the template.
<DateLabel :date="dateGetter()" />
Now, if you want the getter to be evaluated in the child, only when the prop is actually used there, then you would need to write your component to support that.
<script lang="ts" setup>
import { ref } from 'vue'
const { date } = defineProps<{
date: Date | () => Date
}>()
</script>
<template>
{{ typeof date === 'function' ? date() : date }}
</template>
I kind of understand that you want this to happen automatically, but I'm not sure where and when.
We can't "unwrap" getters in the parent automatically like we do refs, because getters are just functions - and so far, we do allow passing a function as a prop value to a child. If we decided to suddenly "unwrap" (call) all functions we pass as props, we would certainly break lots and lots of userland code, so this is not going to happen.
Similarly, unwrapping it with some "magic" in the props in the child on each access would break existing userland code all the same.
@pleek91 Can you probvide feedback to what I layed out here? Otherwise I would close this request as it's not really possible in the way that I understand your request to work.
@LinusBorg thanks for the detailed response. That totally makes sense. I guess I'm suggesting that when the template is converted into a render function to use toValue
rather than unwref
. But you're right that would break things.
I was thinking about this from a pure types perspective where currently if you're using typescript the type for a prop that is a Date
is effectively MaybeRef<Date>
because you can bind Date | Ref<Date>
and if you tried to pass () => Date
that wouldn't be valid. So from that perspective allowing the type that you can bind to include the getter and using toValue
rather than unref
would be a non breaking change. BUT I wasn't considering non typescript users or typescript users who have a prop that is indeed supposed to be a function.
In my project I am currently doing what you suggested by making a MaybeGetter<T> = T | () => T
that I use for props. Then I'm using toValue
or toRef
in my components rather than typeof date === 'function' ? date() : date
. Which is working well but I did have to update quite a few components in order to support that functionality. So this feature request was maybe short sighted based on some frustration.
Thanks again for the detailed explanation!