Components and Props

intermediate
14 min

Components and Props

Components are the heart of Vue: self-contained, reusable pieces of UI that combine into full applications. A page becomes a tree -- App renders a Header, a ProductList, which renders many ProductCards. Data flows down this tree through props. This lesson shows how to define components, register them, and pass data safely.

Defining and Using a Component

With Single File Components, each component is a .vue file. Here is ProductCard.vue:

vue
<script setup> defineProps({ name: { type: String, required: true }, price: { type: Number, required: true }, inStock: { type: Boolean, default: true } }) </script> <template> <article class="card"> <h3>{{ name }}</h3> <p>${{ price.toFixed(2) }}</p> <p v-if="!inStock">Currently unavailable</p> </article> </template>

And the parent uses it by importing it -- with <script setup>, imported components are automatically available in the template:

vue
<script setup> import ProductCard from './ProductCard.vue' import { ref } from 'vue' const products = ref([ { id: 1, name: 'Keyboard', price: 45, inStock: true }, { id: 2, name: 'Monitor', price: 199, inStock: false } ]) </script> <template> <ProductCard v-for="p in products" :key="p.id" :name="p.name" :price="p.price" :in-stock="p.inStock" /> </template>

Note the casing convention: props are declared in camelCase (inStock) and passed in kebab-case in templates (:in-stock). Component tags in SFCs are typically written in PascalCase.

Declaring Props

defineProps is a compile-time macro (no import needed). The object syntax buys you runtime validation:

js
defineProps({ title: String, // type check only likes: { type: Number, default: 0 }, tags: { type: Array, default: () => [] }, // arrays/objects need factory defaults status: { type: String, validator: (v) => ['draft', 'published'].includes(v) } })

If a parent passes the wrong type, Vue logs a console warning in development -- catching bugs early. Static values need no colon (title="Hello"); anything dynamic or non-string does (:likes="42").

One-Way Data Flow

Props flow down only. A child must never mutate a prop:

js
const props = defineProps({ initialCount: Number }) // BAD: props.initialCount++ // GOOD: use the prop as a seed for local state… const count = ref(props.initialCount) // …or derive from it const doubled = computed(() => props.initialCount * 2)

This rule keeps data ownership clear: whoever declares the state mutates it. When a child needs to change parent data, it emits an event instead -- the subject of the next lesson.

Fallthrough Attributes

Attributes you pass that are not declared as props -- class, id, data-* -- automatically land on the component's root element. <ProductCard class="featured" /> adds the class to the <article>. Multiple root nodes or custom targets require inheritAttrs: false and v-bind="$attrs", a technique worth remembering once your components grow wrappers.

Organizing a Component Tree

A practical decomposition for a storefront page:

text
App ├── SiteHeader ├── ProductFilters └── ProductList └── ProductCard (× n) └── PriceTag

Guidelines that scale well:

  • Extract a component when a chunk of template is reused, or when a section has its own clear responsibility.
  • Keep components small; a file over ~200 lines is a hint to split.
  • Name files in PascalCase (ProductCard.vue) and prefer multi-word names to avoid clashing with HTML elements.

Key Takeaways

  • Components are .vue files; import them and use them as tags in <script setup>.
  • Declare props with defineProps, including types, defaults, and validators.
  • camelCase in JavaScript, kebab-case in templates; factory functions for array/object defaults.
  • Props are one-way and read-only -- copy or derive, never mutate.
  • Undeclared attributes fall through to the component's root element.

Next up: Component Events and v-model on Components -- how children talk back to parents.

Components and Props - Vue.js | CodeYourCraft | CodeYourCraft