iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

<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 >

Discussion

Loading…