[Core Team RFC] Reactive Props Destructure #502
Replies: 45 comments 82 replies
-
Not sure what others think or what the best practice is but I prefer not to destructure props on purpose so that I would know if I am dealing with a regular ref or a prop and it also makes it easier to distinguish and have separate variables where I can have <template>
This is a name prop: {{ props.name }}
This is a local variable: {{ name }}
</template>
<script setup>
const props = defineProps(['name'])
const name = ref('')
</script> |
Beta Was this translation helpful? Give feedback.
-
Does the compiler support a code pattern that enables destructuring a few properties and keeping a There's no TS example but I assume everything works the same with the inclusion of a type parameter? const { n } = defineProps<{ n: number }>() Is parameters validation during dev still a thing? |
Beta Was this translation helpful? Give feedback.
-
if i were to hover over const { foo } = defineProps(['foo'])
useFeature(foo) |
Beta Was this translation helpful? Give feedback.
-
That looks awesome but I don't like the fact that we need to use a Getter when passing the prop to a composable in order to retain reactivity. I think many of us do: const props = defineProps({...})
const { foo } = toRefs(props)
useFeature(foo) Now this code starting from 3.3 would be considered legacy/needs-improvements and the new syntax would be: const { foo } = defineProps({...})
useFeature(() => foo) Some things doesn't look right to me:
I believe that this instead of improving the current DX, complicates things and make it more confusing for users with this non-ref reactive variable and the "thunking" pattern. I would personally vote for const { foo } = defineProps(...)
foo.value = 'new value' // ❌ should error
useFeature(foo) // ✅ works
watch(foo, handler) // ✅ |
Beta Was this translation helpful? Give feedback.
-
// vue.config.js
module.exports = {
chainWebpack: (config) => {
config.module
.rule('vue')
.use('vue-loader')
.tap((options) => {
return {
...options,
reactivityTransform: true // Shouldn't it be `propsDestructure: true` ?
}
})
}
} |
Beta Was this translation helpful? Give feedback.
-
This causes a TypeScript error for me:
|
Beta Was this translation helpful? Give feedback.
This comment has been hidden.
This comment has been hidden.
This comment has been hidden.
This comment has been hidden.
-
I highly appreciate the attempt to solve the problem of cumbersome assignment of default values, but I'd like to note that after using this feature with the Vue 3.3 alpha, I have strong reservations against having such a kind of "magic variable" in the I'm not new to Vue or TypeScript, I've been using both since 2016. But still, it was quite confusing to have reactive state that is neither a ref nor a property access. I think this is actually the main problem: that it's such a one-off. It does not align with what Vue usually does. (Of course it does align with what Vue does in templates, but those are a DSL which therefore seems much less problematic to me.) That said, I don't have a better solution to offer, I just wanted to spell out the doubts I have about this after some time of usage and reflection. A less-than-optimal suggestion that crossed my mind: It could be allowed for const { count = ref(0) } = definePropsRefs<{ count: number }>() I acknowledge this is still unwieldy, but it would match expectations with regards to Vue's current behavior, and IMO it's preferrable to |
Beta Was this translation helpful? Give feedback.
-
For me, when I look at the Drawbacks. It's trade off too much just for destructure a variable. |
Beta Was this translation helpful? Give feedback.
-
I hope this will not be removed. I really like it and wouldn't consider the drawbacks that bad. |
Beta Was this translation helpful? Give feedback.
-
We have been using this since it was available and have really come to appreciate the DX improvement. Together with The DX gains in assigning default values are immense, but that was a given and I doubt anyone is against being able to drop While I wasn't fully sold on Reactivity transform (because of the different syntax), this RFC seems to just fit in nicely with what SFC try to do for you as a developer. I encourage people to try this in a real project for a while and not have a knee-jerk reaction from a purely theoretical standpoint. |
Beta Was this translation helpful? Give feedback.
-
I'm surprised this doesn't come with a macro to turn these destructured props back into refs for passing into composables. What's the composables that receive getters supposed to look like? function useSomething(prop) {
const propRef = toRef(prop);
const something = computed(() => propRef.value.map(somethingElse))
} or function useSomething(prop) {
const something = computed(() => toValue(prop).map(somethingElse))
} ? Both seem more verbose and awkward than .value. This seems fine for libraries that want to have generic interfaces as much as possible, but for writing regular code it's not great. We already have Ref, Readonly, MaybeRef and Readonly, now we also get MaybeReadonlyRefOrGetter. And in projects without typescript this just adds another kind of argument to keep track of, or you have to toValue/toRef everything. I feel like this proposal just pushes the awkwardness into composables, where the code is more complex already, instead of removing it. The introduction of getters just makes me feel like there's some regret in choosing I think I would prefer a combined |
Beta Was this translation helpful? Give feedback.
-
Instead of destructure I'd prefer the way vue-macros did with const count = defineProp<number>('count', { default: 1 }) Returning a |
Beta Was this translation helpful? Give feedback.
-
Is there no need to add propsDestructure: true in version 3.3.4? |
Beta Was this translation helpful? Give feedback.
-
Keep both options, by default it will be reactive, but it would be possible to disable it (not at the level of the whole application, but of a specific component) |
Beta Was this translation helpful? Give feedback.
-
const { a } = defineProps<{a: unknown}>()
const b = a
watchEffect(() => {
console.log(a)
console.log(b)
}) |
Beta Was this translation helpful? Give feedback.
-
https://stackblitz.com/edit/vitejs-vite-hkhny8 The only confusion here is what if I have to pass many props to a Update: I meant to use object inside const { foo, bar, baz, qwe, asd, zxc } = defineProps(['foo', 'bar', 'baz', 'qwe', 'asd', 'zxc'])
// or
const { foo, bar, baz, qwe, asd, zxc } = defineProps<{foo: any; bar: any; baz: any; qwe: any; asd: any; zxc: any;}>();
const wholePropObjectWhenItsDestructured = {
foo,
bar,
baz,
qwe,
asd,
zxc
}
const wholePropObjectWithReactive = reactive({
foo: () => foo,
bar: () => bar,
baz: () => baz,
qwe: () => qwe,
asd: () => asd,
zxc: () => zxc
})
// For example I want to keep props objects destructured
// I don't want to pass props object to composable
// How to pass the destructured props in a better way?
useComposable({
foo: () => foo,
bar: () => bar,
baz: () => baz,
qwe: () => qwe,
asd: () => asd,
zxc: () => zxc
}) |
Beta Was this translation helpful? Give feedback.
-
这样做或许是不是会损失性能?如果本身是shallowReactive,这样做会有效果吗?对于新手体验上来说好一些,但是对于老手我相信大部分会觉得直观上会比较反人类,是不是有些过于黑魔法。觉得toRef和toRefs已经能很好的解决问题了,我觉得解构丧失响应性很符合正常js的心智模型,加上去有些不伦不类,有点当初$ref的感觉了,甚至有些方面不如$ref,团队协作$ref每个人可以选择用或者不用,这个直接作用全局就让一些人很难受。 |
Beta Was this translation helpful? Give feedback.
-
So if I get this correct, destructured props will be reactive but not become refs? That actually increases mental overhead imho. Also passing getter function into composables feels inconsistent, to say the least. |
Beta Was this translation helpful? Give feedback.
-
I'm using /**
* // Compare with:
* const {
* n = 123,
* str = 'hello',
* obj = {},
* foo
* } = defineProps(['n', 'str', 'obj', 'foo'])
*
* watch(() => foo, () => {});
*/
const n = defineModel('n');
const str = defineModel('str', { default: 'hello' });
const obj = defineModel('obj', { default: {} });
const foo = defineModel('foo');
// ✅ works
watch(foo, () => {}); |
Beta Was this translation helpful? Give feedback.
-
Since this feature needs compilation anyway, I am all for making destructured props refs and not magic variables. So this: const { foo = 5 } = defineProps<{ foo?: number}>() becomes const $props = defineProps({ foo: { type: Number } })
const foo = toRef(() => $props.foo ?? 5) Implementation (or compilation) details may vary. Doing this also reduced the work the compiler has to do overall because you are actually defining the ref as such. So no need to search for |
Beta Was this translation helpful? Give feedback.
-
The feature makes the reactivity of vue more complex to me. Here is a example: isReactive(data) // true
watch(data, () => { /* ... */ }) What will happen? From our experience, it just work. Of course, a However, now there is a different ending. It could also throw a error if the complete code is: const { data } = defineProps<{ data: Record<string, any> }>()
isReactive(data) // true
watch(data, () => { /* ... */ }) // ! oops! The code is actually equal to Although there is a error message from I like @Fuzzyma 's idea. It feels like |
Beta Was this translation helpful? Give feedback.
-
Just to clarify cause i am a but confused: |
Beta Was this translation helpful? Give feedback.
-
The fact that one variable can be completely different from another variable with exactly the same type seems wild to me. Abusing typescript like this is a major mistake in my opinion. This might be more convenient, but it will cause people to misunderstand how vue's reactivity works. People are going to wonder why one variable is reactive and the other not when they are both not Refs. Perhaps I'm overreacting but the benefit would be that we don't need to use:
These benefits seem tiny to me to be worth it. Perhaps if the compiler would make everything implicitly reactive and not just props then at least that would be consistent, but this halfway approach will only bring confusion imo. |
Beta Was this translation helpful? Give feedback.
-
In vanilla JavaScript, when specifying default values, is it better to use destructuring or the traditional default property? |
Beta Was this translation helpful? Give feedback.
-
There is a small linting concern here, pardon me if it has been addressed. If I use this new API to set a default for a prop but don't use it in Edit: "hasn't" > "has" |
Beta Was this translation helpful? Give feedback.
-
This SFC based magic compilation may lead to a deeper understanding reactivity of vue. Why can't 'reactive' be deconstructed? The story should be reflected in the doc for beginners. |
Beta Was this translation helpful? Give feedback.
-
@yyx990803 个人对于库作者而言的印象是:库的作者非常优秀,但太轴了。很多开发者在实践中给出的建议很多并未被采纳。 ref 和 reactive 的使用已经在整个社区被热议:‘到底应该使用哪一个’。现在有多了一种看上去跟普通变量无二的响应式数据(虽然只是在最后被编译成 props.prop,而不是真正的新增了一种响应式数据结构)。但是这样真的好吗? 使用上已经让开发者犯难(不单单是 ref 和 reactive 两者的问题,还包括各种奇葩的修改响应式数据的方式),现在连区分响应式数据都困难了。 |
Beta Was this translation helpful? Give feedback.
-
@yyx990803 我最近才开始使用,也是最近才发现这个 RFC , 但评论区已经出现这么多不同的声音。这个一致性问题应该是优先考虑的。这个最终结果就是破坏了一致性。还有就是为啥什么要关闭掉 discussion 而且限制发言。 |
Beta Was this translation helpful? Give feedback.
-
Summary
Introduce a compile-time transform that makes destructured bindings from
defineProps
reactive.This was part of the Reactivity Transform proposal and now split into a separate proposal of its own.
Basic example
With types (works exactly the same):
Motivation
Succinct and native-like syntax for default values and local alias. Big DX improvement over
withDefaults()
.Previously, you can implicitly use declared props in
<template>
, e.g. with{{ foo }}
, but in<script>
using it asprops.foo
. With destructured props the usage becomes consistent.Detailed design
Compilation Rules
The compilation logic is straightforward - the above example is compiled like the following:
Input
Output
Default Values
Users can leverage the native destructure default value syntax to declare default values for props:
Also note that when declaring default value for non-primitive objects, it’s no longer necessary to use a factory function.
Local Renaming
Similarly, destructure with renaming is also supported:
Watch Guard
The compiler will error if a destructured prop is passed to the
watch()
API:Drawbacks
Note: this feature has been implemented and tested as part of Reactivity Transform for quite some time, so we are keeping the motivation and design details short. We want to focus on whether this should be landed as a stable feature in this RFC, so we are trying to be exhaustive about potential drawbacks.
Cannot be passed directly to Composables
Users who are new to this feature could be mistakenly passing a destructured prop into a function and expect it to retain reactivity:
This isn't particularly about destructured props though: we have the same issue with the
props
object. You can just passprops.foo
into a composable and expect it to stay reactive, so this isn't really a new problem.Previously users have to come up with various patterns in order to pass props as refs:
With the
toRef()
enhancement andtoValue()
introduced in #7997, I hope we can consolidate the pattern to:Not Obvious that it's a Prop
Some users have expressed that they prefer seeing
props.foo
which clearly indicates that it is a prop.In small components, we are usually very aware of the props we expect. But in large components, it could be helpful to be able to instantly tell if something is a prop or just a normal variable.
However, with IDE support, one can always jump to definition to see whether something is a prop. So while it may affect readability to some extent, it does not fundamentally affect maintainability.
Interestingly, the same issue had always been present in Options API: everything is grabbed off of
this
, and very few users complained about it. There were some users who opted to go withthis.$props.foo
in order to differentiate props from local bindings, but this seems very uncommon, despite even weaker IDE support for Options API.Potential Confusion for Beginners
Similar to Reactivity Transform, this feature introduces the concept of "compiler-magic-powered reactive binding". It requires the user to understand how reactivity tracking works to understand why this works, and is an exception to the "dot access means tracking" mental model.
This is probably the biggest reservation that we currently have about this feature. But we'd like to provide some counter arguments in favor of shipping it:
Props is a component-only concept, and reactive props only ever exist inside SFCs. Unlike Reactivity Transform, the boundary here is very clear. The magic never leaks into normal JS/TS code or composables.
We already have a lot of "compiler magic" in place, and users have managed to internalize them. For example, ref unwrapping in templates is conceptually almost identical to reactive props.
The key here is whether the compiler magic incurs long term mental overhead. If it can be internalized and becomes muscle memory, then the DX gain will likely be worth it.
Composition API already requires understanding how reactivity tracking works to be used effectively. We can further reduce the chance of confusion by improving the intro parts of docs for Composition API - specifically, explain reactivity tracking earlier rather than leaving it in the advanced section.
Alternatives
definePropsRefs
Essentially sugar for
toRefs(props)
, which allows destructuring props as refs. However, this does not improve the DX when declaring default values.Adoption strategy
This is implemented and shipped as an experimental feature in 3.3 beta and requires explicit opt-in.
Vite
vue-cli
Requires
vue-loader@^17.1.1
Beta Was this translation helpful? Give feedback.
All reactions