Tutorials Logic, IN info@tutorialslogic.com

Vue Components defineProps, defineEmits, Slots

Component Boundary

A Vue component owns a focused piece of interface, its local state, and the behavior needed to render it. In a build-based Vue 3 application, components usually live in .vue Single-File Components. Parents send data down through props; children report events upward. This one-way contract keeps reusable pieces understandable.

After this lesson, you can split a page at a meaningful boundary, declare props and emitted events with <script setup>, use slots for parent-owned markup, and diagnose state that was placed in the wrong component.

Components in Vue

Components are reusable, self-contained pieces of UI. In Vue 3, components are defined as Single File Components (.vue files) containing template, script, and style sections.

Components - Props, Emits, Slots

Components - Props, Emits, Slots
<!-- components/AppButton.vue -->
<template>
  <button
    :class="['btn', `btn-${variant}`, `btn-${size}`, { loading }]"
    :disabled="disabled || loading"
    @click="$emit('click', $event)"
  >
    <span v-if="loading" class="spinner"></span>
    <slot>{{ label }}</slot>
  </button>
</template>

<script setup>
// defineProps - declare accepted props
const props = defineProps({
  label:    { type: String, default: 'Click me' },
  variant:  { type: String, default: 'primary', validator: v => ['primary','secondary','danger'].includes(v) },
  size:     { type: String, default: 'md' },
  disabled: { type: Boolean, default: false },
  loading:  { type: Boolean, default: false },
})

// defineEmits - declare emitted events
const emit = defineEmits(['click'])
</script>

<style scoped>
.btn { padding: 8px 16px; border-radius: 4px; cursor: pointer; }
.btn-primary   { background: #42b883; color: white; }
.btn-secondary { background: #6c757d; color: white; }
.btn-danger    { background: #dc3545; color: white; }
.btn-sm { padding: 4px 8px; font-size: 0.875rem; }
.btn-lg { padding: 12px 24px; font-size: 1.125rem; }
.loading { opacity: 0.7; cursor: not-allowed; }
</style>

Components in Vue - HTML Example

Components in Vue - HTML Example
<!-- components/AppCard.vue - using slots -->
<template>
  <div class="card">
    <!-- Named slot: header -->
    <div v-if="$slots.header" class="card-header">
      <slot name="header" />
    </div>

    <!-- Default slot: body content -->
    <div class="card-body">
      <slot />
    </div>

    <!-- Named slot: footer -->
    <div v-if="$slots.footer" class="card-footer">
      <slot name="footer" />
    </div>
  </div>
</template>

<!-- Scoped slot - pass data from child to parent -->
<!-- components/DataList.vue -->
<template>
  <ul>
    <li v-for="item in items" :key="item.id">
      <!-- Pass item data to parent via scoped slot -->
      <slot :item="item" :index="items.indexOf(item)" />
    </li>
  </ul>
</template>

<script setup>
defineProps({ items: Array })
</script>

Components in Vue - HTML Example 2

Components in Vue - HTML Example 2
<!-- Parent.vue - using components -->
<template>
  <div>
    <!-- Using AppButton -->
    <AppButton label="Save" variant="primary" @click="save" />
    <AppButton variant="danger" :loading="isDeleting" @click="deleteItem">
      Delete  <!-- slot content overrides label -->
    </AppButton>

    <!-- Using AppCard with named slots -->
    <AppCard>
      <template #header>
        <h3>User Profile</h3>
      </template>

      <!-- Default slot -->
      <p>Name: Alice</p>
      <p>Email: alice@example.com</p>

      <template #footer>
        <AppButton label="Edit" size="sm" />
      </template>
    </AppCard>

    <!-- Using scoped slot -->
    <DataList :items="users">
      <template #default="{ item, index }">
        <span>{{ index + 1 }}. {{ item.name }}</span>
      </template>
    </DataList>
  </div>
</template>

<script setup>
import { ref } from 'vue'
import AppButton from './AppButton.vue'
import AppCard from './AppCard.vue'
import DataList from './DataList.vue'

const isDeleting = ref(false)
const users = ref([{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }])

function save() { console.log('Saved!') }
async function deleteItem() {
  isDeleting.value = true
  await new Promise(r => setTimeout(r, 1000))
  isDeleting.value = false
}
</script>

Data Ownership

Put state in the nearest component that must coordinate it. A child may own temporary UI state such as whether its menu is open. A parent should own a selected product when several children read or change that selection. Pass the value as a prop and emit an event describing what happened instead of mutating the prop.

Props are read-only from the child perspective. If a child needs an editable draft, initialize local state from the prop and decide how later prop changes should synchronize. Blindly copying every prop creates two sources of truth and subtle stale-state bugs.

Events and Slots

Declare emitted events with defineEmits so the public component contract is visible. Prefer event names that describe an outcome, such as saved or quantityChanged, rather than implementation details such as buttonClicked. The parent decides how that outcome changes application state.

Use a slot when the child owns layout or behavior but the parent owns the inserted content. Props are appropriate for typed data; slots are appropriate for markup. A component with dozens of display props may be easier to use when a small number of named slots expose the intended extension points.

Reuse Test

Render multiple instances to verify local state is not accidentally shared. Each component instance receives its own local reactive state. Shared module-level reactive objects are different: every importer can observe the same object, which should be a deliberate store or composable decision.

Before you move on

Boundary Review

4 checks
  • Give the component one clear UI responsibility.
  • Send values through declared props and outcomes through declared events.
  • Keep shared state in the nearest coordinating owner.
  • Use slots when the parent needs to supply markup.

Ownership Bugs

  • Mutating a prop inside the child.

    Emit a requested change or maintain an explicitly synchronized local draft.
  • Using one giant page component.

    Extract pieces with independent responsibility or reusable contracts.
  • Globally registering every component.

    Prefer local imports so dependencies remain visible and tree-shakable.

Try this next

Design One Contract

0 of 2 completed

Browse Free Tutorials

Explore 500+ free tutorials across 20+ languages and frameworks.