BT

Facilitating the Spread of Knowledge and Innovation in Professional Software Development

Write for InfoQ

Topics

Choose your language

InfoQ Homepage News Vue 3.0 Discards Class-Based API for Reusable, Composable Function-Based Approach

Vue 3.0 Discards Class-Based API for Reusable, Composable Function-Based Approach

This item in japanese

The Vue team recently opened an RFC which describes the proposed function-based component API for the upcoming Vue 3. Like React Hooks, the function-based component API seeks to allow developers to encapsulate logic into so-called “composition functions” and reuse that logic across components. Additionally, the new component API would provide better built-in TypeScript type inference support, in ways that the now discarded Class API RFC cannot. The new API for Vue 3 would not substitute, but live next to, the existing Vue 2 component API.

Evan You, Vue’s creator, explains the main motivation behind the proposed function-based component API:

Logic composition is probably one of the most serious problems in terms of scaling projects. (…) Users dealing with different types of projects will run into different needs; some can be easily dealt with using the object-based API, while some cannot. The primary example is:

  1. Large components (hundreds of lines long) with encapsulates multiple logical tasks
  2. The need for sharing the logic of such tasks between multiple components

The encapsulation of logic into composition functions is enabled by the previous Advanced Reactivity API RFC and Dynamic Lifecycle Injections RFC.

The advanced reactivity API provides standalone APIs for creating, observing and reacting to changes in pieces of state. The documentation provides the following basic example, illustrating the state, value, computed, watch API:

import { state, value, computed, watch } from '@vue/observer'

// reactive object
// equivalent of 2.x Vue.observable()
const obj = state({ a: 1 })

// watch with a getter function
watch(() => obj.a, value => {
  console.log(`obj.a is: ${value}`)
})

// a "pointer" object that has a .value property
const count = value(0)

// computed "pointer" with a read-only .value property
const plusOne = computed(() => count.value + 1)

// pointers can be watched directly
watch(count, (count, oldCount) => {
  console.log(`count is: ${count}`)
})

watch(plusOne, countPlusOne => {
  console.log(`count plus one is: ${countPlusOne}`)
})

state creates reactive objects; value creates value pointers which additionally encapsulates reactive primitive values; computed handles reactive computations derived from existing reactive state; and watch allows to run effects when a watched piece of state changes.

The Dynamic Lifecycle Injections RFC introduces APIs for dynamically injecting component lifecycle hooks. The following basic example illustrates the API:

import { onMounted, onUpdated, onDestroyed } from 'vue'

export default {
  created() {
    onMounted(() => {
      console.log('mounted')
    })

    onUpdated(() => {
      console.log('updated')
    })

    onDestroyed(() => {
      console.log('destroyed')
    })
  }
}

The previous code samples expressed that when a Vue component instance is created, the onMounted, onUpdated, and onDestroyed Vue lifecycle hooks will be added to the instance.

The two previously-illustrated RFCs, when combined together, enable the function-based component API. The following function useMouse encapsulates the logic behind tracking the mouse cursor position:

function useMouse() {
  const x = value(0)
  const y = value(0)
  const update = e => {
    x.value = e.pageX
    y.value = e.pageY
  }
  onMounted(() => {
    window.addEventListener('mousemove', update)
  })
  onUnmounted(() => {
    window.removeEventListener('mousemove', update)
  })
  return { x, y }
}

The following function useFetch encapsulates the logic associated to fetching remote data whenever the relevant property (props.id) is changing:

function useFetch(props) {
 const isLoading = value(true)
 const post = value(null)
 
 watch(() => props.id, async (id) => {
   isLoading.value = true
   post.value = await fetchPost(id)
   isLoading.value = false
 })
 
 return {
   isLoading,
   post
 }
}

The previously described functions can be reused and combined into a consumer component as follows:

<template>
  <div>
    <template v-if="isLoading">Loading...</template>
    <template v-else>
      <h3>{{ post.title }}</h3>
      <p>{{ post.body }}</p>
    </template>
    <div>Mouse is at {{ x }}, {{ y }}</div>
  </div>
</template>

<script>
import { value, watch, onMounted, onUnmounted } from 'vue'
import { fetchPost } from './api'

export default {
 setup(props) {
   return {
     ...useFetch(props),
     ...useMouse()
   }
 }
}
</script>

The composed component fetches remote data and displays the mouse position, while delegating the corresponding logic to what Vue termed as composition functions (useFetch, and useMouse).

An additional benefit of the proposed function-based component API is better type inference with TypeScript. To get proper type inference in TypeScript, it is necessary to wrap a component definition in a function call:

import { createComponent } from 'vue'

const MyComponent = createComponent({
  // props declarations are used to infer prop types
  props: {
    msg: String
  },
  setup(props) {
    props.msg // string | undefined

    // bindings returned from setup() can be used for type inference
    // in templates
    const count = value(0)
    return {
      count
    }
  }
})

The documentation mentions:

createComponent is conceptually similar to 2.x’s Vue.extend, but it is a no-op and only needed for typing purposes. The returned component is the object itself, but typed in a way that would provide type information for Vetur and TSX. If you are using Single-File Components, Vetur can implicitly add the wrapper function for you.

Just like React Hooks, the new RFC has been received with a mix of enthusiasm and skepticism by developers. This may be due in part to the fact that a signification portion of Vue developers do not encounter, or solve differently, the scaling problems motivating the proposed function-based component API. Martin Sotirov notes:

I think the elephant in the room is that the problems this RFC solves (logic composition and better type support) are legitimate, but not faced by the community at large.
I have used Vue exclusively for frontend work since 2016 (from small one-off things to large enterprise projects), and I have yet to face any of the problems this RFC solves. I admit that mixin usage can be a problem for large codebases (see Vuetify) so I just avoid using mixins. There are just better ways to structure large modular codebases.

In order to further adoption, Vue 3 will incorporate the proposed API in a purely additive and backwards-compatible fashion. Existing Vue code needs not be rewritten (this excludes breaking changes introduced in other RFCs).

Vue.js is available under the MIT open-source license. Contributions are welcome via the Vue.js GitHub package and should follow the Vue.js contribution guidelines.

Rate this Article

Adoption
Style

BT