Tutorial de VueJS 3 – Componentes y props con defineProps
verificado el 2 septiembre 2026 · 5 min
Un componente es un archivo .vue con su plantilla, su estado y su lógica. Impórtalo en <script setup> para usarlo directamente en la plantilla. El padre le pasa datos mediante props, declaradas con defineProps y enlazadas con los dos puntos.
Llevamos siete capítulos con todo dentro de un único archivo. Eso no escala. Un componente es un trozo de interfaz autónomo, con su plantilla, su estado y su lógica, que se reutiliza y se anida.
Extraer la ficha de producto
Crea src/components/ProductDisplay.vue y lleva ahí todo lo que tiene que ver con el producto: la imagen, el título, la disponibilidad, los detalles, las muestras de color y el botón.
<script setup>
import { computed, ref } from 'vue'
const product = ref('T-Shirt')
const brand = ref('Gekkode')
const selectedVariant = ref(0)
const details = ref(['60 % coton', '30 % laine', '10 % polyester'])
const variants = ref([
{ id: 2234, color: '#2563eb', image: '/images/t-shirt-bleu.svg', quantity: 20 },
{ id: 2235, color: '#dc2626', image: '/images/t-shirt-rouge.svg', quantity: 0 }
])
const title = computed(() => `${brand.value} ${product.value}`)
const image = computed(() => variants.value[selectedVariant.value].image)
const inStock = computed(() => variants.value[selectedVariant.value].quantity > 0)
function updateVariant(index) {
selectedVariant.value = index
}
</script>
<template>
<div class="product-display">
<div class="product-image">
<img :src="image" :alt="title">
</div>
<div class="product-info">
<h1>{{ title }}</h1>
<p v-if="inStock">En stock</p>
<p v-else>En rupture</p>
<ul>
<li v-for="detail in details" :key="detail">{{ detail }}</li>
</ul>
<div class="variants-wrapper">
<div
v-for="(variant, index) in variants"
:key="variant.id"
class="color-circle"
:style="{ backgroundColor: variant.color }"
@mouseover="updateVariant(index)"
></div>
</div>
<button class="button" :class="{ disabledButton: !inStock }" :disabled="!inStock">
Ajouter au panier
</button>
</div>
</div>
</template>El carrito no se va con él: no pertenece al producto. App.vue se queda en esto:
<script setup>
import { ref } from 'vue'
import ProductDisplay from './components/ProductDisplay.vue'
const cart = ref(0)
</script>
<template>
<div class="cart">Panier ({{ cart }})</div>
<ProductDisplay />
</template>Un componente importado en <script setup> se puede usar directamente en la plantilla: no hay que declararlo aparte.
Es normal, y es el tema del capítulo siguiente. cart vive ahora en el padre, fuera del alcance del componente hijo. De hecho hemos quitado el @click del botón: dejarlo habría provocado un error, porque addToCart ya no existe en este componente.
Nombrar un componente
Conviven dos convenciones. En PascalCase, <ProductDisplay />, se distingue de un vistazo un componente de una etiqueta HTML, y el editor puede saltar a su definición. En kebab-case, <product-display></product-display>, se mantiene la compatibilidad con una plantilla escrita directamente en una página HTML.
En un archivo .vue, quédate con PascalCase.
Cada componente tiene su propio ámbito
Añade tres fichas de golpe:
<template>
<ProductDisplay />
<ProductDisplay />
<ProductDisplay />
</template>Aparecen tres fichas, independientes: pasar el ratón por una muestra de color de la primera no afecta a las demás. Cada instancia ejecuta su propio <script setup> y tiene su propio selectedVariant.
Cada ficha conserva también su propia variante seleccionada.
Es también la razón por la que el estado de un componente se declara dentro de una función. Si variants fuera un simple objeto compartido a nivel de módulo, las tres fichas se pisarían entre ellas.
Quita las dos fichas de más antes de seguir.
Las props
El ámbito aislado plantea una pregunta: ¿cómo le pasa el padre una información al hijo? Con una prop, un atributo personalizado que el componente declara aceptar.
Vamos a añadir el envío, gratuito para los clientes premium. La información pertenece al padre:
<script setup>
const premium = ref(true)
</script>
<template>
<div class="cart">Panier ({{ cart }})</div>
<ProductDisplay :premium="premium" />
</template>Y el hijo la declara:
<script setup>
const props = defineProps({
premium: { type: Boolean, required: true }
})
const shipping = computed(() => (props.premium ? 'Gratuite' : '2,99 €'))
</script>
<template>
<p>Livraison : {{ shipping }}</p>
</template>defineProps está disponible sin importar nada dentro de <script setup>: es una macro del compilador, no una función normal.
En la plantilla se escribe {{ premium }} directamente. En el script hay que pasar por el objeto devuelto: props.premium.
Declarar las props en serio
La forma larga permite validar:
const props = defineProps({
premium: { type: Boolean, required: true },
titre: { type: String, default: 'Sans titre' },
tailles: { type: Array, default: () => [] },
note: {
type: Number,
validator: (valeur) => valeur >= 0 && valeur <= 5
}
})El valor por defecto de un array o de un objeto lo tiene que devolver una función, si no, todas las instancias compartirían el mismo objeto.
:premium="premium" pasa el valor de la variable. premium="premium", sin los dos puntos, pasa la cadena "premium". Como una cadena no vacía es verdadera, el bug pasa desapercibido hasta el día en que el valor debería ser falso. Vue avisa además en la consola de que esperaba Boolean y ha recibido String.
Las props bajan, y no suben
Una prop circula en un solo sentido: del padre al hijo. El hijo no debe modificarla, Vue muestra un aviso si lo intentas.
Si el hijo necesita una versión local modificable, cópiala en un ref. Si tiene que provocar un cambio en el padre, hay que enviarle una señal: de eso trata el capítulo siguiente.
Las props se declaran en la opción props, con la misma sintaxis de validación, y se leen con this.premium. Los componentes hijos, además, hay que registrarlos en la opción components.
ProductDetails.vue que reciba el array como prop. Después muestra dos fichas de producto y comprueba que cada una tiene su propia variante seleccionada.Errores frecuentes
premium="premium" pasa la cadena «premium», que siempre es verdadera. Escribe :premium="premium". Vue avisa además de que esperaba Boolean y ha recibido String.