
A curious look at Vue’s function props and their unexpected advantages over emits.
Abdel Awad
December 15, 2025
I often see Vue developers limiting their props to passive data: strings, numbers, booleans, arrays, objects, and the like.
But rarely do I see them using functions as props. Many seem to believe that parent-component communication should strictly be "props down, events up." I don't see it that way.
You can pass anything as a prop in Vue, and functions are no exception. In this article, you’ll learn just how useful they can be.
It might sound fancy, but it’s really just a prop of type Function.
<script lang="ts" setup>
defineProps<{
fn: () => void;
}>();
</script>
It works similarly to emitting events, but instead of the component signaling that something happened, it directly executes the function provided by the parent.
So, your standard click handling could be re-written like this:
<script lang="ts" setup>
const props = defineProps<{
onClick: () => void;
}>();
function handleClick() {
// Directly invoke the parent's function
props.onClick();
}
</script>
<template>
<button @click="handleClick">Click me</button>
</template>
And you pass it from the parent like any other prop:
<template>
<MyComponent :on-click="doSomething" />
</template>
Naturally, you might ask: Why on earth would I do this? Why not just use events?
Good question. In that specific example, you probably should just use defineEmits. But let me show you a more practical use case.
Assume we have a button component that needs to show a loading spinner while an action is performing.
Normally, the parent would have to manage the loading state for every button instance, which gets muddy fast. With function props, the component can manage its own loading state by wrapping the execution.
If the user function is asynchronous (returns a Promise), we can await it and handle the loading state automatically.
<script lang="ts" setup>
import { ref } from 'vue';
const props = defineProps<{
onClick: () => void | Promise<void>;
}>();
const loading = ref(false);
async function handleClick() {
if (loading.value) return;
loading.value = true;
try {
// We await the parent's function here
await props.onClick();
} finally {
// And reset state regardless of success/failure
loading.value = false;
}
}
</script>
<template>
<button :disabled="loading" @click="handleClick">
<span v-if="loading" class="spinner">↻</span>
{{ loading ? 'Loading...' : 'Click me' }}
</button>
</template>
Now the parent doesn't have to manage a loading ref for every single button. The component handles its own UI state based on the lifecycle of the action provided by the parent.
While the try/finally block ensures our loading state resets correctly, it doesn't catch errors thrown by the parent's function. They will bubble up.
This means you should ensure your parent handlers are "safe" (handle their own errors) or expect the component to bubble them up (which might be what you want).
Here is an example of a safe handler in the parent component:
<script lang="ts" setup>
async function onSubmit() {
try {
await api.submit();
} catch (error) {
// Handle error (toast, alert, etc.)
console.error(error);
}
}
</script>
<template>
<ButtonComponent :on-click="onSubmit" />
</template>
Passing props like :on-click="handler" might feel like a step backward. We all love the @click syntax; it's natural and readable. :on-click feels... icky.
But here is a surprise: Vue's v-on (or @) listener syntax actually compiles to an onX prop check under the hood.
This means if you name your prop starting with on followed by a capital letter (camelCase), you can listen to it from the parent using the event syntax!
<script lang="ts" setup>
defineProps<{
onClick: () => void;
}>();
</script>
Parent usage:
<template>
<!-- This works! calls props.onClick -->
<ButtonComponent @click="onSubmit" />
</template>
It looks exactly like an event and works exactly like an event from the parent's perspective. But inside the component, it's a function prop, giving you direct access to the return value and execution flow.
One limitation of defineEmits is that events are inherently optional. A parent can choose to ignore an event, and there's no way to strictly enforce that a listener must be present.
With function props, you can use TypeScript and Vue's prop validation to make them required.
<script lang="ts" setup>
const props = defineProps<{
// Required: Parent MUST provide this "listener"
onSave: (data: any) => void;
// Optional
onError?: (error: Error) => void;
}>();
</script>
This enforces a stricter contract. The parent must provide a handler for onSave.
<template>
<!-- ✅ Valid -->
<MyComponent @save="handleSave" />
<!-- ❌ Type Error: Property 'onSave' is missing -->
<MyComponent />
</template>
This is critical for components like "Form Steps" or "Wizards" where proceeding without handling the completion event would break the flow.
This is where function props truly shine.
With standard events (emit), communication is one-way: you fire a message into the void and hope the parent hears it. You cannot get a return value back from an emit; the best you can do is use a watcher for a prop you are expecting, or perhaps reach for a store or provide/inject for a more "direct" communication line.
Function props are just functions. You can pass arguments, and crucially, you can receive a return value.
This allows for "two-way" communication during a single action. The component can ask the parent for permission, data, or validation before proceeding.
Imagine a Toggle Switch that should only toggle if the parent approves the change (e.g., waiting for API confirmation, or a "Are you sure?" dialog).
<script lang="ts" setup>
import { ref } from 'vue';
const props = defineProps<{
// The function returns a boolean (or Promise<boolean>)
// indicating if the toggle should proceed
onBeforeToggle: (newValue: boolean) => boolean | Promise<boolean>;
}>();
const active = ref(false);
async function toggle() {
const newValue = !active.value;
// 1. Ask parent for permission
const allowed = await props.onBeforeToggle(newValue);
// 2. Only update state if parent says YES
if (allowed) {
active.value = newValue;
}
}
</script>
<template>
<div @click="toggle" :class="{ active }">
{{ active ? 'ON' : 'OFF' }}
</div>
</template>
And in the parent:
<script lang="ts" setup>
async function handleToggle(newValue: boolean) {
if (newValue === true) {
// Example: Ask for confirmation
return confirm("Are you sure you want to activate the reactor?");
}
return true; // Always allow turning off
}
</script>
<template>
<!-- Usage with event syntax -->
<ToggleSwitch @before-toggle="handleToggle" />
</template>
This pattern of letting the parent "intercept" or control the internal logic of the component via a return value is impossible with standard events without creating complex paths of props and watchers.
You can extend this example to more complex use-cases, like a multi-step form wizard, or a delete row confirmation dialog, a file uploader/picker, and more.
Props don't have to be just passive data. By passing functions as props, you gain:
And thanks to Vue's flexible syntax, you can often keep using the @event style in your templates while enjoying the power of function props under the hood.
Give it a try next time you find yourself juggling loading states or trying to synchronize complex flows between parent and child.
Get the latest news and updates on developer certifications. Content is updated regularly, so please make sure to bookmark this page or sign up to get the latest content directly in your inbox.

The Curious Case of Vue’s Function Props
A curious look at Vue’s function props and their unexpected advantages over emits.
Abdel Awad
Dec 15, 2025

Nuxt UI: Unapologetically complete
Nuxt UI: The Shortcut You Don’t Have to Apologize For The component library that was built with developer experience in mind and solves accessibility and dependency management problems.
Reza Baar
Dec 12, 2025

Controlled vs Uncontrolled Components in React
Understanding controlled vs uncontrolled in React is about one question: who owns the state? Learn both meanings for form inputs and component design patterns.
Aurora Scharff
Dec 8, 2025
We can help you recruit Certified Developers for your organization or project. The team has helped many customers employ suitable resources from a pool of 100s of qualified Developers.
Let us help you get the resources you need.
