[Core Team RFC] Reactive Props Destructure · vuejs/rfcs · Discussion #502 (original) (raw)

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

{{ foo }}

With types (works exactly the same):

const { count } = defineProps<{ count: 0 }>()

Motivation

  1. Succinct and native-like syntax for default values and local alias. Big DX improvement over withDefaults().
  2. Previously, you can implicitly use declared props in <template>, e.g. with {{ foo }}, but in <script> using it as props.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

const { count } = defineProps(['count'])

watchEffect(() => { console.log(count) })

Output

const __props = defineProps(['count'])

watchEffect(() => { console.log(__props.count) })

Default Values

Users can leverage the native destructure default value syntax to declare default values for props:

const { n = 123, str = 'hello', obj = {} } = defineProps(['n', 'str', 'obj'])

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:

const { n: localNumber = 123 } = defineProps(['n'])

Watch Guard

The compiler will error if a destructured prop is passed to the watch() API:

import { watch } from 'vue'

const { foo } = defineProps(['foo'])

watch(foo, () => {}) // ^ compiler error, suggests () => foo

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:

const { foo } = defineProps(['foo'])

useFeature(foo)

This isn't particularly about destructured props though: we have the same issue with the props object. You can just pass props.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:

const props = defineProps(['foo'])

// toRef useComposable(toRef(props, 'foo'))

// computed useComposable(computed(() => props.foo))

// toRefs const { foo } = toRefs(props) useComposable(foo)

With the toRef() enhancement and toValue() introduced in #7997, I hope we can consolidate the pattern to:

// with props object const props = defineProps(['foo']) useComposable(() => props.foo)

// with destructure const { foo } = defineProps(['foo']) useComposable(() => foo)

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 with this.$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:

  1. 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.
  2. 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.
  3. 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

Adoption strategy

This is implemented and shipped as an experimental feature in 3.3 beta and requires explicit opt-in.

Vite

// vite.config.js export default { plugins: [ vue({ script: { propsDestructure: true } }) ] }

vue-cli

Requires vue-loader@^17.1.1

// vue.config.js module.exports = { chainWebpack: (config) => { config.module .rule('vue') .use('vue-loader') .tap((options) => { return { ...options, reactivityTransform: true } }) } }