Skip to main content

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/inject or Pinia instead.

Declaring and using props

vue
<!-- 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:

vue
<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.

js
// 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.

js
// 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, and variant props to configure appearance across the app.
  • Element Plus uses a model-value prop with an update:modelValue emit to power v-model on form inputs.
  • Nuxt projects pass a posts array 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

vue
<!-- 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

vue
<!-- 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 ready
Premium

A concise answer to help you respond confidently on this topic during an interview.

Finished reading?