Subscribe on changes!

Support `MaybeRefOrGetter` as the type for passing props

avatar
Jul 25th 2023

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>
avatar
Jul 26th 2023
<DateLabel :value="dateGetter()" />
avatar
Jul 26th 2023
<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

  1. If you never used the value the getter would never be called (possibly because another prop or user interaction is needed)
  2. When you access .value on a getter that is normalized to a ref using toRef 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>
avatar
Jul 27th 2023

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>
avatar
Jul 27th 2023

@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.

avatar
Aug 1st 2023

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.

avatar
Aug 8th 2023

@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.

avatar
Aug 8th 2023

@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!