8,206 lecturas
8,206 lecturas

Vue.js: Propagando Props como un Pro

por Andrei Sieedugin7m2025/05/03
Read on Terminal Reader

Demasiado Largo; Para Leer

Cuando crees componentes para tu proyecto, todo comienza divertido y fácil. Cree 'MyButton.vue' y añade algo de estilo, y esto es así. Entonces te das cuenta de inmediato de que necesitas una docena de props, porque tu equipo de diseño quiere que sea de diferentes colores y tamaños, con iconos a la izquierda y a la derecha, con contadores. Después de todo, no puedes tener los botones "Cancelar" y "Ok" del mismo color, y necesitas ellos para reaccionar a las interacciones del usuario.
featured image - Vue.js: Propagando Props como un Pro
Andrei Sieedugin HackerNoon profile picture

Cuando crea componentes para su proyecto, todo comienza divertido y fácil.MyButton.vueAñade un poco de estilo y ya está.

<template>
  <button class="my-fancy-style">
    <slot></slot>
  </button>
</template>


Entonces te das cuenta de inmediato de que necesitas una docena de props, porque tu equipo de diseño quiere que sea de diferentes colores y tamaños, con iconos a la izquierda y a la derecha, con contadores...

const props = withDefaults(defineProps<{
  theme?: ComponentTheme;
  small?: boolean;
  icon?: IconSvg; // I’ve described how I cook icons in my previous article
  rightIcon?: IconSvg;
  counter?: number;
}>(), {
  theme: ComponentTheme.BLUE,
  icon: undefined,
  rightIcon: undefined,
  counter: undefined
});


Después de todo, no puedes tener los botones “Cancelar” y “Ok” del mismo color, y los necesitas para reaccionar a las interacciones de los usuarios.

const props = withDefaults(defineProps<{
  theme?: ComponentTheme;
  small?: boolean;
  icon?: IconSvg;
  rightIcon?: IconSvg;
  counter?: number;
  disabled?: boolean;
  loading?: boolean;
}>(), {
  theme: ComponentTheme.BLUE,
  icon: undefined,
  rightIcon: undefined,
  counter: undefined
});


Bueno, tienes la idea: habrá algo salvaje, como pasarwidth: 100%o agregando autofocus - todos sabemos cómo es simple en Figma hasta que la vida real golpee duro.


Ahora imagina un botón de enlace: parece el mismo, pero cuando lo presiona, debe ir al enlace externo o interno.<RouterLink>o<a>etiquetas cada vez, pero por favor no. También puede agregartoyhrefPropiedades a su componente inicial, pero se sentirá asfixiado muy pronto:

<component
  :is="to ? RouterLink : href ? 'a' : 'button'"
  <!-- ugh! -->


Por supuesto, necesitará un componente de "segundo nivel" que envuelva su botón (también manejará los enlaces hipervínculos predeterminados y algunas otras cosas interesantes, pero los omitiré por motivos de simplicidad):

<template>
  <component
    :is="props.to ? RouterLink : 'a'"
    :to="props.to"
    :href="props.href"
    class="my-link-button"
  >
    <MyButton v-bind="$attrs">
      <slot></slot>
    </MyButton>
  </component>
</template>

<script lang="ts" setup>
import MyButton from './MyButton.vue';
import { RouteLocationRaw, RouterLink } from 'vue-router';

const props = defineProps<{
  to?: RouteLocationRaw;
  href?: string;
}>();
</script>

Y aquí es donde comienza nuestra historia.

Square One

Plaza Uno

Bueno, básicamente va a funcionar, no voy a mentir.<MyLinkButton :counter=“2">Pero no habrá autocomplete para los complementos derivados, lo que no es cool:


Only "href" and "to"


Podemos propagar props en silencio, pero el IDE no sabe nada sobre ellos, y eso es una vergüenza.


La solución simple y obvia es propagarlos explícitamente:

<template>
  <component
    :is="props.to ? RouterLink : 'a'"
    :to="props.to"
    :href="props.href"
    class="my-link-button"
  >
    <MyButton
      :theme="props.theme"
      :small="props.small"
      :icon="props.icon"
      :right-icon="props.rightIcon"
      :counter="props.counter"
      :disabled="props.disabled"
      :loading="props.loading"
    >
      <slot></slot>
    </MyButton>
  </component>
</template>

<script lang="ts" setup>
// imports...

const props = withDefaults(
  defineProps<{
    theme?: ComponentTheme;
    small?: boolean;
    icon?: IconSvg;
    rightIcon?: IconSvg;
    counter?: number;
    disabled?: boolean;
    loading?: boolean;

    to?: RouteLocationRaw;
    href?: string;
  }>(),
  {
    theme: ComponentTheme.BLUE,
    icon: undefined,
    rightIcon: undefined,
    counter: undefined,
  }
);
</script>


El IDE tendrá el autocomplete adecuado.Tendremos mucho dolor y lamento apoyarlo.


Obviamente, el principio de “No se repita a sí mismo” no se aplicó aquí, lo que significa que tendremos que sincronizar cada actualización. Un día, tendrás que añadir otro prop, y tendrás que encontrar cada componente que envuelva el básico. Sí, el botón y LinkButton son probablemente suficientes, pero imagina TextInput y una docena de componentes que dependen de él: PasswordInput, EmailInput, NumberInput, DateInput, HellKnowsWhatElseInput.


