Vue Composition API part 1: Composition API fundamentals

Vue Composition API part 1: Composition API fundamentals

8 Jan 2023 javascript vue

Vue 3's Composition API represented an interesting gear-shift for Vue when it landed in Vue 2.7 (although it is nearly always synonymous with Vue 3.)

It didn't bring it with it a trove of new features - Vue 2 was already hugely feature-rich - but it had a lot to say about how you write and organise your code.

In this extensive guide I'll be looking at what that means!

What is the Composition API? 🔗

At its heart, Vue's Composition API is a means to better organise your component JavaScript. This is achieved by importing functions rather than, in the Options API declaring options.

If this sounds a little mundane and underwhelming, worry not; it actually represents a major gear-shift in how components are authored. Whereas the Options API (what Vue calls the pre-Composition API pattern) required us to separate our component JS by type, we can now separate JS by logical concern.

And so the good news right off the bat about the Composition API is it doesn't bring a load of new features to use (hardly any, in fact) - it's more that it brings new and more optimised ways to harness the existing features you already know from Vue < 2.7.

Better-structured component JS 🔗

To briefly rewind, with the Options API JS had to be separated out according to type and hooked into different parts of the component's default export. So component data was declared in a data() callback, methods in a methods object, and so on.

<template> <p @click='click()'>{{num}}</p> </template> <script> export default { data() { return { num: 0 }}, methods: { click() { num++; } } } </script>

Some people liked this type-based separation, and it can work well for simpler components. But it becomes burdensome once you pass a certain complexity theshold.

We can see a hint of this even in the simple example above: two bits of JS, although related to the same concern (the incrementation of a counter), are spread over different hooks. Shouldn't we be able to group our JS more sensibly? Meet the Composition API!

<template> <p>{{num}}</p> </template> <script> import { ref } from 'vue' export default { setup(props, ctx) { const num = ref(0); const click = () => num++; return {num, click}; } } </script>

There's four key things to note here.

  1. Everything goes inside the special setup() hook. (There's also a special setup script tag available as as an alternative to this function, which reduces code footprint. We'll meet this in part 2.) Vue automatically looks for and calls this hook when parsing our component, passing it props and the Vue context.
  2. The different parts of component JS (data, methods) etc. can all intermingle, and be grouped according to logical concern, not data type as with the Options API.
  3. Reactivity happens via a special wrapper function, ref(). We'll look at this more in Reactivity.
  4. Anything that the component's template needs access to must be returned in an object from the setup() hook. You'll get errors if, say, your template references {{foo}} but foo is not returned in the object. This even applies to any child components the component imports; they too must be exported in this object, otherwise they won't render.
  5. Finally, there's no use of this - everything uses locally-defined variables and functions.

Point 4 also means smaller distribution files, as minifiers can safely rename locally-defined variables and functions without fear of naming clashes. Were we using, as in the Options API, we wouldn't have this benefit, since minifiers typically don't change property names (it's usually not safe to do so.)

The Vue website has an excellent coloured-coded visual showing the same component coded in both the Options and Composition API. It's immediately striking how better organised code in the Composition API version is.

Note that the setup() hook can, instead of returning an object of exposed data and methods, return a function. If it does, it's interpreted by Vue as a render function, for times when you may want greater control over your component design via JSX.

Using imported functions 🔗

Along with changes to how your component code is structured, the other main aspect to the Composition API is the use of imported functions.

Where previously, in the Options API, we hung methods, computeds, lifecycle hooks etc. onto the export's object scaffold, we don't have that in the Composition API. Instead, we import the functionality we need from Vue.

<template> <p @click='click()'>Hello</p> </template> <script> import { onMounted } from 'vue' export default { setup(props, ctx) { const click = () => console.log('Click!'); onMounted(() => console.log('Mounted!')); return { click } } } </script>

Notice how, in order to use the mounted lifecycle hook, we have to import it. Notice we didn't have to import anything to use methods, though; they're just normally-declared functions that are then returned as part of the return payload.

And so you'd import whatever you need. To use computed properties, you'd import computed from Vue; to use render functions, you'd import h, and so on.

Reactivity 🔗

I mentioned a little earlier that in the Composition API reactivity is handled slightly different from in the Options API.

In the Options API, anything returned by the data() function is reactive. That is, if we modify it in a method, say, it'll update in the DOM automatically.

With the Composition API, we instead declare reactive data with one of ref(), reactive() or computed(), all of which must be imported from Vue.

<template> <p @click='click()'>{{text}}</p> </template> <script> import { ref } from 'vue' export default { setup() { const text = ref('foo'); const click = () => text.value = 'bar'; //<-- DOM will update return { text, click } } } </script>

Notice how we set up our reactive data with ref(), passing it the starting value "foo". Notice also how our template references text but our method, when updating the value, references text.value. The latter is necessary only when referencing the ref within JS; Vue "unpacks" refs for use in the template, so no need to use .value there.

I mentioned ref() and reactive(), but how do they differ?

The main difference is that refs created with reactive() do not have a value property and thus cannot be reassigned.

<template> <p>{{text}}</p> </template> <script> import { reactive } from 'vue' export default { setup() { const text = reactive('foo'); text.value = 'bar'; //<-- error return { text } } } </script>

Another difference is that ref() can be given primitives (strings, numbers, booleans) as values, whereas reactive() accepts only objects (ref() accepts objects too.)

Behind the scenes, ref() actually calls reactive().

To use computed properties, meanwhile, we just import computed from Vue and then declare and return our computed properties:

import { computed } from 'vue' export default { const foo = computed(() => 'bar'); return { foo } }

So that's it for part one! In part two (soon) we'll look at how the Composition API gets even better, and with even less code, via the special setup script tag. See you there!

Did I help you? Feel free to be amazing and buy me a coffee on Ko-fi!