Capítulo 8 de 11

Tutorial de VueJS 3 – Componentes y props con defineProps

verificado el 2 septiembre 2026 · 5 min

Respuesta rápida

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.

Al final de este capítulo, sabrás dividir una interfaz en componentes y pasarles datos.

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.

src/components/ProductDisplay.vue
<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:

src/App.vue
<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.

El botón ya no llena el carrito

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:

src/App.vue
<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:

src/App.vue
<script setup>
const premium = ref(true)
</script>

<template>
  <div class="cart">Panier ({{ cart }})</div>
  <ProductDisplay :premium="premium" />
</template>

Y el hijo la declara:

src/components/ProductDisplay.vue
<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:

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

Los dos puntos son obligatorios

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

Con la Options API

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.

EjercicioSaca la lista de detalles a un componente 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

Olvidar los dos puntos en una prop 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.
Modificar una prop en el hijo Las props solo bajan, Vue avisa en la consola. Copia el valor en un ref local, o haz subir un evento.
Valor por defecto de un array o de un objeto Lo tiene que devolver una función, si no, todas las instancias comparten el mismo objeto.
Newsletter

Las nuevas pruebas, tutoriales y proyectos, por correo.

Pruebas reproducibles, código versionado, resultados fechados. Nunca spam.