rfcs icon indicating copy to clipboard operation
rfcs copied to clipboard

rfc-0013: Destructuring props

Open mspoulsen opened this issue 5 years ago • 8 comments
trafficstars

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 :slightly_smiling_face:

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! :slightly_smiling_face:

mspoulsen avatar Mar 06 '20 09:03 mspoulsen

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}!`)

jacekkarczmarczyk avatar Mar 06 '20 09:03 jacekkarczmarczyk

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.

mspoulsen avatar Mar 06 '20 10:03 mspoulsen

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.

LinusBorg avatar Mar 06 '20 10:03 LinusBorg

@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 :)

mspoulsen avatar Mar 06 '20 10:03 mspoulsen

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.

ycmjason avatar Mar 06 '20 12:03 ycmjason

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.

LinusBorg avatar Mar 06 '20 12:03 LinusBorg

Ok, I understand that it is complex. Thanks a lot :+1:

mspoulsen avatar Mar 06 '20 14:03 mspoulsen

Update: prop destruction is now supported in script setup!

so1ve avatar May 12 '23 00:05 so1ve