Después de todo, es feo.Y cuanto más props tenemos, más feo se vuelve.

Clean It Up

Limpia lo de arriba

Es bastante difícil reutilizar un tipo anónimo, por lo que le damos un nombre.

// MyButton.props.ts

export interface MyButtonProps {
  theme?: ComponentTheme;
  small?: boolean;
  icon?: IconSvg;
  rightIcon?: IconSvg;
  counter?: number;
  disabled?: boolean;
  loading?: boolean;
}


No podemos exportar una interfaz de la.vuearchivo debido a algún internoscript setupmagia, por lo que necesitamos crear una.tsEn el lado brillante, mira lo que tenemos aquí:

const props = withDefaults(defineProps<MyButtonProps>(), {
  theme: ComponentTheme.BLUE,
  icon: undefined,
  rightIcon: undefined,
  counter: undefined,
});


Mucho más limpio, ¿no es? y aquí está el heredado:

interface MyLinkButtonProps {
  to?: RouteLocationRaw;
  href?: string;
}

const props = defineProps<MyButtonProps & MyLinkButtonProps>();


Sin embargo, aquí hay un problema: ahora, cuando los complementos básicos se tratan comoMyLinkButtonde los propósitos, no se propagan conv-bind=”$attrs”Ya no, así que tenemos que hacerlo nosotros mismos.

<!-- MyLinkButton.vue -->

<component
  :is="props.to ? RouterLink : 'a'"
  :to="props.to"
  :href="props.href"
  class="my-link-button"
>
  <MyButton v-bind="props"> <!-- there we go -->
    <slot></slot>
  </MyButton>
</component>


Es todo bueno, pero pasamos un poco más de lo que queremos:


W3C disapproves


Como podéis ver, ahora nuestro botón subyacente también tiene unhrefNo es una tragedia, solo un poco de desorden y bytes adicionales, aunque no sea cool.

<template>
  <component
    :is="props.to ? RouterLink : 'a'"
    :to="props.to"
    :href="props.href"
    class="my-link-button"
  >
    <MyButton v-bind="propsToPass">
      <slot></slot>
    </MyButton>
  </component>
</template>

<script lang="ts" setup>
// imports and definitions…

const props = defineProps<MyButtonProps & MyLinkButtonProps>();

const propsToPass = computed(() =>
  Object.fromEntries(
    Object.entries(props).filter(([key, _]) => !["to", "href"].includes(key))
  )
);
</script>


Ahora, solo pasamos lo que tiene que pasar, pero todas esas letras de cuerdas no parecen increíbles, ¿no? y esa es la historia más triste de TypeScript, chicos.

Interfaces vs Abstract Interfaces

Interfaces versus interfaces abstractas

Si alguna vez ha trabajado con los idiomas adecuados orientados a objetos, probablemente conozca cosas como:ReflexionesDesafortunadamente, en TypeScript, las interfaces son efímeras; no existen en tiempo de ejecución, y no podemos averiguar fácilmente qué campos pertenecen a laMyButtonProps.


Esto significa que tenemos dos opciones.En primer lugar, podemos mantener las cosas como son: cada vez que añadimos unMyLinkButtonTambién hay que excluirlo depropsToPass(Y incluso si lo olvidamos, no es una gran cosa).


La segunda manera es usar objetos en lugar de interfaces. Puede sonar sin sentido, pero déjame codificar algo: no será horrible; prometo.quehorrorífico .

// MyButton.props.ts

export const defaultMyButtonProps: MyButtonProps = {
  theme: ComponentTheme.BLUE,
  small: false,
  icon: undefined,
  rightIcon: undefined,
  counter: undefined,
  disabled: false,
  loading: false,
};


No tiene sentido crear un objeto sólo para crear un objeto, pero podemos usarlo para los props predeterminados.MyButton.vueSe vuelve más limpio:

const props = withDefaults(defineProps<MyButtonProps>(), defaultMyButtonProps);


Ahora solo tenemos que actualizarpropsToPassenMyLinkButton.vue:

const propsToPass = computed(() =>
  Object.fromEntries(
    Object.entries(props).filter(([key, _]) =>
      Object.hasOwn(defaultMyButtonProps, key)
    )
  )
);


Para realizar este trabajo, necesitamos definir explícitamente todos losundefinedynullcampos endefaultMyButtonPropsDe lo contrario, el objeto no “tiene su propio”.


De esta manera, cada vez que añadas un propósito al componente básico, también tendrás que añadirlo al objeto con valores predeterminados. Así que sí, hay dos lugares de nuevo, y tal vez no sea mejor que la solución del capítulo anterior.

I’m Done

Estoy hecho

No es una obra maestra, pero es probablemente lo mejor que podemos hacer dentro de las limitaciones de TypeScript.


También parece que tener tipos de prop dentro del archivo SFC es mejor, pero no puedo decir que moverlos a un archivo separado lo hizo mucho peor.


Puedes encontrar el código de este artículo en GitHub.

GitHub

Trending Topics

blockchaincryptocurrencyhackernoon-top-storyprogrammingsoftware-developmenttechnologystartuphackernoon-booksBitcoinbooks