Subscribe on changes!

Component setup using ES6 class works out of the box in Vue 3, why not officially support it!

avatar
Mar 5th 2021

What problem does this feature solve?

I just discovered that we could use ES6 class for creating components instead of the Composition API setup function. The best thing is it works out of the box in Vue 3 without any dependency on vue-class-component or other libraries! Even the experimental template interpolation service in Vetur is working flawlessly with it!!!

Surprisingly I couldn't find this way of creating components in any of Vue 3 documentation. Not sure why, or am I missing something!? With official support, we could make the experience even better.

Why use ES6 class:

In the current way of creating a component using setup function & composition API, users are encouraged to create closure functions. When the number of instances of a component is minimal this doesn't make much difference. But when there are so many instances of the component, multiples of those many functions objects would be created, which doesn't look ideal.

For instance, 1000 instances of the following component would create 1000 getUserRepositories function objects. Which is overkill.

import { fetchUserRepositories } from '@/api/repositories'
setup (props) {
  let repositories = []
  const getUserRepositories = async () => {
    repositories = await fetchUserRepositories(props.user)
  }
  return {
    repositories,
    getUserRepositories
  }
}

What does the proposed API look like?

This is how we could create a Counter component using ES6 class & composition API. And it works with the current release of Vue 3!

<template>
  <div class="counter">
    Counter: {{ valueInternal }}<br />
    <button type="button" @click="onAdd">Add</button>&nbsp;
    <button type="button" @click="onSubtract">Sub</button>
  </div>
</template>

<script lang="ts">
import { defineComponent, ref, Ref } from "vue";

class Counter {
  valueInternal: Ref<number>;

  constructor(val: number) {
    this.valueInternal = ref(val);

    // Guess following binding & convertion to own properties are needed because of hasOwn check @ https://github.com/vuejs/vue-next/blob/master/packages/runtime-core/src/componentPublicInstance.ts#L293
    // We must be able to remove it with official support
    this.onAdd = this.onAdd.bind(this);
    this.onSubtract = this.onSubtract.bind(this);
  }

  onAdd() {
    this.valueInternal.value++;
  }

  onSubtract() {
    this.valueInternal.value--;
  }

  someOtherMethod() {
    // Statements
  }
}

export default defineComponent({
  props: {
    value: Number
  },
  setup: props => new Counter(props.value || 0)
});
</script>

I have committed a dummy project with working example @ https://github.com/sreenaths/vue3-composition-class

What could we do through official support:

There could be more, but the following are some of the items that came to my mind.

  1. Direct default export of the class from inside the script tag. export default Counter instead of this working snippet export default defineComponent(props => new Counter(props));.
  2. Some way to bypass hasOwn check explained above in Counter constructor so that inherited properties can be accessed from inside the template - Can we use decorators for that?
  3. Should we have an interface for type-checking?
  4. When directly exporting the Class, can we provide some way to pass props definition?
  5. Documentation.
avatar
Mar 6th 2021

Hey @sreenaths

thanks for doing this write-up! It's kinda cool that this works out of the box for the most part.

However, I don't see a real chance for this to become part of core.

Apart from style-preferences, the only advantage that you can bring up is that of avoiding duplicated functions because of their closures. You seem to think that we can avoid this by using Classes and their prototype inheritance.

But unfortunately, that's not the case. Binding class methods to this (.bind(this)) is necessary regardless of the hasOwn check you point to, because we usually // often want to pass these methods as standalone functions to i.e. v-on as listeners. Without binding, class methods used outside of their original context lose this context.

And .bind create a new function in memory, so the advantage that your perceive in terms of memory doesn't exist.

Besides that (invalid) advantage, using a class has no real objective benefits over plain functions that I can see. So there's really no reason to burden users and the docs with another style of doing things.

If you want to continue this discussion regardless of the points I raised, or have new ones to bring up, please use the discussion section in the rfcs repo.

avatar
Mar 6th 2021

Thanks @LinusBorg

I agree that I should have demonstrated it without using bind. The intention behind the following lines was just to bypass hasOwn check. Technically .bind is irrelevant.

    this.onAdd = this.onAdd.bind(this);
    this.onSubtract = this.onSubtract.bind(this);

Following is the same implementation without using bind, and it works. Im using Object.assign to bypass hasOwn check.

class Counter {
  valueInternal: any;

  constructor(val: number) {
    this.valueInternal = ref<number>(val);

    // Guess this is needed because of hasOwn check @ https://github.com/vuejs/vue-next/blob/master/packages/runtime-core/src/componentPublicInstance.ts#L293
    // We must be able to remove it with official support
    Object.assign(this, {
      onAdd: this.onAdd,
      onSubtract: this.onSubtract
    });
  }

  onAdd() {
    this.valueInternal++;
  }

  onSubtract() {
    this.valueInternal--;
  }

  someOtherMethod() {
    // Statements
  }
}

Thanks for pointing to the discussion section.

avatar
Mar 6th 2021
avatar
Mar 6th 2021

I think you kind of missed the point about how bind() is required regardless of hasOwn.

Here's a plan JS demo:

class Store {
  constructor() {
    this.data = 'hello'
    Object.assign(this, {
    setData: this.setData
    })
  }
  
  setData(v) { this.data = v }
}
const store = new Store()
const setData = store.setData
setData('goodbye') // TypeError: Cannot set property 'data' of undefined 

console.log(store.data) 

without binding the function (which creates a new function in memory), you can't use methods decoupled from the instance - which is something we need to be able to do in order to pass one of these methods as a listener to a child component, for example (and other use cases).

also posted this to the discussion thread, so we can continue there.