What are props in Vue?
Props are the mechanism Vue uses to pass data from a parent component to a child component, giving each child a typed, read-only interface to what the parent provides.
Theory
TL;DR
- Props flow one way: parent to child. The child cannot modify them directly.
- Think of a component like a function: props are its arguments.
- Declare props in the child; pass values from the parent with
:propName="value". - Child needs to update a value? Emit an event up. Don't mutate the prop.
- More than 2-3 levels deep? Use
provide/injector Pinia instead.
Declaring and using props
<!-- Parent: App.vue -->
<template>
<UserCard :name="userName" :age="userAge" />
</template>
<script>
export default {
data() {
return { userName: 'Alice', userAge: 25 };
}
};
</script>
<!-- Child: UserCard.vue -->
<template>
<div>{{ name }} is {{ age }} years old</div>
</template>
<script>
export default {
props: { name: String, age: Number }
};
</script>The parent owns the data. The child receives it through props and renders it.
One-way data flow
Props create a one-way channel from parent to child. When userName changes in the parent, Vue automatically updates name in the child. But the child has no path to push changes back through a prop. That's by design. Parent manages state, child renders it.
This makes components predictable. You look at a child component and know: all inputs come from props, nothing sneaks in from the side.
When to use
- Static config (button label, icon name) → use a prop.
- Dynamic data (user profile, list of items) → use a prop.
- Child needs to update a value → emit an event up to the parent.
- Data needed across many components at once → use Pinia or
provide/inject.
Prop validation
Vue lets you declare more than just a type. You can mark a prop as required, set a default, or add a custom validator:
<script setup>
const props = defineProps({
name: { type: String, required: true },
age: { type: Number, default: 18 },
items: {
type: Array,
default: () => [], // factory function, not a literal
validator: (val) => val.every(item => typeof item === 'string')
}
});
</script>Vue prints a warning in development if validation fails. Nothing breaks in production, but the warning is your signal that the parent is sending wrong data.
How props work internally
When Vue compiles <UserCard :name="value" />, it produces createVNode(UserCard, { name: value }). At runtime, Vue wraps the child instance in a Proxy and tracks the parent's reactive state via effect. When a prop changes, Vue queues a DOM update through queueJob, batching multiple changes into one flush. The child just reads props.name. The machinery is invisible.
Common mistakes
Mutating a prop directly. Vue warns in dev mode and the change doesn't persist.
// Wrong
props.count++;
// Fix: emit the change up
emit('update:count', props.count + 1);Object or array default without a factory function. All component instances share one reference. This is the bug I see most often when reviewing Vue codebases.
// Wrong: every instance shares this same array
default: []
// Fix: new array per instance
default: () => []Forgetting : on dynamic values. <Comp name="val"> passes the string "val". <Comp :name="val"> passes the variable. Easy to miss in a template review.
Missing kebab-case in templates. Declare isAdmin in JS, write :is-admin="true" in the template. Vue maps them, but only when you follow the convention.
Real-world usage
- Vuetify buttons and cards accept
color,elevation, andvariantprops to configure appearance across the app. - Element Plus uses a
model-valueprop with anupdate:modelValueemit to powerv-modelon form inputs. - Nuxt projects pass a
postsarray as a prop into list components for content pages. - GitLab's Vue frontend uses props extensively for rendering dynamic user lists in merge request views.
Follow-up questions
Q: What is the difference between props and state?
A: State (data() or ref()) is internal and mutable. Props are external inputs from the parent, read-only inside the child.
Q: How do you validate a prop in Vue?
A: Use type, required, default, and validator in the prop definition. Vue logs a warning on mismatch in development mode.
Q: What changed for props in Vue 3?
A: Vue 3 adds defineProps() for <script setup>, which gives TypeScript type inference automatically. Runtime behavior is the same as Vue 2.
Q: How do you implement two-way binding with props?
A: Declare a modelValue prop and emit update:modelValue. The parent uses v-model, which is shorthand for both.
Q: How does prop reactivity work with deep objects in Vue 3?
A: Vue wraps props in a shallow Proxy. A primitive prop change triggers a re-render automatically. Deep mutations inside an object prop are not tracked unless you use watch with { deep: true } or the parent replaces the reference entirely. Mutating nested prop objects is exactly the kind of bug that's hard to trace later.
Examples
Basic: displaying user data in a card
<!-- Parent -->
<template>
<UserCard :name="user.name" :age="user.age" />
</template>
<script setup>
import { ref } from 'vue';
import UserCard from './UserCard.vue';
const user = ref({ name: 'Alice', age: 25 });
</script>
<!-- Child: UserCard.vue -->
<script setup>
defineProps({
name: { type: String, required: true },
age: { type: Number, default: 18 }
});
</script>
<template>
<div class="card">
<h2>{{ name }}</h2>
<p>Age: {{ age }}</p>
</div>
</template>The child knows nothing about the parent's data structure. It renders what it receives.
Intermediate: user list with conditional admin badge
<!-- Parent: UsersList.vue -->
<template>
<UserCard
v-for="user in users"
:key="user.id"
:name="user.name"
:avatar="user.avatar"
:is-admin="user.role === 'admin'"
/>
</template>
<script setup>
import { ref } from 'vue';
import UserCard from './UserCard.vue';
const users = ref([
{ id: 1, name: 'Bob', avatar: '/bob.jpg', role: 'admin' },
{ id: 2, name: 'Carol', avatar: '/carol.jpg', role: 'user' }
]);
</script>
<!-- Child: UserCard.vue -->
<script setup>
defineProps({
name: String,
avatar: String,
isAdmin: Boolean
});
</script>
<template>
<div class="user-card">
<img :src="avatar" :alt="name" />
<span>{{ name }}</span>
<span v-if="isAdmin" class="badge">Admin</span>
</div>
</template>Bob gets the admin badge. Carol doesn't. The parent passes the role check result; the child just renders it. Neither component cares about the other's internal logic.
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.