DEV Community

Cover image for Brief Look on Vue 3.0 Composition API: More Readable  Components
Arik Sasmita
Arik Sasmita

Posted on

Brief Look on Vue 3.0 Composition API: More Readable Components

The Options API

With the current Vue 2.x way of building components, we're separating it by the option, not feature. What this means is that for example, a single toggling state for showTitle will need to have a reactive data in data and a method to toggle the state. For a small simple component, this won't be a problem. But as the component grows, more functionality will be added, thus making reading the whole feature more difficult. An example can be seen below.

<template>
  <div>
    <h2>Options API</h2>
    <p>{{ total }}</p>
    <p>
      Data:
      <span
        v-for="(n, idx) in apiRes"
        :key="idx">
        {{ n }}
      </span>
    </p>
    <input type="text" v-model="searchInputText"/>
    <p>Occurence: {{ results }}</p>
    <button
      @click="toggleAddForm">
      Add New Entry
    </button>
    <div v-if="showAddForm">
      <input type="text" v-model="newInputText"/>
      <button
        @click="add">
        Add
      </button>
    </div>
  </div>
</template>

<script>
export default {
  data () {
    return {
      // #1 Search Form
      searchInputText: '',
      apiRes: ['Google', 'Amazon', 'Facebook', 'Uber', 'Netflix', 'Google', 'Twitter', 'Amazon'],
      results: 0,
      // #2 Input Form
      newInputText: '',
      showAddForm: false,
    }
  },
  computed: {
    // #1 Search Form
    total () {
      return `Total data: ${this.apiRes.length}`
    }
  },
  watch: {
    // #1 Search Form
    searchInputText (value) {
      this.results = this.apiRes.filter(itm => itm === value).length
    },
  },
  methods: {
    // #2 Input Form
    toggleAddForm () {
      this.showAddForm = !this.showAddForm
    },
    // #2 Input Form
    add () {
      if (this.newInputText) {
        this.apiRes.push(this.newInputText)
      }
    }
  },
}
</script>

Enter fullscreen mode Exit fullscreen mode

Now that might still be readable to some extent, but as you can see we started to get different concerns of functionality split by data and methods. This will continue to get more complex when the component grows, let alone if we need to add computed or lifecycle hooks.

