Skip to content
← Todas las preguntas
⚡ Intermedio

¿Por qué puede ser mala idea pasar todas las props a un componente hijo con spread?

🧩 Componentes

Pasar todas las props con v-bind="$attrs" o enviar un objeto entero como props manda al hijo todo lo que tiene el padre, incluyendo atributos que el hijo no necesita. Esto provoca re-renders innecesarios, oculta la API real del componente y puede filtrar datos sensibles o crear colisiones de nombres. Pasa solo lo que el hijo necesita, de forma explícita.

El problema

vue
<!-- Padre -->
<script setup>
const user = ref({
  id: 1,
  name: 'Alice',
  email: 'alice@example.com',
  role: 'admin',
  internalNotes: 'High priority account',
  passwordHash: '...'
})
</script>

<template>
  <!-- MAL: pasa todo, incluyendo datos que el hijo no debería ver -->
  <UserCard v-bind="user" />
</template>
Open in Vue Playground
vue
<!-- UserCard.vue -->
<script setup>
defineProps<{ name: string; email: string }>()
// Solo usa name y email, pero recibe id, role, internalNotes, passwordHash también
</script>
Open in Vue Playground

El hijo declara name y email como props. El resto (id, role, internalNotes, passwordHash) cae como atributos HTML en el elemento raíz. Abre las DevTools y verás <div passwordhash="..." internalnotes="..."> en el DOM.

Cinco razones para evitarlo

1. Filtración de datos al DOM

Las props no declaradas se convierten en atributos fallthrough. Acaban como atributos HTML en el elemento raíz del hijo, visibles para cualquiera que inspeccione la página:

html
<!-- DOM renderizado -->
<div
  class="user-card"
  id="1"
  role="admin"
  internalnotes="High priority account"
  passwordhash="..."
>
  Alice
</div>

role es especialmente peligroso porque es un atributo HTML válido (ARIA role), así que sobrescribe silenciosamente la semántica de accesibilidad.

2. Re-renders innecesarios

Cada propiedad del spread se convierte en una prop. Cuando cualquier propiedad cambia, Vue re-renderiza el hijo, aunque el hijo no use esa propiedad:

vue
<template>
  <!-- Cada vez que user.lastLoginAt cambia, UserCard se re-renderiza -->
  <UserCard v-bind="user" />

  <!-- Solo se re-renderiza cuando name o email cambian -->
  <UserCard :name="user.name" :email="user.email" />
</template>
Open in Vue Playground

3. API del componente oculta

Las props explícitas documentan la interfaz del componente. Con spread, no puedes saber desde el template qué datos necesita el hijo:

vue
<!-- ¿Qué necesita ProfileHeader exactamente? No hay manera de saberlo. -->
<ProfileHeader v-bind="user" />

<!-- Claro: necesita name, avatar y role -->
<ProfileHeader :name="user.name" :avatar="user.avatar" :role="user.role" />
Open in Vue Playground

4. Colisiones de nombres

El spread puede incluir propiedades que entran en conflicto con los atributos propios del hijo:

vue
<script setup>
defineProps<{ class: string; style: string }>()
// Si el padre hace spread de un objeto con class o style, se mezclan o se sobreescriben
</script>
Open in Vue Playground

Más sutilmente, si el objeto del padre tiene una propiedad key, interfiere con la reconciliación de v-for de Vue.

5. TypeScript pierde el control

Cuando haces spread de un objeto genérico, TypeScript no puede validar que el hijo reciba los tipos correctos:

vue
<!-- TypeScript no puede verificar esto -->
<UserCard v-bind="someObject" />

<!-- TypeScript valida cada prop -->
<UserCard :name="someObject.name" :email="someObject.email" />
Open in Vue Playground

Cuándo es apropiado v-bind="$attrs"

Reenviar atributos intencionalmente en componentes envolventes es el caso de uso válido:

vue
<!-- BaseInput.vue: un wrapper fino que reenvía todos los attrs al input real -->
<script setup>
defineOptions({ inheritAttrs: false })
defineProps<{ label: string }>()
</script>

<template>
  <label>
    {{ label }}
    <input v-bind="$attrs" />
  </label>
</template>
Open in Vue Playground
vue
<!-- Padre -->
<BaseInput label="Email" type="email" placeholder="you@example.com" required />
Open in Vue Playground

Aquí el wrapper existe específicamente para reenviar atributos. El type, placeholder y required del padre llegan al <input> como se espera. Esto es distinto a hacer spread ciegamente de un objeto de datos.

La solución: pasa solo lo necesario

vue
<!-- MAL -->
<UserCard v-bind="user" />

<!-- BIEN -->
<UserCard :name="user.name" :email="user.email" />
Open in Vue Playground

Si varios componentes necesitan el mismo subconjunto de datos del usuario, crea un computed o un tipo para ello:

vue
<script setup>
interface UserSummary {
  name: string
  email: string
}

const userSummary = computed<UserSummary>(() => ({
  name: user.value.name,
  email: user.value.email
}))
</script>

<template>
  <UserCard v-bind="userSummary" />
</template>
Open in Vue Playground

Ahora el spread es intencional y tiene tipado. El objeto contiene exactamente lo que UserCard espera, nada más.

Ver también: ¿Qué son los fallthrough attributes? · ¿Cómo declarar props con TypeScript?

Referencias

Publicado bajo la licencia MIT.