Introduction

When composition API came to Vue, some people, including me, didn't like it. One of the reasons was that it's fundamentally different from what we had in Options API. I thought that after spending so much time writing apps with Options API, I need to forget all of that and start from scratch. Because I don't want to left behind with using old way of writing apps, I want to use the new syntax.

My concerns weren't lasted for long. I've decided to get more familiar with the new API and understand why experienced frontend developers praise it.

When something new comes out, and I don't fully understand it, I tend to don't like it. I've noticed that when I first introduced myself to TypeScript. I thought that it makes JavaScript ugly and complicated. But after using it for a while, I started to understand the reason behind it and the power it gives you when working with JavaScript.

Now, I'm writing pure JavaScript only in old projects that doesn't have TypeScript. I default to TS all the time in all the projects. Same thing happened with composition API. I constantly use it, even though I didn't like it in the beginning.

In this short article, I am going to convince you to use script setup functionality in composition API if you don't use it. It already became the recommended syntax for Vue apps if you're using Single File Components (SFC). I'll show you the major advantages, and try to find some drawbacks, if there are any. Before we do that, let's recall what is options API.

Options API

What is the issue with options API? Why do we need a new way of writing apps? Before I'm going to talk about problems with that, I want to point out that composition API is optional. It's not required, you can ignore it and work the way you worked with Vue 2.

Just remember, that when our projects get bigger over time, we repeat functionality from one component in others because there is no one simple way to reuse functionality between components.

Here is the example of a method that I found from one of my Vue 3 projects using Options API:

<template>
    <div
        v-if="isOpen"
        class="bg-white shadow-lg absolute p-10 border-2 top-[50%] left-[50%] translate-x-[-50%] translate-y-[-50%] rounded-md"
    >
        <h1 class="text-center text-xl">Contact modal</h1>
        <button
            @click="closeContactModal"
            class="px-3 py-2 rounded-md bg-red-700 text-white m-6"
        >Close modal</button>
    </div>
    <button
        @click="openContactModal"
        class="m-4 px-3 py-2 rounded-md bg-cyan-700 text-white"
    >Open modal</button>
</template>

<script lang="ts">
import { defineComponent } from 'vue'

export default defineComponent({
    data() {
        return {
            isOpen: false,
        }
    },

    mounted() {
        window.addEventListener('keydown', e => {
            if (e.key === 'Escape') {
                this.closeContactModal()
            }
        })
    },

    methods: {
        openContactModal(): void {
            this.isOpen = true
        },
        closeContactModal(): void {
            this.isOpen = false
        },
    },
})
</script>

For example, I want to reuse openContactModal method, closeContactModal method and isOpen property in other components. I can't just extract it in a module and import it because I can't export it with reactive properties. That's why many people started extracting simple logic like this in Vuex store.

What about computed and watchers? Can we extract them? No, we can only create mixins with these things because the purpose of mixins to distribute functionality between components. If you worked with mixins, you probably know, how complex your code becomes when you frequently use them.

That's where the composition API comes in.

Composition API

Composition API is basically a new way of organizing logic. It allows us to extract repeatable parts of the interface into reusable pieces of code. You're not going to see a real benefit on this in small projects, but I don't see any reasons to use the old Options API anymore. Once you figure out how it works, you have no reason to go back to previous syntax.

Here is the same component that I showed you before but with composition API.

<template>
    <div
        v-if="isOpen"
        class="bg-white shadow-lg absolute p-10 border-2 top-[50%] left-[50%] translate-x-[-50%] translate-y-[-50%] rounded-md"
    >
        <h1 class="text-center text-xl">Contact modal</h1>
        <button
            @click="closeContactModal"
            class="px-3 py-2 rounded-md bg-red-700 text-white m-6"
        >Close modal</button>
    </div>
    <button
        @click="openContactModal"
        class="m-4 px-3 py-2 rounded-md bg-cyan-700 text-white"
    >Open modal</button>
</template>

<script lang="ts">
import { ref, defineComponent, onMounted } from 'vue'

export default defineComponent({
    setup() {
        const isOpen = ref(false)

        function openContactModal(): void {
            isOpen.value = true
        }

        function closeContactModal(): void {
            isOpen.value = false
        }

        onMounted(() => {
            window.addEventListener('keydown', e => {
                if (e.key === 'Escape') {
                    closeContactModal()
                }
            })
        })

        return {
            isOpen,
            openContactModal,
            closeContactModal,
        }
    },
})
</script>