Now on Vue 2.x, we have mixins that can address this concern. Mixins can be used for functionality purposes, making related data end methods in one file, then importing that file wherever it needed. For me, this still looks relevant and is the way to go for most cases (this post doesn't imply we should make it obsolete either). The downside on this, however, is that sometimes we need to know what happens in it, to decide if we can use it as-is, or we need to adjust it for our needs. We'll need to open the file, read again and when finished, check other places that use it making sure none of them broke. Other things are that there is a great potential that mixins can have similar names, which will make various issues.

Composition API

The upcoming Composition API is the solution Vue 3 introduces to resolve the above issues. At the moment of writing (May 2020) it is still in beta, but we can already try it. It can make related data, methods, and functions scoped in one place instead of spread all over the file. We can use it in existing Vue 2.x projects by installing an additional npm package.

npm i --save @vue/composition-api
Enter fullscreen mode Exit fullscreen mode

Afterward, we will need to import the package in our app.js file as we can see below.

import Vue from "vue";
import VueCompositonApi from "@vue/composition-api";
...
Vue.use(VueCompositonApi);
Enter fullscreen mode Exit fullscreen mode

If you are starting from scratch with Vue 3.0 however, we can directly import everything in a .vue file from vue. For example:

import { refs, reactive } from "vue";
Enter fullscreen mode Exit fullscreen mode

We can now access the setup() function inside our .vue file. This function will run on component initialization, acts as a replacement to beforeCreate and created lifecycle hooks. Now let's see how we can use some of this enhancement compared to existing Options API in the previous Vue version.

Data Reactivity

Reactivity is the main advantage when using Front End Javascript framework like Vue. In Vue 2.5 we are defining our data in data () function or data object. In Composition API, we need to import ref or reactive from @vue/composition-api to achieve the same functionality.

...
<script>
import { ref, reactive } from "@vue/composition-api"

export default {
    name: 'ComposedComponent',
    setup () {
        // Using ref, for simple values
        const searchInputText = ref('')
        const results = ref(0)
        // Using reactive, for object values
        const optData = reactive({
            displayTitle: true
        })

        // Accessing 
        const toggleDisplay = () => {
            optData.displayTitle = !compData.displayTitle;
            searchInputText.value = ! searchInputText.value;
        };

        return { results, searchInputText, toggleDisplay };
    }
}
</script>
Enter fullscreen mode Exit fullscreen mode

Both ref and reactive can make a value reactive, but they have slight differences in use and access. ref can directly be assigned to a single variable or constants, while reactive can be used as the usual data function we use often in Vue 2.0, it will make the whole Object it covers to be reactive. If you can see above, ref will need .value for us to have access to its content, while reactive can be accessed directly.

Another way we can implement reactivity is by wrapping all values as an object in a reactive function. This way, if we need to have a computed value, we can directly access it without specifying .value. For example, we will use the above code and wrap the values in reactive function, then add a computed value which accesses the result value.

...
<script>
import { ref, reactive, computed } from "@vue/composition-api"

export default {
    name: 'ComposedComponent',
    setup () {
        // Wrapping all values in a reactive function
        const allData = reactive({
            searchInputText: '',
            results: 0,
            resultText: computed(() => {
                return `Total result: ${allData.result}
            }),
        })

        // Accessing 
        const logData = () => {
            console.log(allData.resultText)
        };

        return { allData, logData }
    }
}
</script>
Enter fullscreen mode Exit fullscreen mode

There is a drawback from this setup though. We will need to change how we access it in the template as well.

<template>
  <div>
    <p>{{ allData.total }}</p>
    <p>
      Data:
      <span
        v-for="(n, idx) in allData.apiRes"
        :key="idx">
        {{ n }}
      </span>
    </p>
    <input type="text" v-model="allData.searchInputText"/>
    ...
  </div>
</template>
Enter fullscreen mode Exit fullscreen mode

Naturally, if you are familiar with ES6, we will first think that we can just spread the object upon exporting like return { ...allData, logData }. But this will throw an error. Even if you specify it one by one like allData.total, the value will lose its reactivity.

For this, Vue 3.0 introduces toRefs that will do just this. The function will convert each of the object values and map it in their own ref. With this applied, we can access the values in the template like before.

...
<script>
import { ref, reactive, computed, toRefs } from "@vue/composition-api"

export default {
    name: 'ComposedComponent',
    setup () {
        ...
        return { ...toRefs(allData), logData }
    }
}
</script>
Enter fullscreen mode Exit fullscreen mode

If we don't need to access any other than the reactive value, we can simply do return ...toRefs(allData)

Computed and Watch

Computed values can be added by using computed function imported from Vue, similar to reactive. It receives a function that returns the computed value as we have previously in Options API.

import { computed } from '@vue/composition-api'
...
setup () {
    const apiRes = ['Google', 'Amazon', 'Facebook', 'Uber', 'Netflix', 'Google', 'Twitter', 'Amazon']
    const total = computed(() => {
      return `Total data: ${apiRes.length}`
    })
    return { total }
}
Enter fullscreen mode Exit fullscreen mode

As for the Watch, we can assign a watch function using watch, also imported from vue. What we can do in there is similar to what we have in previous version.

import { ref, computed, watch } from 'vue'
...
setup () {
    const results = ref(0)
    const searchInputText = ref('')
    watch(() => {
    results.value = apiRes.filter(itm => itm === searchInputText.value).length
    console.log(searchInputText.value)
  })
  return { results, searchInputText }
}
Enter fullscreen mode Exit fullscreen mode

Props

The props is similar to previous version. But in order to have it accessible in setup function, we need to pass it in as an argument. The no-destructure rule still applies here, as if we do it, we will lose the reactivity

<script>
...
export default {
  props: {
    withPadding: {
      type: Boolean,
      default: false,
    },
  },
  setup (props) {
    const classNames = props.withPadding ? 'padded' : ''
    return { classNames }
  }
</script>
Enter fullscreen mode Exit fullscreen mode

File Management

Knowing above that much, some of us might think that this can make the setup function gigantic in no time. That goes in contrast with the readability improvement theme we have here. But fear not! As handy as we have mixins previously, we can also outsource related functions into separate files. They are functions after all.

createCounter.js

import { reactive, computed, toRefs } from '@vue/composition-api'

export default function useEventSpace() {
  const event = reactive({
    capacity: 5,
    attending: ["Hey", "Some", "Name"],
    spacesLeft: computed(() => {
      return event.capacity - event.attending.length
    }),
  })
  function increase () {
    event.capacity++
  }
  return {
    ...toRefs(event),
    increase,
  }
}
Enter fullscreen mode Exit fullscreen mode

Page.vue

<script>
import createCounter from '@/../createCounter'
...
export default {
  setup () {
    return { ...createCounter() }
  }
</script>
Enter fullscreen mode Exit fullscreen mode

Emit

One change for emit in the new version is we are now encouraged to declare it in a separate emits property. This acts as a self-documentation of the code, ensuring that developers that come to a component they didn't make understand the relations to its parent.

Similar to props, we can also validate the payload passed and returns a Boolean as result.

<script>
...
export default {
  // we can also pass an array of emit names, e.g `emits: ['eventName']`,
  emits: {
    inputChange: payload => {
      // payload validation
      return true
    }
  }
  ...
  mounted () {
    this.$emit('inputChange', {
      // payload
    })
  }
</script>
Enter fullscreen mode Exit fullscreen mode

Lifecycle Hooks

We can also set lifecycle hooks in setup function by importing onXXX beforehand. Important note on this one is we can't access this in setup function. If we still needed for example emit to parrent on mounted, using mounted hook from Options API seems to be the way for now.

import { onMounted, onBeforeMount } from '@vue/composition-api'
...
export default {
  setup() {
    onMounted(() => {
      console.log('Mounted')
    }
    onBeforeMounted(() => {
      console.log('Before Mounted')
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Multi Root Template

As you may already know with Vue 2.x, we can only have one root element in the template. Not anymore in Vue 3.0 though. Thanks to the Fragments feature, having only one root element is no longer mandatory.

Note: Your linter might complain about this being illegal. Mine does. Best to save this till we have proper release. But still exciting nonetheless

<template>
  <div class="main-content">
    <p>{{ allData.total }}</p>
    ...
  </div>
  <div class="modal">
    <p>modal content</p>
    ...
  </div>
</template>
Enter fullscreen mode Exit fullscreen mode

Conclusions

Ease of code writing and readability seems to be one of the main focuses on the upcoming update of Vue. Apart from under the hood optimizations and better support of TypeScript, these are all exciting updates to look forward to. Especially they can be treated as add-on updates on an existing app rather, than complete rewrite, as the current API still supported.

There's so much more to the features listed as upcoming updates on the next version on Vue. you can see the complete list and updates over in Vue's RFCs repo here: https://github.com/vuejs/rfcs.

Other features worth their in-depth article with detailed samples. More on that will be coming.

Sources

Image Credit

Markus Spiske on Unsplash

Top comments (0)