Skip to content

prop sugar #229

Closed
Closed
@patak-dev

Description

@patak-dev

I want to avoid commenting on the Ref Sugar RFC about this because it may interfere in the discussion about ref:. I tried to find if the following proposal was discussed before but I couldn't find public discussions about it. Sorry for the noise if this was already analyzed.

Context

In the RFC it is stated that

... we believe ref:'s ergonomics value outweighs the cost by a fair margin. This is also why we are limiting this proposal to ref: only, since ref access is the only problem that requires alternative semantics to solve.

I think that it is on point, but there is also an asymmetry in the usage of props in the template and in the <script>. For simple components, we would end up with:

<script setup="props">
  export default { 
    props: {
      num: { type: Number, default: 4 }
    }
  }
  ref: multiplier = 2
</script>
<template>
  <p @click="multiplier++">{{ `${num} by ${multiplier} = ${num*multiplier}` }}</p>
</template>

With ref sugar, we can move the onClick handler to the script without the .value. But if we want to move the multiplication out of the template, we can not directly copy it in a computed, because we need to add the props.

<script setup="props">
  import { computed } from 'vue' 
  export default { 
    props: {
      num: { type: Number, default: 4 }
    }
  }
  ref: multiplier = 2
  ref: result = computed( () => props.num * multiplier )
</script>

We could convert the prop to a ref, taking care not to lose reactivity while destructuring (this is also something that we need also when passing the props to composables). We also need to explain what toRefs is achieving and the prop name needs to be repeated.

<script setup="props">
  import { computed, toRefs } from 'vue' 
  export default { 
    props: {
      num: { type: Number, default: 4 }
    }
  }
  ref: ({ num } = toRefs(props))
  ref: multiplier = 2
  ref: result = computed( () => num * multiplier )
</script>

The DX also suffers from extra boilerplate needed in this case. If ref: ends up being adopted, I think that having better ergonomics for props could also outweighs the cost of adding new syntax.

Proposal

Following the same model as ref:, we introduce a new label prop: to declare components props:

<script setup>
  import { computed } from 'vue'
  prop: num = 4
  ref: multiplier = 2
  ref: result = computed( () => num * multiplier )
</script>

When a prop is defined in this way, you can directly use its value in the template and in the script, and reactivity works properly.

Edited:
We could add a defineProp type-only helper to support complex validation of props:

<script setup>
import { defineProp } from 'vue'

prop: num = 4

prop: msg = defineProp({
  type: String,
  required: true
  validator: (val) => ![""].includes(val)
})
</script>

Implementation

  • When a variable is declared using prop:, it is aggregated to the props block of the component.
  • Every place where the variable is used it is replaced with props.num.
  • In the same way that with ref:, if you use $num it is replaced with toRef(props,'num'), so you get a ref that you can pass to composables. If a different syntax is pushed to get the reference from ref:, that will be the syntax for prop: too (the important part is that they work in the same way).

Benefits

  • Boilerplate is greatly reduced
  • Code that uses props can be moved from template and script without modifications to a computed
  • Getting a ref out of any reactive value (ref or prop) works in the same way (using $var, no need to explain toRefs and why reactivity is lost when destructuring right away when learning)
  • If you need to lift state (a ref) to become a prop, you can change ref: with prop: and refactoring is easier because you do not need to add props. and toRef in all the places the prop is being used.
  • props related to different features can be moved next to the code that uses them, making refactoring easier also for props (like splitting a component for example). It is the same principle used to justify ref, computed instead of the options API
  • Using prop: is optional, if you want a custom validator for example you can declare that props in the normal props block
  • We can use the same scheme for declaring default values and variable types used by svelte for props (looks like typescript support works fine for them now)

Cons

  • Cost in tooling to support the new syntax (it should be similar to the cost of ref:).
  • Extra syntax to explain, same as with ref:

Alternative

  • In case using prop: wants to be avoided, export let could be used to declare props as svelte does. But I think that prop: plays better with ref: for Vue.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions