Description
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 withtoRef(props,'num')
, so you get a ref that you can pass to composables. If a different syntax is pushed to get the reference fromref:
, that will be the syntax forprop:
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:
withprop:
and refactoring is easier because you do not need to addprops.
andtoRef
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 thatprop:
plays better withref:
for Vue.