Vue.js Props
Introduction
In Vue.js applications, components are the building blocks that help us create reusable and modular code. However, these components often need to communicate with each other. Props (short for properties) are the primary way to pass data from a parent component down to its child components.
Props enable you to create flexible and reusable components by allowing parent components to feed data into child components. Think of props as custom HTML attributes that you can register on a component.
In this guide, we'll explore:
- What props are and why they're important
- How to define and use props
- Prop validation
- One-way data flow
- Best practices for working with props
What Are Props?
Props are custom attributes you can register on a component. When a value is passed to a prop attribute, it becomes a property on that component instance. A child component can then use this property value as needed.
Registering Props
Basic Props Registration
To use props in a component, you need to register them first. Vue.js provides several ways to register props:
Array Syntax
The simplest way to register props is using an array:
<script>
export default {
props: ['message', 'user', 'isActive']
}
</script>
Object Syntax
For more control, you can use the object syntax to add validation:
<script>
export default {
props: {
message: String,
user: Object,
isActive: Boolean
}
}
</script>
Using Props in a Component
Once registered, props can be used in your component's template like regular component properties:
<template>
<div>
<h1>{{ message }}</h1>
<p>User: {{ user.name }}</p>
<span v-if="isActive">Active!</span>
</div>
</template>
Passing Props to Components
To pass props to a child component, include them as attributes in the component's tag:
<!-- In the parent component -->
<template>
<div>
<ChildComponent
message="Hello from parent!"
:user="{ name: 'John', role: 'Admin' }"
:is-active="true"
/>
</div>
</template>
<script>
import ChildComponent from './ChildComponent.vue';
export default {
components: {
ChildComponent
}
}
</script>
Note the :
(or v-bind:
) before some prop names. This is Vue's binding syntax that allows you to pass dynamic JavaScript values instead of static strings.
Prop Validation
Vue allows you to specify requirements for your props, which is especially helpful when creating components that will be used by other developers.
Basic Type Validation
You can specify the expected type for a prop:
<script>
export default {
props: {
title: String,
likes: Number,
isPublished: Boolean,
commentIds: Array,
author: Object,
callback: Function,
contactsPromise: Promise
}
}
</script>
Advanced Validation
For more complex validation needs, you can define detailed requirements:
<script>
export default {
props: {
// Basic type check (`null` and `undefined` values will pass any type validation)
propA: Number,
// Multiple possible types
propB: [String, Number],
// Required string
propC: {
type: String,
required: true
},
// Number with a default value
propD: {
type: Number,
default: 100
},
// Object with a default value (always use a factory function)
propE: {
type: Object,
default() {
return { message: 'hello' }
}
},
// Custom validator function
propF: {
validator(value) {
// The value must match one of these strings
return ['success', 'warning', 'danger'].includes(value)
}
}
}
}
</script>
One-Way Data Flow
Props in Vue follow a one-way data flow, from parent to child. This means:
- When the parent property updates, it flows down to the child
- When the child property updates, it does NOT affect the parent
This principle helps maintain a clear and predictable data flow in your application.
If you try to mutate a prop directly in a child component, Vue will warn you in the console. There are two common cases where you might be tempted to mutate a prop:
1. Using a prop as initial value for a local data property
<script>
export default {
props: ['initialCounter'],
data() {
return {
// Create a local data property that uses the prop as its initial value
counter: this.initialCounter
}
}
}
</script>
2. Transforming a prop value
<script>
export default {
props: ['size'],
computed: {
// Create a computed property that transforms the prop value
normalizedSize() {
return this.size.trim().toLowerCase()
}
}
}
</script>
Practical Examples
Example 1: Product Card Component
Let's create a reusable product card component that accepts various props:
ProductCard.vue (Child Component):
<template>
<div class="product-card" :class="{ 'on-sale': onSale }">
<h2>{{ name }}</h2>
<p class="description">{{ description }}</p>
<p class="price">
<span v-if="onSale" class="original-price">${{ price.toFixed(2) }}</span>
<span class="current-price">${{ discountedPrice.toFixed(2) }}</span>
</p>
<button @click="addToCart" :disabled="!inStock">
{{ inStock ? 'Add to Cart' : 'Out of Stock' }}
</button>
</div>
</template>
<script>
export default {
props: {
id: {
type: [Number, String],
required: true
},
name: {
type: String,
required: true
},
description: {
type: String,
default: 'No description available'
},
price: {
type: Number,
required: true,
validator(value) {
return value >= 0
}
},
discount: {
type: Number,
default: 0,
validator(value) {
return value >= 0 && value <= 100
}
},
inStock: {
type: Boolean,
default: true
}
},
computed: {
onSale() {
return this.discount > 0
},
discountedPrice() {
return this.price * (1 - this.discount / 100)
}
},
methods: {
addToCart() {
this.$emit('add-to-cart', this.id)
}
}
}
</script>
Parent Component Using ProductCard:
<template>
<div class="products-container">
<ProductCard
v-for="product in products"
:key="product.id"
:id="product.id"
:name="product.name"
:description="product.description"
:price="product.price"
:discount="product.discount"
:in-stock="product.inStock"
@add-to-cart="handleAddToCart"
/>
</div>
</template>
<script>
import ProductCard from './ProductCard.vue';
export default {
components: {
ProductCard
},
data() {
return {
products: [
{
id: 1,
name: 'Wireless Headphones',
description: 'High-quality wireless headphones with noise cancellation',
price: 129.99,
discount: 15,
inStock: true
},
{
id: 2,
name: 'Smartphone',
description: 'Latest model with advanced camera features',
price: 799.99,
discount: 0,
inStock: true
},
{
id: 3,
name: 'Laptop',
description: 'Powerful laptop for work and gaming',
price: 1299.99,
discount: 10,
inStock: false
}
]
}
},
methods: {
handleAddToCart(productId) {
console.log(`Product ${productId} added to cart!`);
// Add logic to update cart
}
}
}
</script>
Example 2: Button Component with Props
Here's how to create a reusable button component with customizable properties:
AppButton.vue (Child Component):
<template>
<button
:class="['app-button', `app-button--${type}`, { 'app-button--small': small }]"
:disabled="disabled || loading"
@click="$emit('click')"
>
<span v-if="loading" class="loader"></span>
<slot v-else></slot>
</button>
</template>
<script>
export default {
props: {
type: {
type: String,
default: 'primary',
validator(value) {
return ['primary', 'secondary', 'success', 'danger', 'warning'].includes(value)
}
},
disabled: {
type: Boolean,
default: false
},
loading: {
type: Boolean,
default: false
},
small: {
type: Boolean,
default: false
}
}
}
</script>
<style scoped>
.app-button {
padding: 10px 15px;
border: none;
border-radius: 4px;
cursor: pointer;
font-weight: bold;
transition: opacity 0.3s;
}
.app-button--small {
padding: 6px 10px;
font-size: 0.8em;
}
.app-button:disabled {
cursor: not-allowed;
opacity: 0.7;
}
.app-button--primary {
background-color: #4c75af;
color: white;
}
.app-button--secondary {
background-color: #6c757d;
color: white;
}
.app-button--success {
background-color: #28a745;
color: white;
}
.app-button--danger {
background-color: #dc3545;
color: white;
}
.app-button--warning {
background-color: #ffc107;
color: #212529;
}
.loader {
display: inline-block;
width: 12px;
height: 12px;
border: 2px solid rgba(255,255,255,0.3);
border-radius: 50%;
border-top-color: #fff;
animation: spin 1s infinite linear;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>
Parent Component Using AppButton:
<template>
<div class="button-showcase">
<h2>Button Types</h2>
<div class="button-row">
<AppButton type="primary">Primary</AppButton>
<AppButton type="secondary">Secondary</AppButton>
<AppButton type="success">Success</AppButton>
<AppButton type="danger">Danger</AppButton>
<AppButton type="warning">Warning</AppButton>
</div>
<h2>Button States</h2>
<div class="button-row">
<AppButton>Normal</AppButton>
<AppButton disabled>Disabled</AppButton>
<AppButton loading>Loading</AppButton>
</div>
<h2>Small Buttons</h2>
<div class="button-row">
<AppButton small>Small</AppButton>
<AppButton small type="success">Small Success</AppButton>
<AppButton small type="danger">Small Danger</AppButton>
</div>
<h2>Interactive Button</h2>
<div class="button-row">
<AppButton
:loading="isLoading"
:type="buttonType"
@click="handleClick"
>
{{ buttonText }}
</AppButton>
</div>
</div>
</template>
<script>
import AppButton from './AppButton.vue';
export default {
components: {
AppButton
},
data() {
return {
isLoading: false,
buttonType: 'primary',
buttonText: 'Click Me!'
}
},
methods: {
handleClick() {
this.isLoading = true;
this.buttonText = 'Processing...';
setTimeout(() => {
this.isLoading = false;
this.buttonType = 'success';
this.buttonText = 'Done!';
// Reset after showing success
setTimeout(() => {
this.buttonType = 'primary';
this.buttonText = 'Click Me!';
}, 1500);
}, 2000);
}
}
}
</script>
<style scoped>
.button-showcase {
max-width: 600px;
margin: 0 auto;
}
.button-row {
display: flex;
gap: 10px;
margin-bottom: 20px;
}
h2 {
margin-top: 30px;
}
</style>
Best Practices for Using Props
1. Use prop validation
Always define and validate your props to make your components more predictable and easier to debug.
// Good
export default {
props: {
status: {
type: String,
required: true,
validator: (value) => ['active', 'inactive', 'pending'].includes(value)
}
}
}
// Not recommended
export default {
props: ['status']
}
2. Use kebab-case in templates
In HTML templates, use kebab-case for prop names since HTML is case-insensitive:
<!-- In parent template -->
<UserProfile user-name="John" profile-image="/images/john.jpg" />
<!-- In UserProfile.vue -->
<script>
export default {
props: {
userName: String, // Access as this.userName in component
profileImage: String // Access as this.profileImage in component
}
}
</script>
3. Don't mutate props directly
Props are meant to be read-only. If you need to modify data passed via props, use one of these approaches:
<!-- Define a local data property based on a prop -->
<script>
export default {
props: ['initialValue'],
data() {
return {
internalValue: this.initialValue
}
}
}
</script>
<!-- Or use a computed property -->
<script>
export default {
props: ['label'],
computed: {
formattedLabel() {
return this.label.toUpperCase()
}
}
}
</script>
4. Use default values appropriately
Provide sensible defaults to make components more flexible:
<script>
export default {
props: {
level: {
type: Number,
default: 1
},
items: {
type: Array,
// For object or array defaults, use a factory function
default: () => []
}
}
}
</script>
5. Document your props for other developers
Add comments to describe what each prop is for and how it's used:
<script>
export default {
props: {
/**
* The title of the dialog
*/
title: String,
/**
* Whether the dialog can be closed by clicking outside
*/
closable: {
type: Boolean,
default: true
},
/**
* Callback function executed when dialog is closed
* @param {string} reason - The reason why dialog was closed
*/
onClose: Function
}
}
</script>
Summary
Props are a fundamental concept in Vue.js that allow parent components to pass data to their child components. Key points to remember:
- Props create a one-way data flow from parent to child
- Always validate your props to make your components more robust
- Use kebab-case in templates and camelCase in JavaScript
- Don't modify props directly; use local data or computed properties instead
- Provide default values when appropriate
By mastering props, you'll be able to create flexible, reusable components that can be composed together to build complex applications.
Additional Resources
Exercises
-
Create a simple
UserCard
component that acceptsname
,email
,role
, andavatarUrl
props and displays them in a card format. -
Build a
Pagination
component that acceptstotalPages
,currentPage
, andmaxVisibleButtons
props, and emits page change events. -
Create a component that transforms a prop before displaying it (e.g., a
CurrencyDisplay
component that formats a number as currency). -
Build a star rating component that accepts a
rating
prop (0-5) and displays the appropriate number of filled/empty stars. -
Create a component with prop validation that throws custom error messages when invalid props are provided.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)