<script setup>
<script setup> is Vue 3’s preferred Single File Component syntax. Variables, imports, computeds, watchers, and lifecycle hooks declared at the top level are automatically exposed to the template — less boilerplate, better TypeScript ergonomics, faster runtime than the Options API.
Imports, props, emits, defineModel, exposes
EXAMPLE
<!-- 1) Minimum example -->
<script setup lang="ts">
import { ref, computed } from 'vue';
const count = ref(0);
const doubled = computed(() => count.value * 2);
function inc() { count.value++; }
</script>
<template>
<p>{{ count }} (double {{ doubled }})</p>
<button @click="inc">+1</button>
</template>
<!-- Everything you declare is automatically available in the template. -->
<!-- 2) defineProps + defineEmits — type-safe, compile-time macros -->
<script setup lang="ts">
import { withDefaults } from 'vue';
interface Props {
title: string;
count?: number;
tags?: string[];
}
const props = withDefaults(defineProps<Props>(), { count: 0, tags: () => [] });
const emit = defineEmits<{
(e: 'change', value: number): void;
(e: 'submit'): void;
}>();
function increment() { emit('change', props.count + 1); }
</script>
<!-- 3) defineModel (Vue 3.4+) — clean two-way binding -->
<script setup lang="ts">
const modelValue = defineModel<string>(); // ref<string | undefined>
const count = defineModel<number>('count', { default: 0 });
</script>
<template>
<input v-model="modelValue" />
<p>{{ count }}</p>
</template>
<!-- Parent: <Comp v-model="name" v-model:count="n" /> -->
<!-- 4) defineExpose — control what parent sees on a child ref -->
<script setup lang="ts">
import { ref } from 'vue';
const input = ref<HTMLInputElement | null>(null);
function focus() { input.value?.focus(); }
defineExpose({ focus });
</script>
<!-- Parent -->
<script setup lang="ts">
import Child from './Child.vue';
import { ref } from 'vue';
const childRef = ref<InstanceType<typeof Child> | null>(null);
</script>
<template>
<Child ref="childRef" />
<button @click="childRef?.focus()">Focus child input</button>
</template>
<!-- 5) defineSlots (Vue 3.3+) — typed slot signatures -->
<script setup lang="ts">
defineSlots<{
default(props: { user: User }): any;
actions(props: { close: () => void }): any;
}>();
</script>
<!-- 6) Async setup + Suspense -->
<script setup lang="ts">
const data = await fetch('/api/data').then((r) => r.json());
</script>
<!-- Parent: <Suspense><MyAsyncComponent /></Suspense> -->
<!-- 7) Top-level await ONLY inside <Suspense> — careful with reactivity loss after the first await -->
<script setup lang="ts">
import { ref, watch } from 'vue';
const search = ref('');
// Use watch() to react to changes; await INSIDE the watcher doesn't lose reactivity
watch(search, async (q) => {
const r = await fetch(`/search?q=${q}`);
/* … */
});
</script>
<!-- 8) Imports auto-exposed to template -->
<script setup lang="ts">
import { format } from 'date-fns';
import { logger } from '@/services';
const now = new Date();
</script>
<template>
<!-- 'format', 'logger', 'now' all usable directly -->
<p>{{ format(now, 'yyyy-MM-dd') }}</p>
</template>
<!-- 9) Components — just import, no register -->
<script setup lang="ts">
import UserCard from './UserCard.vue';
import BaseButton from '@/components/BaseButton.vue';
</script>
<template>
<UserCard :user="u" />
<BaseButton @click="save">Save</BaseButton>
</template>
<!-- 10) Composables — pure logic extraction -->
<!-- /composables/useMouse.ts -->
<script lang="ts">
import { ref, onMounted, onUnmounted } from 'vue';
export function useMouse() {
const x = ref(0), y = ref(0);
function update(e: MouseEvent) { x.value = e.clientX; y.value = e.clientY; }
onMounted(() => window.addEventListener('mousemove', update));
onUnmounted(() => window.removeEventListener('mousemove', update));
return { x, y };
}
</script>
<!-- Use in a component -->
<script setup lang="ts">
import { useMouse } from '@/composables/useMouse';
const { x, y } = useMouse();
</script>
<!-- 11) Inheriting attrs — useAttrs (Vue 3.0+) -->
<script setup lang="ts">
import { useAttrs } from 'vue';
const attrs = useAttrs(); // all unknown attributes & event listeners
defineOptions({ inheritAttrs: false }); // stop them auto-attaching to root
</script>
<template>
<div class="wrapper">
<button v-bind="attrs">Inner</button>
</div>
</template>
<!-- 12) Provide / inject — context-style DI -->
<script setup lang="ts">
import { provide, ref } from 'vue';
import type { InjectionKey } from 'vue';
export const ThemeKey: InjectionKey<{ mode: 'light' | 'dark' }> = Symbol('Theme');
const theme = ref({ mode: 'light' as const });
provide(ThemeKey, theme.value);
</script>
<!-- 13) Lifecycle hooks -->
<script setup lang="ts">
import { onMounted, onUnmounted, onBeforeUpdate, onActivated, onDeactivated, onErrorCaptured } from 'vue';
onMounted(() => console.log('mounted'));
onErrorCaptured((err) => { console.error(err); return false; });
</script>
<!-- 14) defineOptions — set component options -->
<script setup lang="ts">
defineOptions({
name: 'MyCustomCard',
inheritAttrs: false,
});
</script>
<!-- 15) Common bugs -->
<!-- • Forgot 'setup' on the script tag — Composition API utilities silently don't work -->
<!-- • Forgot .value in JS — typeof count !== 'number'; arithmetic returns NaN -->
<!-- • Using setup() AND <script setup> together — not supported; pick one -->
<!-- • Top-level await before reactive setup → reactivity context lost after await -->
<!-- • defineExpose forgotten — parent can't call child methods -->
<!-- • Imports tree-shaken because unused — re-add the import or reference -->
<!-- • Trying to import compile macros (defineProps etc) — they're compile-time only -->
Why it matters
<script setup> is now the default Vue 3 syntax: cleaner code, full type inference, automatic template binding. Use defineProps/defineEmits/defineModel for the public API, defineExpose to control parent access, and composables (useX) to extract reusable reactive logic.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
<script setup>
// Imports and top-level vars are exposed to template automatically.
import { ref } from 'vue';
const count = ref(0);
function inc() { count.value++; }
</script>
Try it Yourself »
Exercise
Composition-API SFC opener.
<script
>
Five letters.
Discussion
Loading…