DEV Community

Cover image for defineComponent vs <script setup>
Matija Novosel
Matija Novosel

Posted on • Updated on • Originally published at matijanovosel.com

defineComponent vs <script setup>

Introduction

With the creation of Vue 3 the community has been introduced to a new, more function oriented way of organizing component structure dubbed the Composition API.

The previous way being named the Options API was more akin to an object oriented approach, heavily relying on the this keyword.

Options API

Even though both ways are fully capable of covering common use cases, the options API had its use but in the bigger picture it severely limited the developer as it abstracted away the reactivity details and enforced code organization via option groups.

<template>
  <button @click="increment">
    Count is: {{ count }}
  </button>
</template>

<script>
export default {
  // Properties returned from data() become reactive state and will be exposed on this.
  data() {
    return {
      count: 0
    }
  },
  // Methods are functions that mutate state and trigger updates.
  // They can be bound as event listeners in templates.
  methods: {
    increment() {
      this.count++
    }
  },
  // Lifecycle hooks are called at different stages of a component's lifecycle.
  // This will be called when the component is mounted.
  mounted() {
    console.log(`The initial count is ${count.value}.`)
  }
}
</script>
Enter fullscreen mode Exit fullscreen mode

Composition API

The Composition API is centered around declaring reactive state variables directly in a function scope, and composing state from multiple functions together to handle complexity.

It is more free-form, and requires understanding of how reactivity works in Vue to be used effectively.

In return, its flexibility enables more powerful patterns for organizing and reusing logic.

<template>
  <button @click="increment">
    Count is: {{ count }}
  </button>
</template>

<script>
import { ref, onMounted, defineComponent } from "vue";

export default defineComponent({
  setup() {
    // Reactive state
    const count = ref(0);

    // Functions that mutate state and trigger updates
    function increment() {
      count.value++;
    }

    // Lifecycle hooks
    onMounted(() => {
      console.log(`The initial count is ${count.value}.`);
    });

    return {
      count,
      increment
    };
  }
});
</script>
Enter fullscreen mode Exit fullscreen mode

Data that needs to be used on the template must be manually exposed through the return values of the defineComponent function which is very verbose, but it allows for clearer insight into what is being sent up.

<script setup>

Additionally, another option was added to the Composition API in the form of syntactic sugar - the setup directive.

It allows the developer to write top level statements without the additional boilerplate of the defineComponent function.

<template>
  <button @click="increment">
    Count is: {{ count }}
  </button>
</template>

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

const count = ref(0)

function increment() {
  count.value++
}

onMounted(() => {
  console.log(`The initial count is ${count.value}.`)
})
</script>
Enter fullscreen mode Exit fullscreen mode

Unlike the previous entry, everything is exposed to the template which might make it difficult to manage the things that were actually meant to be on the template, but with the amount of reduced boilerplate this syntax makes up for that.

Props and emits

The defineComponent approach is verbose and very explicit in the way it functions, such as the props property where the properties of the component are defined.

<template>
  <span>
    {{ title }}
  </span>
</template>

<script>
import { defineComponent } from "vue";

export default defineComponent({
  props: {
    title: {
      type: String,
      default: "",
      required: false
    }
  },
  setup(props) {
    return {};
  }
});
</script>
Enter fullscreen mode Exit fullscreen mode

The title prop is immediately available to the template and if it needs to be used in the script it is exposed through the parameters of the setup function.

The same can be done with the setup directive, using the defineProps function.

<template>
  <span>
    {{ title }}
  </span>
</template>

<script setup>
const props = defineProps({
  title: {
    type: String,
    default: "",
    required: false
  }
});
</script>
Enter fullscreen mode Exit fullscreen mode

Notice how we do not need to explicitly write props.title in the template.

If the title needs to be used in the script, the defineProps function returns a reactive object with the aforementioned prop.

The defineProps macro is available by default if using the setup directive.

The same principle is applied to the emission of events:

<script>
import { defineComponent } from "vue";

export default defineComponent({
  emits: ["update:value"],
  setup(props, { emit }) {
    emit("update:value");
    return {};
  }
});
</script>
Enter fullscreen mode Exit fullscreen mode

When not using the setup directive, the emit function must be exposed through the setup function parameters.

<script setup>
  const emit = defineEmits(["update:value"]); emit("update:value");
</script>
Enter fullscreen mode Exit fullscreen mode

In this case, however, only the defineEmits macro is neccessary. The return value is a function that can be used to emit events.

In comparison

<template>
  <button @click="increment">
    Count is: {{ count }}
  </button>
</template>

<script>
import { ref, onMounted, defineComponent } from "vue";

export default defineComponent({
  setup() {
    const count = ref(0);

    function increment() {
      count.value++;
    }

    onMounted(() => {
      console.log(`The initial count is ${count.value}.`);
    });

    return {
      count,
      increment
    };
  }
});
</script>
Enter fullscreen mode Exit fullscreen mode
<template>
  <button @click="increment">
    Count is: {{ count }}
  </button>
</template>

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

const count = ref(0)

function increment() {
  count.value++
}

onMounted(() => {
  console.log(`The initial count is ${count.value}.`)
})
</script>
Enter fullscreen mode Exit fullscreen mode

Comparing the previous blocks of code side by side, we can see a slight decrease in boilerplate. To be exact 29 lines vs 20 lines, a 30ish% decrease!

Conclusion

The defineComponent approach and the setup macro approach are one in the same in terms of functionality but differ in regards to the way things are set up.

One is verbose and the other is not, if the need for more explicit code arises defineComponent is the way to go.

When using setup though, a lot of boilerplate is removed and top level code becomes the norm.

By personal choice, I go with the setup syntactic sugar.

Top comments (5)

Collapse
 
bitstream101 profile image
Patrik Lindqvist

Thanks! This was very helpful. A simple and short explanation, just what I needed.

Collapse
 
abdurrkhalid333 profile image
Abdur Rehman Khalid

Thank you for the nice comment, I really appreciate that.

Collapse
 
5uperdan profile image
Danny

After following some video tutorials for several hours, they still hadn't fully explained this. Thanks for writing it up

Collapse
 
matijanovosel profile image
Matija Novosel

Really glad it helped, honestly it gets a bit tedious trying to learn Vue sometimes. Not a self plug, but I'd recommend this book for some quality explanations.

Collapse
 
smithgit profile image
Sam Smith

This was very helpful; it explains this area substantially better than the Electron docs, which are good but not as complete or understandable as they could be.