Skip to content

Composition functions support and this value changes #416

Open
@ktsn

Description

@ktsn

Summary

  • Composition functions are available in class property initializers by wrapping setup helper.
    • Class property initializers are handled in setup function under the hood.
  • Only $props (and its derived prop values), $attrs, $slots and $emit are available on this in class property initializers.

Example:

<template>
  <div>Count: {{ counter.count }}</div>
  <button @click="counter.increment()">+</button>
</template>

<script lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { Vue, setup } from 'vue-class-component'

function useCounter () {
  const count = ref(0)

  function increment () {
    count.value++
  }

  onMounted(() => {
    console.log('onMounted')
  })

  return {
    count,
    increment
  }
}

export default class Counter extends Vue {
  counter = setup(() => useCounter())
}
</script>

Details

Prior to v7, class component constructor is initialized in data hook to collect class properties. In v8, it will be initialized in setup hook so that the users can use composition function in class property initializer.

The above class component definition is as same as following canonical component definition.

function useCounter () {
  const count = ref(0)

  function increment () {
    count.value++
  }

  onMounted(() => {
    console.log('onMounted')
  })

  return {
    count,
    increment
  }
}

export default {
  setup() {
    return { counter: useCounter() }
  }
}

setup helper

Wrapping a composition function with setup helper is needed because we need to delay the invocation of the composition function. Let's see the following example:

function usePost(postId) {
  const post = ref(null)

  watch(postId, async id => {
    post.value = await fetch('/posts/' + id)
  }, {
    immediate: true
  })

  return {
    post
  }
}

class App extends Vue {
  postId = '1'

  // DO NOT do this
  post = usePost(toRef(this, 'postId'))
}

In the above example, this.postId will be referred by watch helper to track reactive dependencies immediately but it is not reactive value at that moment. Then the watch callback won't be called when postId is changed.

setup helper will delay the invocation until this.postId become a proxy property to the actual reactive value.

setup unwrapping

As same as setup in the Vue core library, setup in Vue Class Component unwraps ref values. The unwrapping happens shallowly:

// The returned value is:
// { 
//    count: { value: 0 },
//    nested: {
//      anotherCount: { value: 1 }
//    }
// }
function useCount() {
  const count = ref(0)
  const anotherCount = ref(1)

  return {
    count,
    nested: {
      anotherCount
    }
  }
}

class Counter extends Vue {
  // counter will be:
  // { 
  //    count: 0, <-- unwrapped
  //    nested: {
  //      anotherCount: { value: 1 }
  //    }
  // }
  // The shallow ref (count) is unwrapped while the nested one (anotherCount) retains
  counter = setup(() => useCount())
}

In addition, if you return a single ref in setup helper, the ref will also be unwrapped:

// The returned value is: { value: 42 }
function useAnswer() {
  const answer = ref(42)
  return answer
}

class Answer extends Vue {
  // answer will be just 42 which is unwrapped
  answer = setup(() => useAnswer())
}

Available built in properties on this

Since the class constructor is used in setup hook, only following properties are available on this.

  • $props
    • All props are proxied on this as well. (e.g. this.$props.foo -> this.foo)
  • $emit
  • $attrs
  • $slots

Example using $props and $emit in a composition function.

function useCounter(props, emit) {
  function increment() {
    emit('input', props.count + 1)
  }

  return {
    increment
  }
}

export default class App extends Vue {
  counter = setup(() => {
    return useCounter(this.$props, this.$emit)
  })
}

Alternative Approach

Another possible approach is using super class and mixins.

import { ref } from 'vue'
import { setup } from 'vue-class-component'

const Super = setup((props, ctx) => {
  const count = ref(0)

  function increment() {
    count.value++
  }

  return {
    count,
    increment
  }
})

export default class App extends Super {}

Pros

  • Can define properties directly on this.

Cons

  • Need duplicated props type definition.

    // Props type definition for setup
    interface Props {
      foo: string
    }
    
    const Super = setup((props: Props) => { /* ... */ })
    
    export default class App extends Setup {
      // Another definition for foo prop to use it in the class
      @Prop foo!: string
    }

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions