Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support MaybeRefOrGetter as the type for passing props #8842

Closed
pleek91 opened this issue Jul 25, 2023 · 7 comments
Closed

Support MaybeRefOrGetter as the type for passing props #8842

pleek91 opened this issue Jul 25, 2023 · 7 comments
Labels
✨ feature request New feature or request

Comments

@pleek91
Copy link

pleek91 commented Jul 25, 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>
@pleek91 pleek91 added the ✨ feature request New feature or request label Jul 25, 2023
@Shyam-Chen
Copy link
Contributor

<DateLabel :value="dateGetter()" />

@pleek91
Copy link
Author

pleek91 commented Jul 26, 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>

@Shyam-Chen
Copy link
Contributor

Shyam-Chen commented Jul 27, 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>

@pleek91
Copy link
Author

pleek91 commented Jul 27, 2023

@Shyam-Chen There are a few key differences between computed and getters which are outlined quite well in #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.

@LinusBorg
Copy link
Member

LinusBorg commented Aug 1, 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.

@LinusBorg
Copy link
Member

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

@pleek91
Copy link
Author

pleek91 commented Aug 8, 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!

@pleek91 pleek91 closed this as completed Aug 8, 2023
@github-actions github-actions bot locked and limited conversation to collaborators Sep 6, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
✨ feature request New feature or request
Projects
None yet
Development

No branches or pull requests

3 participants