Skip to content

Difficulty documenting Vue 3 and avoiding a schism #41

@chrisvfritz

Description

@chrisvfritz

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 the beforeCreate and created 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 in created, it will make more intuitive sense that it's not yet available in the create 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 to create (see proposal 1).

  • Removes the only naming inconsistency between the option and function versions of an API. For example, computed and watch correspond to Vue.computed and Vue.watch, but created and mounted correspond to Vue.onCreated and Vue.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 in import { 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 vs data/computed/methods, by having everyone start with create from the beginning. With everyone already familiar with and using the create 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 like data - 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 the create function, they would see an object with a value rather than the value directly. They won't understand exactly why Vue.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 of this.$refs/context.refs).

  • When users/plugins add properties to the prototype or to this in onBeforeCreate, users can rely on those properties being available on the context object in create.

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 in onCreated, $refs populated in onMounted, 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 for render functions though, right? Don't they still have access to this?

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!

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