All the component logic lives inside a setup() function that returns everything that you want to be available inside the template. We define isOpen reactive variable with the special function ref() with default value false, and every time we want to assign a new value to it, we're adding .value to it. We need to do it because isOpen is not a regular variable, it's an object that handles reactivity.

If you've never seen composition API in Vue.js you might wonder, why put all the logic inside 1 function? When we talk about composition API, we have to see and understand this image:

Composition API Vue.js

The image shows us a component written with Options API on the left, and the same component after refactoring it to component composition model. You can now see, how composition allows us to organize functionality by function rather than option, thereby it improves readability of the component and makes it easier to work with.

However, the example given on the image is huge. When you have a component like that, it's definitely a sign that you might need to split it or extract some functionality into separate files. But as we talked earlier, you can only extract pure functions because pure functions do not depend on any state.

What is script setup

Script setup is just a syntactic sugar for using component composition model. It removes lots of boilerplate that you write in every component and improves the runtime performance. Here's the same contact modal component with script setup syntax:

<script setup lang="ts">
import { ref, onMounted } from 'vue'

const isOpen = ref(false)

function openContactModal(): void {
    isOpen.value = true
}

function closeContactModal(): void {
    isOpen.value = false
}

onMounted(() => {
    window.addEventListener('keydown', e => {
        if (e.key === 'Escape') {
            closeContactModal()
        }
    })
})
</script>

<template>
    <div
        v-if="isOpen"
        class="bg-white shadow-lg absolute p-10 border-2 top-[50%] left-[50%] translate-x-[-50%] translate-y-[-50%] rounded-md"
    >
        <h1 class="text-center text-xl">Contact modal</h1>
        <button
            @click="closeContactModal"
            class="px-3 py-2 rounded-md bg-red-700 text-white m-6"
        >Close modal</button>
    </div>
    <button
        @click="openContactModal"
        class="m-4 px-3 py-2 rounded-md bg-cyan-700 text-white"
    >Open modal</button>
</template>

You can see that setup function has gone, and the whole export default boilerplate has been removed as well. All the variables defined inside <script setup> are available inside your template. This is my favorite feature in Vue. I want to avoid writing the setup() function, export default and return all variables to make them available in the template.

Note. When you use script setup syntax, it's recommended to write <script setup></script> tags first, and after them <template></template>. It's a common convention that you can follow.

In my opinion, script setup syntax is something that puts Vue.js on another level. It gave me a feeling of using a new JavaScript framework that has all the Vue.js features. Especially if you're using TypeScript. Just look for example on the simple counter component, it's very short and straightforward to read.

<script setup lang="ts">
import { ref } from 'vue'

const count = ref(0)
</script>

<template>
    <button @click="count++">{{count}}</button>
</template>

Another cool thing is that you can write big production apps with script setup. Moreover, it's already used in many big applications.

Script setup in production

On the time when I'm writing this article, I've written two Single Page Apps from scratch with script setup syntax, and I'm currently working on one. I've never gotten any problems with this syntax. So, you can easily start using it from now on. Especially after script setup became the recommended syntax for Single File Components with composition API.

Just look at the short example how I use props in one of my apps:

<script setup lang="ts">
import { useI18n } from 'vue-i18n'

interface Props {
    paymentLink: string
    debtSum: string
}

const { t } = useI18n()
const { paymentLink, debtSum } = defineProps<Props>()
</script>

You just create a regular TypeScript interface and pass it as a generic type to a defineProps() global function. Nice and simple. I ❤️ it.

Here is the example with default props from a Spinner component.

<script setup lang="ts">
interface Props {
    center?: boolean
}

const { center } = withDefaults(defineProps<Props>(), {
    center: false
})
</script>

<template>
    <div
        style="border-top-color:transparent"
        class="w-10 h-10 border-4 border-blue-400 border-solid rounded-full animate-spin"
        :class="{ 'center-the-spinner': center }"
    ></div>
</template>

<style scoped>
.center-the-spinner {
    position: absolute;
    top: 50%;
    left: 50%;
}
</style>

We use withDefaults() global function, and pass all default values as the second argument. In case when you use default values with TypeScript, you have to make an interface property to be optional by adding a question mark to each one.

Conclusion

The composition API is already used in production-ready applications. It's not a proposed feature anymore. There are already tons of tutorials on YouTube and across the internet that will teach you everything you need to know about composition API and script setup. Why not write less code with the same power as before? I hope, I'll see more apps with script setup in the next 2 years.

📄 Read more about script setup in the official Vue.js documentation.

🧑‍💻 Link to the code examples on GitHub: github.com/SerhiiChoBlog/script-setup-future

Keywords: framework