Ir al contenido
wannacode

Props: datos desde arriba

En resumen: Los props son los datos que un componente recibe de su padre, igual que una función recibe parámetros; el hijo no los cambia, y los slots traen contenido desde fuera.

Problema

Tres veces pan

El catálogo tiene que mostrar tres productos: pan, leche y queso. La tarjeta de producto está lista de la lección anterior, así que App la pone tres veces.

Vue

Las tres tarjetas son pan. El producto está escrito dentro de ProductCard, y todas las tarjetas lo muestran. El otro camino sería BreadCard, MilkCard y CheeseCard: tres copias del mismo componente, y cada cambio en la tarjeta habría que hacerlo tres veces. Lo que falta es una forma de decirle a la tarjeta qué producto mostrar.

La idea

Props: datos desde arriba

Un componente es una función. Una función recibe sus datos como parámetros, y un componente los recibe como atributos de su padre: el componente que lo usa. Estos atributos se llaman props (de «properties»). El padre los escribe, el hijo los lee.

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

defineProps dice qué props acepta el componente. La plantilla los ve directamente, por su nombre. En el script están en el objeto que devuelve defineProps: const props = defineProps(…), y luego props.product.name. Los dos puntos de :product son los mismos que en cualquier atributo: el valor es una expresión, no texto.

Hay una sola tarjeta, y cada uso tiene datos distintos. Con una función pasa lo mismo: una definición, muchas llamadas con argumentos distintos.

Míralo

Mira cómo fluyen

<script setup>
defineProps({
product: Object,
discount: { type: Number, default: 0 },
});
</script>
<template>
<article>
<h2>{{ product.name }}</h2>
<p>Precio: {{ money(product.price * (1 - discount / 100)) }}</p>
</article>
</template>
<!-- App.vue -->
<script setup>
const bread = { name: 'Pan blanco', price: 1.2 };
const milk = { name: 'Leche fresca', price: 1.6 };
const cheese = { name: 'Queso blanco', price: 7.9 };
</script>
<template>
<main>
<h1>Pan y leche</h1>
<div>
<ProductCard :product="bread" />
<ProductCard :product="milk" />
<ProductCard :product="cheese" :discount="10" />
</div>
</main>
</template>
Paso a paso, en palabras
  1. App guarda los datos: tres productos. Debajo hay tres ProductCard: el mismo componente tres veces, y ninguna sabe aún qué mostrar.
  2. :product="bread" le pasa el objeto bread a la primera tarjeta. Los atributos de un componente se llaman props.
  3. Las otras dos tarjetas reciben su propio producto. La tercera recibe además discount = 10. Cada tarjeta tiene sus propios props.
  4. Cada tarjeta se dibuja a partir de sus props. El pan y la leche no recibieron discount, por eso vale 0 por defecto. El queso lleva un 10 % de descuento: 7,11 €.
  5. Si la tarjeta intenta props.discount = 0, no sube nada: Vue avisa «target is readonly» y no hace el cambio. Los datos de App siguen igual.
  6. Los datos fluyen en una sola dirección: del padre hacia abajo, al hijo. Para cambiar algo arriba, el hijo tiene que pedírselo al padre. Ese es el módulo «Estado».

Cada tarjeta conoce solo sus propios props. No ve las otras tarjetas, y no sabe de dónde salen los datos de App.

Míralo

Una tarjeta, tres productos

Vue

Los productos están en App, y hay una sola ProductCard. Añade un cuarto producto en App (la imagen img/apples.svg existe) y un <ProductCard … /> más. La tarjeta no la tocas. Con 30 productos querrás una lista en lugar de 30 líneas: es la próxima lección.

Pruébalo

Encuentra el error

La tarjeta no se muestra, y en la consola dice «TypeError: Cannot read properties of undefined (reading 'toFixed')».

Elige la línea con el error.

La idea

Un valor por defecto

Algunos productos están en oferta. La tarjeta recibe un segundo prop, discount, en porcentaje. La mayoría de los productos no tienen descuento, y no queda bien que cada línea de App escriba discount={0}. Un prop que falta es undefined, igual que un argumento que falta en una función. El precio se convierte en price * (1 - undefined / 100), o sea NaN.

Por eso el prop recibe un valor por defecto:

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

