Vue.js 3 Script Setup Tutorial

In this Vue tutorial we learn an easier way to use the Composition API in components with the script setup.

We cover the lack of config object, how to use components, props and events and event data.

Lesson Video

If you prefer to learn visually, you can watch this lesson in video format.

Lesson Project

If you want to follow along with the examples, you will need to create an app generated by the Vue CLI as well as the following extra component.

  • src/components/GreetingMessage.vue

The project should look similar to the following.

Example: project
project-name/
├── src/
|   ├── components/
|   |   └── GreetingMessage.vue
|   └── App.vue

The GreetingMessage component will have a paragraph with a simple greeting.

Example: src/components/GreetingMessage.vue
<template>
  <p>Hello from GreetingMessage</p>
</template>

What is the script setup?

The script setup is a different way for us to use the Composition API in components. Basically, we don’t need to use the config object and its setup option.

We can opt-in to the script setup by simply adding the setup property to the script block’s tag.

Syntax: script setup
<script setup>
  // ...
</script>

We can also use it in combination with the regular script block.

Syntax: script and script setup
<script setup>
  // ...
</script>

<script>
  export default {
    setup() {
      // ...
    }
  }
</script>

One of the great things about the script setup is that we need a lot less boilerplate. As an example, let’s consider the following.

Example: src/App.vue
<template>
  <p>Hello {{ name }}</p>
</template>

<script>
import { ref } from 'vue'

export default {
  setup() {
    const name = ref('John')

    return { name }
  }
}
</script>

In the example above, we need the config object, the setup option and we need to return the “name” ref from it to be used in the template.

With the script setup block, we only need the ref and its import for it to work.

Example: src/App.vue
<template>
  <p>Hello {{ name }}</p>
</template>

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

  const name = ref('John')
</script>

How to use components with the script setup

A component only needs to be imported to be available, we don’t have to register it like we did before.

Syntax: script setup
<template>
  <!-- component instance -->
  <component-name />
</template>

<script setup>
  import ComponentName from 'path-to-component'
</script>

As an example, let’s import our GreetingMessage component into the root App component and invoke an instance of it in the template.

Example: src/App.vue
<template>
  <greeting-message />
</template>

<script setup>
  import GreetingMessage from './components/GreetingMessage'
</script>

If we run the example in the browser, GreetingMessage renders as expected. The file is a lot simpler, smaller and easier to read.

How to use props with the script setup

In the Composition API, we access the props on a component instance by using the props object from the setup option’s parameter list.

Example: props parameter
setup(props) {
  // access props through
  // 'props' object
  props.propName
}

With the script setup, we define props with defineProps , which takes a single argument.

  1. Object with the prop and its type as key:value pair.
Syntax: defineProps
<template>
  {{ props.propName }}
</template>

<script setup>
const props = defineProps({
  propName: PropType
})
</script>

defineProps is actually a compiler macro that’s compiled away at build time and can only be used inside the script setup tag.

It doesn’t need to be imported, but the ESLint plugin will raise an undef warning for it. In that case, we have two options to fix the warning.

1. Explicitly import the macro from the core ‘vue’ package.

Example: import
import { defineProps } from 'vue'

2. Define a global readonly variable for it in your ESLint config file.

The .eslintrc.js config file can be found in your project’s root folder.

Example: .eslintrc.js
module.exports = {
  globals: {
    defineProps: "readonly"
  }
}

To demonstrate, let’s change our example and let the GreetingMessage component accept two props. To keep the example simple, we’ll explicitly import the defineProps macro.

Example: src/components/GreetingMessage.vue
<template>
  <p>Hello {{ props.firstName }} {{ props.lastName }}</p>
</template>

<script setup>
import { defineProps } from 'vue'

  const props = defineProps({
    firstName: String,
    lastName:  String
  })
</script>

In the root App component, we can pass values to the two props on the GreetingMessage component instance just like we normally would.

Example: src/App.vue
<template>
  <greeting-message
    :firstName="user.fName"
    :lastName="user.lName"
  />
</template>

<script setup>
  import { ref } from 'vue'
  import GreetingMessage from './components/GreetingMessage'

  const user = ref({
    fName: 'John',
    lName: 'Doe'
  })
</script>

If we run the example in the browser, the greeting message will show the first and last names.

How to send events with the script setup

In the Composition API, we send an event with context.emit function through a custom function.

Example: emit (from child)
setup(props, context) {

  function sendEvent() {
    context.emit('event')
  }
}

Then listen for the event in the parent component on the child component’s instance.

Example: listen for, and react to (in parent)
<template>
  <child-component @event="doSomething" />
</template>

With the script setup, we define events with defineEmits , which takes a single argument.

  1. Array of events in string format.

The event is also sent through a custom function.

Example: defineEmits
const emits = defineProps([
  'event'
])

function sendEvent() {
  emits('event')
}

Like defineProps , defineEmits defineEmits is a macro. It will also raise an ESLint undef warning if we don’t import it explicitly or define a global variable.

Example: .eslintrc.js
module.exports = {
  globals: {
    defineProps: "readonly",
    defineEmits: "readonly"
  }
}

To demonstrate, we’ll change our example to emit a “greet” event from GreetingMessage when the user clicks a button. To keep the example simple, we’ll import the defineEmits macro explicitly.

Example: src/components/GreetingMessage.vue
<template>
  <button @click="greetAlert">Greet Me</button>
</template>

<script setup>
  import { defineEmits } from 'vue'

  // define event to emit
  const emits = defineEmits([
    'greet'
  ])

  function greetAlert() {
    // emit 'greet' event to parent
    emits('greet')
  }
</script>

In the root App component, we’ll listen for the event and just invoke a function that displays an alert.

Example: src/App.vue
<template>
  <greeting-message @greet="greeting" />
</template>

<script setup>
  import GreetingMessage from './components/GreetingMessage'

  function greeting() {
    alert('Greetings!')
  }
</script>

If we run the example in the browser and click the button, it will show the alert.

How to send data with events

To send data with the event, we add the data as the second parameter when we emit the event.

Syntax: emit with data
const emits = defineProps([
  'event'
])

function sendEvent() {
  emits('event', data)
}

As an example, let’s add a name to the event in GreetingMessage .

Example: src/components/GreetingMessage.vue
<template>
  <button @click="greetAlert">Greet Me</button>
</template>

<script setup>
  import { defineEmits } from 'vue'

  const emits = defineEmits(['greet'])

  function greetAlert() {
    // send 'John Doe' with 'greet' event
    emits('greet', 'John Doe')
  }
</script>

In the root App component, we’ll accept the data as a parameter to the custom function and use it in the alert.

Example: src/App.vue
<template>
  <greeting-message @greet="greeting" />
</template>

<script setup>
  import GreetingMessage from './components/GreetingMessage'

  function greeting(name) {
    alert('Greetings ' + name)
  }
</script>

When we run the example in the browser and click the button, it will show the greeting with the name.

How to use top-level await with the Script Setup

If we want to request data asynchronously from a service, we can use the await keyword.

When a component uses await, it must be instantiated with suspense in the parent. This allows Vue to take care of resolving the asynchrony and load the component properly.

Syntax: Suspense
<script setup>
// child component
  await fetch('api')
    .then((response) => response.json())
</script>

<template>
<!-- parent component -->
  <suspense>
    <greeting-message />
  </suspense>
</template>

To demonstrate, let’s fetch a random Chuck Norris joke in the greeting component.

We’ll store the response in a ref, then render it in the template.

Example: src/components/GreetingMessage.vue
<script setup>
  import { ref } from 'vue'

  let joke = ref({})

  await fetch('https://api.chucknorris.io/jokes/random')
    .then((response) => response.json())
    .then((data) => joke = data)
</script>

<template>
  <p>{{ joke.value }}</p>
</template>

In the root app component, we’ll import GreetingMessage and add an instance of it in suspense tags in the template.

Example: src/App.vue
<script setup>
  import GreetingMessage from '@/components/GreetingMessage.vue'
</script>

<template>
  <suspense>
    <greeting-message />
  </suspense>
</template>

If we head over to the browser, we’ll see the joke so everything works fine.

Further Reading

For more information on the topics covered in this lesson, please see the relevant section below.