-
-
Notifications
You must be signed in to change notification settings - Fork 8.9k
Description
The Problem
Many on the team would like the functions API to become the new recommended way of writing components in Vue 3. And even if we did not officially recommend it as the standard, many users would still gravitate toward it for its organizational and compositional advantages. That means a schism in the community is inevitable with the current API.
I've also been experimenting with how we might document Vue 3 and have been really struggling. I think I finally have to admit that with the current, planned API, making Vue feel as simple and elegant as it does now is simply beyond my abilities.
Proposed solution
I've been experimenting with potential changes to the API, outlining affected examples to create a gentler and more intuitive learning path. My goals are to:
- Make the recommended API feel intuitive and familiar.
- Make changes we're making feel like a logical simplification or extension of our current strategy, rather than a radical change in direction.
- Reduce the feeling of there being two different ways of writing components.
- Reduce the number of concepts users have to learn.
- Reduce the frequency and severity of context switching.
Finally, I think have a potential learning path that is worth attention from the team, though you may want to read my explanations for the proposed API changes before checking it out.
Proposed API changes
1. Rename setup
to create
Example
Vue.component('button-counter', {
props: ['initialCount'],
create(props) {
return {
count: props.initialCount
}
},
template: `
<button v-on:click="count++">
You clicked me {{ count }} times.
</button>
`
})
Advantages
-
Avoid introducing a new concept with
setup
, since we already have a concept of instance creation with thebeforeCreate
andcreated
lifecycle functions. -
With
create
, it's more obvious and easier to remember when in the lifecycle the function is called. -
Since
this
is first available increated
, it will make more intuitive sense that it's not yet available in thecreate
function.
Disadvantages
- Downsides related to autocomplete and confusion with lifecycle functions can be resolved with proposal 2.
2. Rename lifecycle function options to begin with on
Example
new Vue({
el: '#app',
onCreated() {
console.log(`I've been created!`)
}
})
Advantages
-
Removes any confusion or autocomplete conflicts if
setup
is renamed tocreate
(see proposal 1). -
Removes the only naming inconsistency between the option and function versions of an API. For example,
computed
andwatch
correspond toVue.computed
andVue.watch
, butcreated
andmounted
correspond toVue.onCreated
andVue.onMounted
. -
Users only have to make the context switch once, when going from Vue 2 to Vue 3, rather than every time they move between options and functions.
-
Better intellisense for lifecycle functions, because when users type the
on
inimport { on } from 'vue'
, they'll see a list of all available lifecycle functions.
Disadvantages
- None that I can see.
3. Consolidate 2.x data
/computed
/methods
into create
and allow its value to be an object just like data
currently
Example
const app = new Vue({
el: '#app',
create: {
count: 0,
doubleCount: Vue.computed(() =>
return app.count * 2
),
incrementCount() {
app.count++
}
}
})
Vue.component('button-counter', {
props: ['initialCount'],
create(props) {
const state = {
count: props.initialCount,
countIncrease: Vue.computed(
() => state.count - props.initialCount
),
incrementCount() {
state.count++
}
}
return state
},
template: `
<button v-on:click="incrementCount">
You clicked me {{ count }}
({{ initialCount }} + {{ countIncrease }})
times.
</button>
`
})
Advantages
-
It's easier for users to remember which options add properties to the instance, since there would only be one:
create
. -
Users don't need to be more advanced to better organize their properties. This one change provides the vast majority of the organizational benefit, without the complexity that can arise once you get into advanced composition.
-
New users won't have to learn methods as a separate concept - they're just functions.
-
It's even less code and fewer concepts than the current status quo.
-
Prevents the larger rift of people using
create
vsdata
/computed
/methods
, by having everyone start withcreate
from the beginning. With everyone already familiar with and using thecreate
function, sometimes moving more options there for organization purposes (e.g.watch
,onMounted
, etc) will be a dramatically smaller leap. -
Makes the transition to a
create
function feel more natural, both for current users ("oh, it's just likedata
- when it's a function, I just return the object") and new users ("oh, this is just like what I was doing before, except I return the object"). -
Although
Vue.computed
will return a binding, users won't have to worry about learning the concept of bindings for the entirety of Essentials. Only once we get to advanced composition that splits features into reusable functions will it become relevant, because then you have to worry about whether you're passing a value or binding.
Disadvantages
-
If users decided to log a computed property (e.g.
console.log(state.countIncrease)
) inside thecreate
function, they would see an object with avalue
rather than the value directly. They won't understand exactly whyVue.computed
returns this until they're introduced to bindings, but I don't see it as a significant problem because it won't stop them from getting their work done. -
When doing something very strange, like trying to immediately use the setter on a computed property inside
create
, the abstraction of a binding would leak. However, if we think this is likely to actually happen, I believe it could be resolved by emitting a warning on the setter of bindings, since I believe that's always likely to be a mistake.const state = { count: 0, doubleCount: Vue.computed( () => state.count * 2, newValue => { state.count = newValue / 2 } ) } // This will not work, because `doubleCount` has not yet // been normalized to a reactive property on `state`. state.doubleCount = 2 return state
4. Make context
the same exact object as this
in other options
Example
Vue.component('button-counter', {
create(props, context) {
return {
map: context.$parent.map
}
},
onCreated() {
console.log(this.$parent.map)
},
template: '...'
})
Vue.component('username-input', {
create(props, context) {
return {
focus() {
context.$refs.input.focus()
}
}
},
onMounted() {
console.log(this.$refs.input)
},
template: '...'
})
Advantages
-
Avoids a context switch (no pun intended 😄) when moving between options and the
create
function, because properties are accessed under the same name (e.g.this.$refs
/context.$refs
instead ofthis.$refs
/context.refs
). -
When users/plugins add properties to the prototype or to
this
inonBeforeCreate
, users can rely on those properties being available on thecontext
object increate
.
Disadvantages
-
Requires extra documentation to help people understand that
this
===context
, and that properties are added/populated at different points in the lifecycle (e.g. props and state added inonCreated
,$refs
populated inonMounted
, etc). We'll probably need a more detailed version of the lifecycle diagram with these details (or whatever the reality ends up being). -
For TypeScript users, every plugin that adds properties to the prototype (e.g.
$router
,$vuex
) would require extending the interface of the Vue instance. I think they probably want to do this already forrender
functions though, right? Don't they still have access tothis
?
5. Reconsider staying consistent with object-based syntax in the arguments for the function versions of the API?
This one is more of a question than an argument. We have a lot of inconsistencies between the object-based and function-based APIs. For example, when a computed property takes a setter, it's a second argument:
Vue.computed(
() => {}, // getter
() => {} // setter
)
rather than providing an object with get
and set
, like this:
Vue.computed({
get() {},
set() {}
})
It's my understanding that these changes were made for the performance benefits of monomorphism. However, they have some significant disadvantages from a human perspective:
-
They force users to learn two versions of every API, rather than being able to mostly copy/paste when refactoring between options and functions, creating more work and making them feel like a significant context switch.
-
They create code that's less explicit and less readable, since either intellisense or comments are necessary to provide more information on what these arguments actually do.
As a starting place, could we create some benchmarks from realistic use cases so we can see exactly how much extra performance we're getting from monomorphism? That could make it easier to judge the pros and cons.
Thoughts?
@vuejs/collaborators What does everyone think about these? They include some big changes, but I think they would vastly simplify the experience of learning and using Vue. I'm also very open to alternatives I may have missed!