Skip to content
wannacode

Props: data from above

In short: Props are the data a component gets from its parent, the way a function gets parameters; the child does not change them, and slots pass in content from outside.

Problem

Bread three times

The catalog must show three products: bread, milk and cheese. The product card is ready from the last lesson, so App puts it in three times.

Vue

All three cards are bread. The product is written inside ProductCard, and every card shows it. The other way is BreadCard, MilkCard and CheeseCard: three copies of the same component, and every change to the card has to be made three times. What is missing is a way to tell the card which product to show.

The idea

Props: data from above

A component is a function. A function gets its data as parameters, and a component gets them as attributes from its parent: the component that uses it. These attributes are called props (from properties). The parent writes them, the child reads them.

App.vue
<ProductCard :product="bread" />
<ProductCard :product="milk" />
ProductCard.vue
<script setup>
defineProps({ product: Object });
</script>
<template>
<h2>{{ product.name }}</h2>
</template>

defineProps says which props the component takes. The template sees them directly, by name. In the script they are in the object that defineProps returns: const props = defineProps(…), and then props.product.name. The colon in :product is the same as for any attribute: the value is an expression, not text.

There is one card, and every use has different data. A function works the same way: one definition, many calls with different arguments.

See it

See how they flow

<script setup>
defineProps({
product: Object,
discount: { type: Number, default: 0 },
});
</script>
<template>
<article>
<h2>{{ product.name }}</h2>
<p>Price: {{ money(product.price * (1 - discount / 100)) }}</p>
</article>
</template>
<!-- App.vue -->
<script setup>
const bread = { name: 'White bread', price: 1.2 };
const milk = { name: 'Fresh milk', price: 1.6 };
const cheese = { name: 'White cheese', price: 7.9 };
</script>
<template>
<main>
<h1>Bread and milk</h1>
<div>
<ProductCard :product="bread" />
<ProductCard :product="milk" />
<ProductCard :product="cheese" :discount="10" />
</div>
</main>
</template>
Step by step, in words
  1. App holds the data: three products. Below it are three ProductCards: the same component three times, and none of them knows yet what to show.
  2. :product="bread" hands the object bread to the first card. The attributes of a component are called props.
  3. The other two cards get their own product. The third one also gets discount = 10. Each card has its own props.
  4. Each card draws itself from its props. Bread and milk got no discount, so it is 0 by default. The cheese is 10% off: €7.11.
  5. If the card tries props.discount = 0, nothing goes up: Vue warns "target is readonly" and does not make the change. App’s data stay the same.
  6. Data flow one way only: from the parent down to the child. To change something above, the child has to ask the parent. That is the module "State".

Every card knows only its own props. It does not see the other cards, and it does not know where the data in App come from.

See it

One card, three products

Vue

The products are in App, and there is one ProductCard. Add a fourth product in App (the image img/apples.svg exists) and one more <ProductCard … />. You do not touch the card. With 30 products you will want a list rather than 30 lines: that is the next lesson.

Try it

Find the bug

The card does not show, and the console says "TypeError: Cannot read properties of undefined (reading 'toFixed')".

Pick the line with the bug.

The idea

A default value

Some products are on sale. The card gets a second prop, discount, in percent. Most products have no discount, and it is not nice for every line in App to write discount={0}. A missing prop is undefined, like a missing function argument. The price becomes price * (1 - undefined / 100), which is NaN.

So the prop gets a default value:

defineProps({
product: Object,
discount: { type: Number, default: 0 },
});

default is the value when the parent passes nothing. type is a check: if you pass discount="10" (text, no colon), Vue warns in the console "Invalid prop: type check failed". The check runs only in development mode, and it only warns.

Try it

A discount

App gives the cheese a discount of 10 (percent). Make the card take discount, 0 by default, and show the price with the discount: the bread €1.20, the milk €1.60, the cheese €7.11.

Vue

The idea

Read-only

The data belong to whoever holds them. The card can read its props, but it cannot change them. Try the "One more" button: the child tries to increase its own qty.

Vue

The quantity stays 2, and the console says "[Vue warn] Set operation on key "qty" failed: target is readonly.". Vue gives the component read-only props and warns on an attempt to write.

Even if it went through, the change would be only in the child: App would still say 2, and on the next drawing App would pass 2 again. Two places with different truths are the bug from "A list from data". The rule: data flow down, and changing them is the parent’s job. How a child asks its parent to change something is the module "State".

The idea

Content from outside

The shop has frames that look the same with different things inside: "Delivery" with two paragraphs, "Returns" with one. The title is a prop. The content is a whole piece of HTML, and it is handier to write it between the opening and the closing tag, as with <section>.

<Card title="Delivery">
<p>Within 2 working days.</p>
</Card>
<template>
<section class="card">
<h2>{{ title }}</h2>
<slot />
</section>
</template>

The content between <Card> and </Card> goes where <slot /> is. Card decides where the slot is.

Vue

Try it

The frame

Card shows only the title. Make it also show the content that App writes between <Card> and </Card>, under the title.

Vue
More precisely, if you are curious

Named slots. A frame can have several places: <slot /> for the main part and <slot name="footer" /> for the bottom. The parent fills a named slot with <template #footer>…</template>.

Names. In the script a prop is camelCase (productId), and in the parent’s template it may also be kebab-case (:product-id="7"). Vue connects them.

Props as variables. Since Vue 3.5 you can write const { product, discount = 0 } = defineProps(['product', 'discount']): the default value works as in JavaScript, and the variables stay connected to the parent.

Only the first level is frozen. props.qty = 3 warns, but props.product.price = 2 goes through silently: Vue guards only the first level against writes, and the product object is the same one as in App. That way the child changes the parent’s data behind its back. Do not do it.

Types. Besides type and default there is required: true (a warning if the parent does not pass the prop) and validator (a function that checks the value). All of it runs only in development mode. With TypeScript the types are written in defineProps<{ … }>().

Exercise

The shop window

Build the shop window from scratch. ProductCard takes product and discount (0 by default) and shows the image (alt is the name), the name as a level-two heading and "Price: …" with the discount. Card takes title and shows the title (level two) and the content from outside. In App, under the heading: the three cards in a div (the cheese with discount 10), then a Card "Delivery" with "Within 2 working days." and "Free over €30.".

Vue

Test

Check yourself

1. What are props?
2. What does the card get from <ProductCard product="bread" />?
3. The card gets qty and wants to add 1 to it. Which is true?
4. App writes <Card title="Delivery"><p>Within 2 working days.</p></Card>. How does Card show the paragraph?
5. App writes <ProductCard product={bread} /> without discount. What is discount in the card?

Made by Ivo Gergov as part of Evo Tech's campaign for accessible programming education ·