default es el valor cuando el padre no pasa nada. type es una comprobación: si pasas discount="10" (texto, sin los dos puntos), Vue avisa en la consola «Invalid prop: type check failed». La comprobación solo se ejecuta en modo de desarrollo, y solo avisa.

Pruébalo

Un descuento

App le pasa al queso un discount de 10 (por ciento). Haz que la tarjeta acepte discount, 0 por defecto, y que muestre el precio con el descuento: el pan 1,20 €, la leche 1,60 €, el queso 7,11 €.

Vue

La idea

Solo lectura

Los datos son de quien los tiene. La tarjeta puede leer sus props, pero no puede cambiarlos. Prueba el botón «Uno más»: el hijo intenta aumentar su propio qty.

Vue

La cantidad se queda en 2, y la consola dice «[Vue warn] Set operation on key "qty" failed: target is readonly.». Vue le da al componente props de solo lectura y avisa ante un intento de escritura.

Aunque pasara, el cambio sería solo en el hijo: App seguiría diciendo 2, y en el siguiente dibujo App volvería a pasar 2. Dos lugares con verdades distintas son el error de «Una lista a partir de datos». La regla: los datos fluyen hacia abajo, y cambiarlos es trabajo del padre. Cómo le pide un hijo a su padre que cambie algo es el módulo «Estado».

La idea

Contenido desde fuera

La tienda tiene marcos que se ven igual y llevan cosas distintas dentro: «Envío» con dos párrafos, «Devoluciones» con uno. El título es un prop. El contenido es un trozo entero de HTML, y es más cómodo escribirlo entre la etiqueta de apertura y la de cierre, como con <section>.

<Card title="Envío">
<p>En un máximo de 2 días laborables.</p>
</Card>
<template>
<section class="card">
<h2>{{ title }}</h2>
<slot />
</section>
</template>

El contenido entre <Card> y </Card> va donde está <slot />. Card decide dónde está el slot.

Vue

Pruébalo

El marco

Card muestra solo el título. Haz que muestre también, bajo el título, el contenido que App escribe entre <Card> y </Card>.

Vue
Más precisamente, para curiosos

Slots con nombre. Un marco puede tener varios lugares: <slot /> para la parte principal y <slot name="footer" /> para la de abajo. El padre rellena un slot con nombre con <template #footer>…</template>.

Nombres. En el script un prop va en camelCase (productId), y en la plantilla del padre también puede ir en kebab-case (:product-id="7"). Vue los conecta.

Props como variables. Desde Vue 3.5 puedes escribir const { product, discount = 0 } = defineProps(['product', 'discount']): el valor por defecto funciona como en JavaScript, y las variables siguen conectadas con el padre.

Solo el primer nivel está congelado. props.qty = 3 avisa, pero props.product.price = 2 pasa en silencio: Vue solo protege de la escritura el primer nivel, y el objeto del producto es el mismo que en App. Así el hijo cambia los datos del padre a sus espaldas. No lo hagas.

Tipos. Además de type y default hay required: true (un aviso si el padre no pasa el prop) y validator (una función que comprueba el valor). Todo se ejecuta solo en modo de desarrollo. Con TypeScript los tipos se escriben en defineProps<{ … }>().

Ejercicio

El escaparate

Construye el escaparate desde cero. ProductCard recibe product y discount (0 por defecto) y muestra la imagen (alt es el nombre), el nombre como título de nivel dos y «Precio: …» con el descuento. Card recibe title y muestra el título (nivel dos) y el contenido desde fuera. En App, bajo el título: las tres tarjetas en un div (el queso con discount 10), después una Card «Envío» con «En un máximo de 2 días laborables.» y «Gratis a partir de 30 €.».

Vue

Test

Ponte a prueba

1. ¿Qué son los props?
2. ¿Qué recibe la tarjeta de <ProductCard product="bread" />?
3. La tarjeta recibe qty y quiere sumarle 1. ¿Qué es verdad?
4. App escribe <Card title="Envío"><p>En un máximo de 2 días laborables.</p></Card>. ¿Cómo muestra Card el párrafo?
5. App escribe <ProductCard product={bread} /> sin discount. ¿Qué es discount en la tarjeta?

Creado por Ivo Gergov como parte de la campaña de Evo Tech por una educación accesible en programación ·