Some Tips For Migrating To The VueJS Composition API

The composition API introduced in VueJS 3.0 has been controversial, to say the least.

In the newest version of Vue (as of the time of writing), the simple object based syntax has been replaced with a new, programmatic approach that, while arguably more powerful and extensible, comes at the cost of complexity.

Here are some tips that you can use to hopefully make the migration for your team as smooth as possible

A quick overview of the composition API

Though the syntax may be significanly different, at the end of the day Vue is still largely the same reactive component based framework you already know and love.

  • Components still accept outside data using props. Data still flows downwards.

  • Components still emit messages to parent components. You must now explicitly define all emitted messages, though.

  • Computed functions still run only when data has changed allowing for dynamic template data

  • Data is still reactive and triggers templates to re render upon value change

To use the composition API you simply add a new key called "setup" to the object exported in your components.

<script>
export default {
    name: "MyComponent",
    props: {
        name: {
            type: String,
            default: "Andrew"
        }
    },
    setup(props) {
        // Instead of a data function we now wrap individual variables with refs
        const buttonPressed = ref(false);

        // Computed functions are now created by passing a callback to the computed function
        const helloString = computed(() => `Hello ${props.name}`);

        // functions are now declared as const variables instead of within the methods attribute of the returned object
        const buttonClicked = function() {
            // Values inside reactive refs must be accessed using .value attribute of the ref object 
            buttonPressed.value = true;
        }

        // Return all data you need to build your component in an object
        return {
            buttonPressed,
            helloString,
            buttonClicked
        }       
    }
}
</script>

<template>
  <div>
    <input type="button" @click.prevent="buttonClicked" value="Click Here" />

    <span v-if="buttonPressed">
        {{ helloString }}
    </span>
  </div>
</template>

At first glance, the composition API setup method might seem like a baffling change. Yeah, it provides a bit more freedom than what could be achieved with the attribute based syntax of Vue and Vue 2, but it comes at the expense of great complexity.

However, once you see the "script setup" syntax, this change starts making a bit more sense.

## The new script setup syntax

The "script setup" syntax is a compiler level directive that you can use within your single file vue components.

Instead of adding a setup function to the object returned, you can now just write your setup function directly and the compiler will convert this into the correct syntax at compile time.

<script setup>
// defineProps is a compile time directive that automatically converts this function into the object syntax used in previous 
// versions of vue
const props = defineProps({
  name: {
      type: String,
      default: "Andrew"
  }
});

// Instead of a data function we now wrap individual variables with refs
const buttonPressed = ref(false);

// Computed functions are now created by passing a callback to the computed function
const helloString = computed(() => `Hello ${props.name}`);

// functions are now declaed as const variables instead of within the methods attribute of the returned object
const buttonClicked = function() {
    // Values inside reactive refs must be accessed using .value attribute of the ref object 
    buttonPressed.value = true;
}

// No need to return anything like the setup() function, all refs in scope will
// be exported automatically. Neat!
</script>

<template>
  <div>
    <input type="button" @click.prevent="buttonClicked" value="Click Here" />

    <span v-if="buttonPressed">
        {{ helloString }}
    </span>
  </div>
</template>

This turns our single file components into a nice hybrid of OO-like syntax while taking advantage of Javascripts powerful "functional first" approach to development. There are downsides, though. This new syntax no longer "holds your hand" like the object syntax did. It's now up to you to decide how you want to structure your codebase, for better or worse. Which leads us to my next tip

Organize your code (even if you dont have to anymore)

The "script setup" syntax leaves it up to you to decide how you want your components to be structured. That doesn't mean your work has to be a mess. All of your components can be organized in exactly the same way so that when you have to go back to your previous work to fix something or add additional functionality, you already know exactly how to read this piece of code before you even open it.

Heres the structure that I use for every component that I write.

<script setup>
// Props 

// Emits 

// Modals

// Setup

// Data

// Watchers 

// Computed

// Hooks

// Methods 
</script>

<template>
    <span>Templates go at the bottom</span>
</template>

Ideally this would be enforced by an eslint rule but I haven't researched whether this exists yet. If not I plan to make my own in the future.

Prefer composables to renderless functions

I loved renderless functions in vue2. Any limitation of the options API could easily be fixed by making a renderless component that you added to the template of your component. It made development simple and fun. Components felt more like building something with Lego blocks than doing actual work.

You can still use renderless components in vue 3 but there is another alternative that should be considered first, composables. Composables are a new way to reuse functionality in vue 3 by passing reactive variables back from a function that you can call from any component. Composables avoid added the performance hit your application receives when using renderless components. Theres also no reason you can't use composables inside of your renderless components if you really wanted to.

Consider the code snippet below. Its a simple component that accepts a first and last name via component props and displays a name and a greeting. The greeting changes if you click the button.

<script setup>
import { ref, computed } from 'vue'

const props = defineProps({
  first:{
    type: String,
    default: 'John'
  },

  last:{
    type: String,
    default: 'Smith'
  },
});

const greeting = ref('hello');

const greetUser = computed(() => `${ greeting } ${props.first} ${props.last}`);

const changeGreeting = function () {
  const theGreeting = greeting.value;

  if(theGreeting === 'hello') {
    greeting.value = 'goodbye';
  }
  else {
    greeting.value = 'hello';
  }
};
</script>

<template>
  <div>
    <input type="button" @click.prevent="changeGreeting" value="Change Greeting" />
    <span>{{ greetUser }}</span>
  </div>
</template>
Output:
hello John Smith

Output after clicking button:
goodbye John Smith

If we needed this functionality to be reusable, we could do so very easily using a composable.

import { ref, computed } from 'vue'

// The functionality of the original component has been moved over to a reusable function.
// useXYZ is the recommended syntax for your composables
export default function useGreeting(props) {
    const greeting = ref('hello');

    const greetUser = computed(() => `${ greeting } ${props.first} ${props.last}`);

    const changeGreeting = function () {
      const theGreeting = greeting.value;

      if(theGreeting === 'hello') {
        greeting.value = 'goodbye';
      }
      else {
        greeting.value = 'hello';
      }
    };

    // Return any variables and functions necessary to use this functionality at the end of the function
    return {
      greetUser,
      changeGreeting
    }
}

Then in our original component, we can now import and run the useGreeting function to extract its contents

<script setup>
import { useGreeting } from './greeting.js'

const props = defineProps({
  first:{
    type: String,
    default: 'John'
  },

  last:{
    type: String,
    default: 'Smith'
  },
});

// Javascript destructuring is used to pull the data from the composable into the local scope of the component. 
// These variables will automatically be available to use in our template. Note that we are passing the props 
// from our component to the composable as a function argument. Any data required in the composable can be
// passed in this way.
const { greetUser, changeGreeting } = useGreeting(props); 
</script>

<template>
  <div>
    <input type="button" @click.prevent="changeGreeting" value="Change Greeting" />
    <span>{{ greetUser }}</span>
  </div>
</template>

Conclusion

The new composition API introduced in Vue 3 is a big change, but the underlying functionality is largely the same. In addition to these tips, I would recommend reading the documentation for Vue 3 available on the Vue.js website. Best of luck migrating your codebase!

Similar Blog Posts

Batch Converting PNG files to WEBP

A script I made to convert png files to webp

Using Callback Function Props In Your Laravel/Breeze Apps

I show you how I handle third party dependencies and global functions in my Vue apps

How I nearly accidentally automated my entire department out of a job

A story of an internship I did with a governmenmental IT Dept