question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Composition functions support and `this` value changes

See original GitHub issue

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
    }
    

Issue Analytics

  • State:open
  • Created 3 years ago
  • Reactions:20
  • Comments:26 (13 by maintainers)

github_iconTop GitHub Comments

5reactions
lgarczyncommented, Feb 18, 2021

Couldn’t the setup function simply be static class function?

5reactions
Mikilll94commented, Nov 20, 2020

@ktsn

<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>

For me this approach looks too verbose and a little bit strange. Why are we assiging setup function to counter variable. Why can’t we just use the setup function in a similar way as lifecycle hooks?

<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 {
  setup(props) {
    const { count, increment } = useCounter();
    return {
        count,
        increment
    };
  }
}
</script>
Read more comments on GitHub >

github_iconTop Results From Across the Web

Composition of Functions - Nool - Ontario Tech University
The composition of two functions f and g is the new function h, where h(x) = f(g(x)), for all x in the domain...
Read more >
3.5: Composition of Functions - Algebra
Function composition is only one way to combine existing functions. Another way is to carry out the usual algebraic operations on functions, ...
Read more >
Compose Functions: Definition, Properties & Examples
In mathematics, the composition of a function is an action in which two functions, 'a and 'b', are combined to produce a new...
Read more >
Function composition
In mathematics, function composition is an operation ∘ that takes two functions f and g, and produces a function h = g ∘...
Read more >
Composition of Functions - Definition, Properties and ...
Properties of Function Compositions ... Few more properties are: The function composition of one-to-one function is always one to one. The function composition...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found