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

rfc-0013: Destructuring props #142

Open
mspoulsen opened this issue Mar 6, 2020 · 8 comments
Open

rfc-0013: Destructuring props #142

mspoulsen opened this issue Mar 6, 2020 · 8 comments

Comments

@mspoulsen
Copy link

Hi,

I am a big fan of the new Composition API and the wonderful typescript support and code reuse that comes with it. However, there are a couple of thing I don't quite understand yet.

I Vue2 it was easy and straightforward to create a computed property based on a prop:

export default {
  props: {
    name: String

  },
  computed: {
    greeting() {
      return `Hello ${name}!`
    }
  }
}

And the intuitive way to do it in Vue3 would be:

setup(props) {
    const { name } = props
    const greeting = computed(() => `Hello ${name}!`)

    return {
      greeting
    }
  }
}

However, this does not work because if we destructure props we lose reactivity! This was a bit of a gotcha for me 🙂

So "workarounds" would be either to aviod destructuring:

setup(props) {
    const greeting = computed(() => `Hello ${props.name}!`)

    return {
      greeting
    }
  }
}

...or to use toRefs:

setup(props) {
    const { name } = toRefs(props)
    const greeting = computed(() => `Hello ${name.value}!`)

    return {
      greeting
    }
  }
}

Now, my question is: why are props now passed in as refs to begin with? That would mean that we could use the "intuitive" code without having to remember not to destructure.

Thanks in advance! 🙂

@jacekkarczmarczyk
Copy link

That would mean that we could use the "intuitive" code without having to remember not to destructure

But you would have to use .value everywhere you want to use some prop

const greeting = computed(() => `Hello ${props.name}!`)

vs

const greeting = computed(() => `Hello ${name.value}!`)

@mspoulsen
Copy link
Author

I know but I think it definitely outweighs the drawback of losing reactivity or having to use toRefs every time. I don't mind having to use .value at all. Especially when using TS.

@LinusBorg
Copy link
Member

LinusBorg commented Mar 6, 2020

When we designed this new API, we assumed that the majority of use cases would use reactive() and ref() was a necessary tool to work with some less common cases that people might not need to touch that often.

Consequently, it would have been odd to make props an object that returns refs, as then refs would be everywhere.

As we now have collected a lot of hands-on experience with the new API, it seems that this initial assumption was wrong: refs are required or at least more useful in a lot of situations, and much more often than we anticipated.

So far that a lot of people, myself included, see refs as the default reactivity mechanism when working with the API and fall back to reactive only in a few scenarios.

In light of that, having props defined as a plain object of refs would make more sense indeed.

On the other hand, that only works if the list of properties on that object are static, as we would not be able to watch property additions / removals on a plain object of refs.

But props can be undefined initially and passed with a value form the parent later. We might think: "ok, so let's pre-populate the props object's properties with all of the props defined in the opros options!" then we would always have refs for all props, and those that were not passed have a .value of undefined.

But that also won't work, as we now support dynamic Props in Vue 3 - you can omit the props: option completely and have all attribues/props passed to the component end up in the setup funcion's props argument.

That means we have no reliable way to predefine these properties on a plain props object, which means the props object has to be reactive.

@mspoulsen
Copy link
Author

mspoulsen commented Mar 6, 2020

@LinusBorg Thanks for the in-depth answer!

My feeling as a new user is: why even have the "reactive" option? Why not only provide ref? That would alleviate a lot of confusion in my opinion and developers would be using the same patterns. So, I am happy to hear that you have come to the same conclusion basically.

The big problem here with destructuring is that it is a silent error. You get no warning or anything. Reactivity has just disappeared without you knowing! Errors like that lead to huge headaches so I think they should be avoided at all costs.

Also with all the recommendations of using "composition funcitons" like:

const { x, y } = useMousePosition()

...then I gets really(!) confusing that things start to fail when you destructure props!

The fact that ref props would not work with additions / removals is only a minor drawback. I don't remember ever having removed a property dynamically from props.

The other reason you mention I don't fully understand. Maybe that feature could be left out?

I hope you find some sort of solution. My personal dream scenario would be:

  1. no "reactive"
  2. props are refs

I don't have the insight you have to be able to pinpoint the drawbacks and what is possible and what is not. These are just my initial feelings as a new consumer of the compositional api :)

@ycmjason
Copy link

ycmjason commented Mar 6, 2020

The big problem here with destructuring is that it is a silent error. You get no warning or anything. Reactivity has just disappeared without you knowing! Errors like that lead to huge headaches so I think they should be avoided at all costs.

I am pretty sure there will be linting rules that warns about destructing props.

However, I still think it would be nicer if props contains refs in the first place. I don't mind doing toRefs too much tho.

@LinusBorg
Copy link
Member

LinusBorg commented Mar 6, 2020

The big problem here with destructuring is that it is a silent error. You get no warning or anything. Reactivity has just disappeared without you knowing! Errors like that lead to huge headaches so I think they should be avoided at all costs.

This was already true in Vue 2. It's just a more common to come across it in Vue 3.

data() {
  return {
    myReactiveProperty: 'foo',
    myReactiveObject: {
      bar: 'baz'
    }
  }
},
provide() {
 const { myReactiveProperty } = myReactiveProperty  
 const { bar } = this.myReactiveObject 

  return {
    myContent: {
      myReactiveProperty, // will not be reactive
      bar, // will not be reactive
     }
  }
},

As Jason said, linters can help here. Also, note that this is independent of ref or reactive, basically.

const ref1 = ref({ foo: 'bar' })
const { foo } = ref1.value // foo will not be reactive

const reactive1 = reactive({ foo: 'bar' })
const { foo } = reactive1 // foo will not be reactive

The limitation is basically the same as this in pure JS:

const obj1 = {foo: 'bar' }
const { foo } = obj1

obj1.foo = 'baz'

console.log(foo) // => still return 'bar'

So it's rather a Javascript limitation than a Reactivity limitation.

Refy only solve the problem when used within the object:

const obj1 = {foo: ref('bar') }
const { foo } = obj1

obj1.foo.value = 'baz'

console.log(foo.value) // => 'baz'

but then you can't have unwrapping.

And I think realizing that, we can come up with hopefully relatively straightforward rules of thumb:

  1. if you destructure something reactive
    a. outside of a computed or watch, it won't be reactive (just like vanilla JS, see example above)
    b. inside of a computed or watch, everything will be reactive in all cases.
  2. If destructuring something non-reactive, it's - well - non-reactive,
    2.1 unless the property contains a ref, which then is responsible for the reactivity.

Furthermore, you say:

The fact that ref props would not work with additions / removals is only a minor drawback. I don't remember ever having removed a property dynamically from props.

They might not be common for you but you're not the only user of Vue ;) And you also might be underestimating how often you might need it.

Simple example:

<myComponent  v-if="user" :user="user" />
<myComponent  v-else />

That's enough to break a plain props object as the user prop would only be added later (The myComponent instance here would be re-used).

Yes, you can work around it by inmitializing user as an empty object but the component might not want to receive a placeholder item.

@mspoulsen
Copy link
Author

Ok, I understand that it is complex. Thanks a lot 👍

@so1ve
Copy link
Member

so1ve commented May 12, 2023

Update: prop destruction is now supported in script setup!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants