A Comprehensive Guide to Vue 3's script-setup Syntax
Vue 3.0 introduced the Composition API, which, while powerful, initially felt more verbose for developers accustomed to the Options API. The Vue team addressed this feedback within the Single File Component (SFC) context by introducing the <script setup> syntax, a compile-time syntactic sugar designed to streamline the developer experience.
Prerequisite: This guide assumes familiarity with Vue 3's Composition API and SFC structure. Ensure your project uses
vueand@vue/compiler-sfcat version3.1.4or higher for stable support.
Introduction to script-setup
The <script setup> feature, which moved out of experimental status in Vue 3.2.0-beta.1, offers a more concise way to write component logic. By adding the setup attribute to a <script> tag, the entire block is treated as the setup() function. Top-level variables and functions are automatically available in the template, eliminating the need for explicit returns.
Standard Composition API Syntax:
<script>
import { defineComponent, ref } from 'vue'
export default defineComponent({
setup() {
const message = ref('Hello')
return { message }
}
})
</script>
script-setup Syntax:
<script setup>
import { ref } from 'vue'
const message = ref('Hello')
</script>
The compiler transforms this syntax back into a standard component during build. This feature is intended for use within .vue files in a build-tool-enabled project (e.g., Vite, Vue CLI).
Global Compiler Macros
In <script setup>, several compiler macros are available globally without import. To avoid ESLint warnings, you can configure them as globals.
.eslintrc.js Configuration:
module.exports = {
globals: {
defineProps: 'readonly',
defineEmits: 'readonly',
defineExpose: 'readonly',
withDefaults: 'readonly',
},
}
Streamlined Template Usage
Variables Are Automatically Exposed
In the standard setup(), data must be explicitly returned for template access. With <script setup>, top-level declarations are automatically available.
Standard Syntax:
<template>
<p>{{ text }}</p>
</template>
<script>
import { defineComponent } from 'vue'
export default defineComponent({
setup() {
const text = 'Auto-exposed'
return { text }
}
})
</script>
script-setup Syntax:
<template>
<p>{{ text }}</p>
</template>
<script setup>
const text = 'Auto-exposed'
</script>
Automatic Component Registration
Imported components are automatically registered for use in the template.
Standard Syntax:
<template>
<CustomButton />
</template>
<script>
import { defineComponent } from 'vue'
import CustomButton from './CustomButton.vue'
export default defineComponent({
components: { CustomButton },
setup() { /* ... */ }
})
</script>
script-setup Syntax:
<template>
<CustomButton />
</template>
<script setup>
import CustomButton from './CustomButton.vue'
</script>
Working with Props
Since there is no explicit setup() function signature, props are declared using the defineProps macro.
Basic Array Syntax
<script setup>
defineProps(['title', 'count', 'metadata'])
</script>
To access props within the script, assign the result too a variable.
<script setup>
const componentProps = defineProps(['title', 'count'])
console.log(componentProps.title)
</script>
Runtime Type Checking with Constructors
You can define prop types using JavaScript constructors, similar to the Options API.
<script setup>
defineProps({
title: String,
count: {
type: Number,
required: false,
default: 0
},
metadata: Object
})
</script>
Type-Based Declarations with TypeScript
Using TypeScript, you can define prop types via a type annotation.
<script setup lang="ts">
interface UserData {
id: number
name: string
}
defineProps<{
title: string
count?: number // Optional prop
userData: UserData
}>()
</script>
Setting Default Values with withDefaults
For TypeScript users, the withDefaults helper provides default values for optional props.
<script setup lang="ts">
interface Settings {
itemsPerPage?: number
labels?: string[]
}
const props = withDefaults(defineProps<Settings>(), {
itemsPerPage: 10,
labels: () => ['default']
})
console.log(props.itemsPerPage) // 10
</script>
Note: Constructor-based and type-annotation-based prop definitions are mutually exclusive.
Defining Custom Events with defineEmits
Custom events are declared using the defineEmits macro.
Basic Usage
<script setup>
const emitEvent = defineEmits(['updateTitle', 'deleteItem'])
function triggerUpdate() {
emitEvent('updateTitle', 'New Title')
}
</script>
Full event validation syntax is also supported.
Accessing Attributes and Slots
Accessing Non-Prop Attributes with useAttrs
Attributes not declared as props are accessible via the useAttrs composition function (Vue >= 3.1.4).
<script setup>
import { useAttrs } from 'vue'
const attrs = useAttrs()
console.log(attrs.class)
console.log(attrs['data-test'])
</script>
Accessing Slots with useSlots
Slot content can be accessed programmatically using useSlots (Vue >= 3.1.4), particularly useful in render functions/JSX.
Parent Component:
<template>
<ChildComponent>
<p>Default slot content</p>
<template #footer>
<p>Footer slot content</p>
</template>
</ChildComponent>
</template>
<script setup>
import ChildComponent from './ChildComponent.vue'
</script>
Child Component (using JSX):
// ChildComponent.jsx
import { defineComponent, useSlots } from 'vue'
export default defineComponent({
setup() {
const slots = useSlots()
return () => (
<div>
<div>{slots.default ? slots.default() : null}</div>
<div>{slots.footer ? slots.footer() : null}</div>
</div>
)
}
})
Exposing Component Data with defineExpose
By default, a component's internal state in <script setup> is private. To expose specific data or methods to a parent component via a template ref, use defineExpose.
Child Component:
<script setup>
import { ref } from 'vue'
const internalState = ref('secret')
const publicMethod = () => { console.log('called') }
defineExpose({
internalState,
publicMethod
})
</script>
Parent Component:
<template>
<ChildComponent ref="childRef" />
</template>
<script setup>
import { ref, onMounted } from 'vue'
import ChildComponent from './ChildComponent.vue'
const childRef = ref(null)
onMounted(() => {
console.log(childRef.value.internalState) // 'secret'
childRef.value.publicMethod() // 'called'
})
</script>
Using Top-Level await
<script setup> allows the use of await at the top level. The component's setup context is automatically made asynchronous.
<script setup>
const apiResponse = await fetch('/api/data').then(r => r.json())
</script>
This is equivalent to an async setup() function in the standard syntax.