Understanding these functions deepens your grasp of Vue 3's Composition API and enables you to build more robust and maintainable applications. This guide provides practical examples you can immediately implement in your projects.
Overview of Composition API Macros
Vue 3 provides these macro functions specifically for defining component properties, events, exposed methods, options, and slots within the script setup syntax.
| Function | Purpose | Basic Syntax | Notes |
|---|---|---|---|
defineProps |
Declares component props | const props = defineProps({...}); |
Enables type-safe prop validation |
defineEmits |
Declares component events | const emit = defineEmits([...]); |
Type-safe event emission |
defineExpose |
Exposes component internals to parent | defineExpose({...}); |
Essential for template refs |
defineOptions |
Sets component options in Composition API | defineOptions({...}); |
Alternative to script setup attributes |
defineSlots |
Defines and accesses slot content | const slots = defineSlots(); |
Useful for dynamic slot handling |
Practical Examples and Analysis
1. defineProps: Declaring Component Properties
The defineProps function enables you to declare and validate component props, making data flow between parent and child components more preditcable and secure. This approach works exceptionally well with TypeScript, providing automatic type inference and runtime validation.
Best suited for: Components requiring clear prop contracts, especially in TypeScript projects where type safety is paramount. While it adds clarity and validation, it may feel verbose for simple props that don't need strict validation.
Here's a counter component accepting an initial value:
<!-- Counter.vue -->
<template>
<div>
<p>{{ currentValue }}</p>
<button @click="increase">Increase</button>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
interface Props {
startValue: number;
}
const props = defineProps<Props>();
const currentValue = ref(props.startValue);
function increase() {
currentValue.value++;
}
</script>
Parent component usage:
<!-- App.vue -->
<template>
<div>
<Counter :start-value="initialCount" />
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import Counter from './Counter.vue';
const initialCount = ref<number>(100);
</script>
2. defineEmits: Declaring Component Events
The defineEmits function provides a type-safe mechanism for declaring events that a component can emit, replacing the traditional $emit approach. This creates a clear contract between components about what events they can communicate.
Best suited for: Complex component communication scenarios where clear event contracts improve maintainability. Extending our counter example:
<!-- Counter.vue -->
<template>
<div>
<p>{{ currentValue }}</p>
<button @click="increase">Increase</button>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
interface Props {
startValue: number;
}
const props = defineProps<Props>();
const emit = defineEmits<{
(e: 'value-changed', value: number): void;
}>();
const currentValue = ref(props.startValue);
function increase() {
currentValue.value++;
emit('value-changed', currentValue.value);
}
</script>
Parent component handling the event:
<!-- App.vue -->
<template>
<div>
<Counter :start-value="initialCount" @value-changed="onValueChanged" />
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import Counter from './Counter.vue';
const initialCount = ref<number>(100);
function onValueChanged(newValue: number) {
console.log('Counter value:', newValue);
}
</script>
defineProps and defineEmits form the foundation of Vue 3 component communication and are used together frequently in production applications.
3. defineExpose: Exposing Component Methods to Parents
The defineExpose function lets you explicitly control which internal methods or properties are accessible to parent components via template refs. By default, components using <script setup> keep their state private.
Best suited for: Building reusable component libraries or creating components that need controlled public APIs. This improves encapsulation but requires careful consideration of what gets exposed.
Let's add a reset method to our counter:
<!-- Counter.vue -->
<template>
<div>
<p>{{ currentValue }}</p>
<button @click="increase">Increase</button>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
interface Props {
startValue: number;
}
const props = defineProps<Props>();
const emit = defineEmits<{
(e: 'value-changed', value: number): void;
}>();
const currentValue = ref(props.startValue);
function increase() {
currentValue.value++;
emit('value-changed', currentValue.value);
}
function resetToInitial() {
currentValue.value = props.startValue;
}
defineExpose({
reset: resetToInitial
});
</script>
Parent accessing the exposed method:
<!-- App.vue -->
<template>
<Counter ref="counterRef" :start-value="initialCount" @value-changed="onValueChanged" />
<button @click="resetCounter">Reset</button>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import Counter from './Counter.vue';
interface CounterRef {
reset: () => void;
}
const counterRef = ref<CounterRef | null>(null);
const initialCount = ref<number>(10);
function onValueChanged(value: number) {
console.log('Current value:', value);
}
function resetCounter() {
counterRef.value?.reset();
}
</script>
Important: Methods and properties exposed via defineExpose must be accessed through a template ref on the parent component.
4. defineOptions: Setting Component Options
The defineOptions function enables you to configure component options like name, components, and directives while using the Composition API. This provides a way to access options-based configuration without switching between APIs.
Best suited for: Complex components requiring detailed configuration. It bridges the gap between Composition and Options APIs, though it may be unnecessary for simpler components.
defineOptions({
name: 'DataDisplay',
components: { ChildComponent },
directives: {
focus: {
mounted(element: HTMLElement) {
element.focus();
}
}
}
});
Full example with custom component and directive:
<!-- Dashboard.vue -->
<template>
<div v-highlight>
<p>{{ greeting }}</p>
<InfoPanel />
</div>
</template>
<script setup>
import { ref } from 'vue';
import InfoPanel from './InfoPanel.vue';
const greeting = ref('Welcome to the dashboard');
defineOptions({
name: 'Dashboard',
components: {
InfoPanel
},
directives: {
highlight: {
mounted(el: HTMLElement) {
el.style.backgroundColor = '#f0f0f0';
el.style.padding = '16px';
}
}
}
});
</script>
5. defineSlots: Handling Component Slots
The defineSlots function provides programmatic access to slot content, enabling dynamic slot handling and inspection. While less commonly used then other macros, it proves valuable for components with complex layout requirements.
Best suited for: Components requiring dynamic content manipulation or flexible layouts. The API simplifies slot processing but may be overkill for straightforward slot usage.
<!-- LayoutCard.vue -->
<template>
<div class="card">
<div class="card-header">
<slot name="header"></slot>
</div>
<div class="card-body">
<slot></slot>
</div>
<div class="card-footer">
<slot name="footer"></slot>
</div>
</div>
</template>
<script setup lang="ts">
import { defineSlots } from 'vue';
const slots = defineSlots();
console.log('Header exists:', !!slots.header);
console.log('Default exists:', !!slots.default);
console.log('Footer exists:', !!slots.footer);
</script>
Parenet using the slot component:
<!-- App.vue -->
<template>
<LayoutCard>
<template #header>
<h2>Card Title</h2>
</template>
<template #footer>
<p>Additional information</p>
</template>
</LayoutCard>
</template>
<script setup>
import LayoutCard from './LayoutCard.vue';
</script>
When to Use Each Function
| Scenario | Recommended Functions |
|---|---|
| Basic component communication | defineProps + defineEmits |
| Building reusable component APIs | defineExpose |
| Complex component configuration | defineOptions |
| Dynamic content rendering | defineSlots |
Additional Configuration
ESLint Configuration for Vue 3 Macros
When using ESLint with Vue 3, configure it to recognize Composition API macros as global variables:
// .eslintrc.js
module.exports = {
root: true,
env: {
node: true,
browser: true,
es2021: true
},
parser: 'vue-eslint-parser',
extends: [
'eslint:recommended',
'plugin:vue/vue3-strongly-recommended'
],
parserOptions: {
ecmaVersion: 12,
parser: '@typescript-eslint/parser',
sourceType: 'module'
},
plugins: [
'vue',
'@typescript-eslint'
],
rules: {
quotes: [1, 'single'],
'vue/script-setup-uses-vars': 0,
'no-unused-vars': 0
},
globals: {
withDefaults: true,
defineExpose: true,
defineEmits: true,
defineProps: true,
defineOptions: true,
defineSlots: true
}
};
withDefaults Helper for Type-Safe Props
The withDefaults helper provides default values for props while maintaining TypeScript type inference:
// Using runtime declaration with defaults
const props = withDefaults(defineProps({
title: String,
quantity: { type: Number, default: 0 },
}), {
title: 'Default Title',
quantity: 0
});
// Using TypeScript generics with defaults
interface Props {
title: string;
quantity: number;
}
const props = withDefaults(defineProps<Props>(), {
title: 'Default Title',
quantity: 0
});
withDefaults is highly recommended for TypeScript projects as it enables proper type inference and reduces repetitive default value assignments.