En esta edición de esta serie sobre patrones de diseño en Vue, vamos a ver los componentes dinámicos: qué problema solucionan y cómo los podemos usar.
Si los lectores de este artículo ya están usando Vue, probablemente se hayan encontrado con casos en los que queremos renderizar un componente u otro según se den o no ciertas condiciones. Por ejemplo, puede que estemos creando un "muro" de publicaciones similar al de Facebook. Al momento de cargar las publicaciones desde el backend, deberíamos mostrar un componente distinto según el tipo de publicación: solo texto, imagen, video, enlace a otro sitio, etc.
Es más que probable que para manejar este tipo de situaciones, lo primero que se nos venga a la mente sean precisamente las directivas que vimos en el artículo anterior: v-if, v-else y v-else-if. Veamos cómo podría ser el template de un componente como el que acabamos de describir:
<template>
<div class="publicacion">
<p>{{ publicacion.texto }}</p>
<publicacion-link-imagen v-if="publicacion.link.tipo == 'imagen'">
</publicacion-link-imagen>
<publicacion-link-video v-else-if="publicacion.link.video == 'video'">
</publicacion-link-video>
<publicacion-link-pinterest v-else-if="publicacion.link.tipo == 'pinterest'">
</publicacion-link-pinterest>
<publicacion-link-website v-else-if="publicacion.link.tipo == 'website'">
</publicacion-link-website>
etc, etc, etc...
</div>
</template>
<script>
import publicacionLinkImagen from './publicacionLinkImagen';
import publicacionLinkVideo from './publicacionLinkVideo';
import publicacionLinkPinterest from './publicacionLinkPinterest';
import publicacionLinkWebsite from './publicacionLinkWebsite';
etc, etc, etc...
</script>
Este patrón presenta algunos problemas de escalabilidad:
- Cada nueva variante implica agregar una nueva etiqueta en el template, con su condición correspondiente.
- En ciertos casos, cada nueva variante puede implicar agregar una nueva prop.
- La lógica de los v-else-if puede comenzar a complicarse y dificultar el entendimiento de los diferentes casos.
- Cada nueva variante implica importar un nuevo componente, por si llega a necesitarse a la hora de hacer el renderizado.
Para solucionar estos problemas, Vue introduce un tipo especial de componente.
El componente <component>
Vue cuenta con un componente llamado <component> (nombre bastante incómodo a la hora de buscar en Google para investigar) cuya función, en principio, es muy simple: Renderizar el componente que indiquemos mediante la directiva v-bind:is. O sea, para renderizar el componente "publicaciónLinkWebsite" utilizando esta etiqueta, podríamos hacer algo así:
<template>
<component v-bind:is="publicacion-link-website"></component>
</template>
<script>
import publicacionLinkWebsite from './publicacionLinkWebsite';
...
</script>
Como podemos ver, sigue siendo necesario importar el componente para que esté disponible a la hora de usarlo en la etiqueta <component>.
Si combinamos la etiqueta <component> con un data, podemos atacar uno de los problemas que describimos al comienzo del artículo: la necesidad de agregar cosas al template cada vez que queremos añadir una nueva variante. Veamos cómo quedaría:
<template>
<div class="publicacion">
<p>{{ publicacion.texto }}</p>
<component v-bind:is="publicacionLink" v-if="publicacionLink"></component>
</div>
</template>
<script>
import publicacionLinkImagen from './publicacionLinkImagen';
import publicacionLinkVideo from './publicacionLinkVideo';
import publicacionLinkPinterest from './publicacionLinkPinterest';
import publicacionLinkWebsite from './publicacionLinkWebsite';
etc, etc, etc...
data() {
return {
publicacionLink: null,
}
}
...
</script>
De esta manera, desde el componente padre podemos asignar el nombre del componente que queremos mostrar al data publicacionLink, lo cual hará que este se cargue en la etiqueta <component>. Sin embargo, vemos que sigue siendo necesario importar todos los componentes correspondientes a los valores que puede adoptar la variable publicacionLink. Para resolver este problema, podemos recurrir a nuestro amigo Webpack.
Importación dinámica de componentes
Una ventaja importante de la etiqueta <component> es que su directiva v-bind:is admite recibir tanto el nombre de un componente (un String) como el "objeto de opciones" (option object) de un componente (o sea, la declaración del componente, como la que escribimos en un archivo .vue). Gracias a esa característica, podemos elaborar una propiedad computed que termine devolviendo precisamente dicho objeto, y vincular dicha propiedad a la directiva is de la etiqueta:
<template>
<div class="publicacion">
<p>{{ publicacion.texto }}</p>
<component v-bind:is="componenteOpcional" v-if="nombreTipoPublicacion"></component>
</div>
</template>
<script>
export default {
props: {
nombreTipoPublicacion: {
type: String,
default: null
}
},
computed: {
componenteOpcional() {
return () => import(`./${nombreTipoPublicacion}`);
}
}
}
</script>
¿Qué está pasando en este ejemplo? La directiva v-bind:is de la etiqueta <component> está recibiendo una Promise. Si la Promise puede resolverse, se renderiza el componente indicado. Si no, no se renderiza, y no hay errores ni problemas que manejar. Un lujo.
Algo de lo que cuidarse
Hay una cosa que es necesario tener en cuenta antes de ponerse a usar este patrón.
Cuando compila el proyecto, Webpack crea un archivo de chunk (literalmente, "trozo", una suerte de caché) por cada componente que se encuentra en la carpeta que indicamos en nuestra línea:
return () => import(`./${nombreTipoPublicacion}`);
Entonces, si nuestra aplicación tiene 800 componentes dentro de la carpeta components y en nuestro código indicamos:
return () => import(`./components/${nombreTipoPublicacion}`);
Webpack estaría creando 800 chunks, por más que las posibilidades reales sean solo 2 o 3. Porque, obviamente, Webpack no sabe cuáles son los valores posibles para la variable nombreTipoPublicacion.
Para no caer en esta pobre optimización, es recomendable crear al menos una subcarpeta "chunks" o "bundles" dentro de nuestra carpeta de componentes, para que Webpack no haga archivos chunk para todos los componentes:
return () => import(`./components/bundles/${nombreTipoPublicacion}`);
Según el volumen de la aplicación, puede que incluso sea conveniente crear subcarpetas para cada una de estas instancias dentro de la carpeta "chunks" o "bundles":
return () => import(`./components/bundles/publicacion/${nombreTipoPublicacion}`);
Bueno, eso fue todo acerca de los componentes dinámicos. Este es un patrón genial que realmente puede reducir en gran medida tanto la complejidad del código como el uso de recursos y los tiempos de carga de nuestra aplicación web. Esperamos que les sirva. ¡Nos vemos en la próxima edición!