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

Single File Components

Single File Components bundle template, script, and styles in one .vue file. The <script setup> syntax makes component code feel like writing a small JS module — declarative, reactive, scoped CSS by default, and excellent TypeScript ergonomics.

script setup, props, emits, slots

EXAMPLE
<!-- 1) Skeleton — the three blocks of a Vue SFC -->
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue';

const count = ref(0);
const doubled = computed(() => count.value * 2);

onMounted(() => {
    console.log('mounted');
});

function reset() { count.value = 0; }
</script>

<template>
    <div class="counter">
        <p>Count: {{ count }} (doubled: {{ doubled }})</p>
        <button @click="count++">+1</button>
        <button @click="reset">reset</button>
    </div>
</template>

<style scoped>
.counter { padding: 1rem; border: 1px solid #ddd; border-radius: 8px; }
button  { margin-right: 0.5rem; }
</style>

<!-- 2) Props — typed and validated -->
<script setup lang="ts">
interface Props {
    title: string;
    count?: number;
    items: string[];
}
const props = withDefaults(defineProps<Props>(), {
    count: 0,
});
props.items;            // string[]
</script>

<!-- 3) Emits — type-safe events -->
<script setup lang="ts">
const emit = defineEmits<{
    (e: 'update:modelValue', value: string): void;
    (e: 'submit'): void;
}>();

function handleInput(event: Event) {
    const target = event.target as HTMLInputElement;
    emit('update:modelValue', target.value);
}
</script>

<!-- 4) v-model on custom components — implicit emit('update:modelValue') + prop -->
<script setup lang="ts">
defineProps<{ modelValue: string }>();
defineEmits<{ (e: 'update:modelValue', v: string): void }>();
</script>
<template>
    <input :value="modelValue" @input="$emit('update:modelValue', ($event.target as HTMLInputElement).value)" />
</template>

<!-- Use like: <MyInput v-model="name" /> -->

<!-- 5) defineModel — Vue 3.4+ shorthand for two-way binding -->
<script setup lang="ts">
const modelValue = defineModel<string>();   // ref<string | undefined>
const count      = defineModel<number>('count', { default: 0 });

function reset() {
    modelValue.value = '';
    count.value      = 0;
}
</script>
<template>
    <input v-model="modelValue" />
    <p>{{ count }}</p>
</template>

<!-- 6) Slots — default + named + scoped -->
<!-- Card.vue -->
<template>
    <article class="card">
        <header><slot name="title" /></header>
        <div><slot /></div>
        <footer><slot name="actions" :close="close" /></footer>
    </article>
</template>
<script setup lang="ts">
function close() { /* close logic */ }
</script>

<!-- Caller -->
<Card>
    <template #title><h3>Profile</h3></template>
    <p>email: mara@example.com</p>
    <template #actions="{ close }">
        <button @click="close">Close</button>
    </template>
</Card>

<!-- 7) defineExpose — control what parent refs see -->
<script setup lang="ts">
import { ref } from 'vue';
const input = ref<HTMLInputElement | null>(null);
function focus() { input.value?.focus(); }
defineExpose({ focus });           // parent can call (childRef.value as any).focus()
</script>

<!-- 8) Scoped styles + CSS modules + :deep -->
<style scoped>
.title { color: var(--brand); }                /* scoped to this component */
.title :deep(.child) { color: red; }            /* reach into a child component's DOM */
</style>

<style module>
/* CSS Modules — generates locally scoped class names accessible as $style.foo */
.title { font-weight: 600; }
</style>
<template>
    <h2 :class="$style.title">…</h2>
</template>

<!-- 9) Component composition — composables -->
<!-- /composables/useCounter.ts -->
<script lang="ts">
import { ref } from 'vue';
export function useCounter(initial = 0) {
    const n = ref(initial);
    function inc() { n.value++; }
    function reset() { n.value = initial; }
    return { n, inc, reset };
}
</script>

<!-- In a SFC -->
<script setup lang="ts">
import { useCounter } from '@/composables/useCounter';
const { n, inc } = useCounter(10);
</script>

<!-- 10) Provide / inject — pass data without props -->
<!-- Parent -->
<script setup lang="ts">
import { provide, ref } from 'vue';
import type { InjectionKey } from 'vue';
export const ThemeKey: InjectionKey<ReturnType<typeof useTheme>> = Symbol();
const theme = useTheme();
provide(ThemeKey, theme);
</script>

<!-- Descendant -->
<script setup lang="ts">
import { inject } from 'vue';
import { ThemeKey } from '@/components/Provider.vue';
const theme = inject(ThemeKey);
if (!theme) throw new Error('ThemeProvider missing');
</script>

<!-- 11) Async components — code-split a heavy widget -->
<script setup lang="ts">
import { defineAsyncComponent } from 'vue';
const Chart = defineAsyncComponent(() => import('./HeavyChart.vue'));
</script>
<template>
    <Suspense>
        <template #default><Chart :data="data" /></template>
        <template #fallback>Loading…</template>
    </Suspense>
</template>

<!-- 12) Templates: directives at a glance -->
<!--   v-if / v-else-if / v-else     conditional rendering (removes from DOM) -->
<!--   v-show                          toggles 'display: none' -->
<!--   v-for="item in items" :key="item.id"   list rendering — always provide :key -->
<!--   v-bind / :prop                  bind data
<!--   v-on / @event                   listen for events -->
<!--   v-model                          two-way binding -->
<!--   v-html                           sets innerHTML; sanitise first -->
<!--   v-text                           sets textContent -->
<!--   v-pre                            skip compilation for the element -->
<!--   v-once                            render once, cache -->

<!-- 13) Lifecycle hooks (Composition API) -->
<script setup lang="ts">
import { onBeforeMount, onMounted, onBeforeUpdate, onUpdated, onBeforeUnmount, onUnmounted, onErrorCaptured } from 'vue';
onMounted(() => { /* DOM available */ });
onUnmounted(() => { /* cleanup timers, subscriptions */ });
onErrorCaptured((err, instance, info) => { console.error(err); return false; });
</script>

<!-- 14) Tooling -->
<!--   <script setup> requires the @vitejs/plugin-vue build pipeline -->
<!--   TypeScript: 'lang="ts"'; install Volar (the official VS Code extension) -->
<!--   ESLint:    eslint-plugin-vue + @vue/eslint-config-typescript -->
<!--   Prettier:  works out of the box on .vue files -->

<!-- 15) Common bugs -->
<!--   • Forgot 'setup' on the script tag — Composition API utilities don't work -->
<!--   • Using a ref's value inside the template — refs auto-unwrap, don't write '.value' there -->
<!--   • Mutating a prop — log warning + change doesn't reflect; emit an update instead -->
<!--   • Async setup() without <Suspense> — fails silently -->
<!--   • Scoped styles can't reach into <Teleport> destinations — use :deep() or global styles -->
<!--   • Multiple root elements need <Suspense> wrapper if used inside <Transition> -->
<!--   • defineProps + defineEmits + defineModel are MACROS — Vue compiles them away; don't import them -->

Why it matters

<script setup> is the modern Vue idiom — concise, type-safe, and built around composables you can extract and reuse. Use defineProps with TypeScript interfaces, defineEmits for typed events, and defineModel (Vue 3.4+) to skip the boilerplate of two-way binding. Scoped styles keep CSS local; composables keep logic local.

Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.

Example

Example
<!-- App.vue -->
<script setup>
import { ref } from 'vue';
const msg = ref('hello');
</script>
<template>
    <h1>{{ msg }}</h1>
</template>
<style scoped>
    h1 { color: #42b883; }
</style>
Try it Yourself »

Discussion

Loading…