El Vue 3 será lanzado pronto con la introducción del API de composición (Composition API). Viene con muchos cambios y mejoras en el rendimiento.
Los componentes de orden superior (HOC o Higher-order components) son componentes que añaden ciertas funcionalidades a su aplicación de forma declarativa utilizando la plantilla. Creo que seguirán siendo muy relevantes incluso con la introducción de la API de composición.
Los HOC siempre han tenido problemas para exponer toda la potencia de su funcionalidad, y como no son tan comunes en la mayoría de las aplicaciones de Vue, a menudo están mal diseñados y pueden introducir limitaciones. Esto se debe a que la plantilla es sólo eso - una plantilla, o un lenguaje restringido en el que se expresa algo de lógica. Sin embargo, en los entornos JavaScript o JSX, es mucho más fácil expresar la lógica porque tienes todo el JavaScript disponible para que lo utilices.
Lo que Vue 3 aporta es la capacidad de mezclar y combinar sin problemas la expresividad de JavaScript utilizando el API de composición y la facilidad declarativa de las plantillas.
Estamos usando activamente HOCs en las aplicaciones que construimos para varias piezas de lógica como red, animaciones, interfaz de usuario y estilo, utilidades y bibliotecas de código abierto. Hay algunos consejos para compartir sobre cómo construir HOCs, especialmente con la próxima API de composición de Vue 3.
La plantilla (template)
Asumamos el siguiente componente de búsqueda (fetch
component). Antes de entrar en cómo implementar tal componente, deberías pensar en cómo usarías tu componente. Luego, debes decidir cómo implementarlo. Esto es similar a TDD pero sin las pruebas - es más como jugar con el concepto antes de que funcione.
Lo ideal sería que ese componente utilizara un endpoint y devolviera su resultado como una propriedad de scoped slot:
<fetch endpoint="/api/users" v-slot="{ data }"> <div v-if="data"> <!-- Show the response data --> </div> </fetch>
Ahora bien, aunque esta API tiene el propósito básico de buscar algunos datos en la red y mostrarlos, hay muchas cosas que faltan y que sería útil tener.
Empecemos con el manejo de errores. Lo ideal sería poder detectar si se ha producido un error de red o de respuesta y mostrar alguna indicación de ello al usuario. Vamos a esbozar eso en nuestro recorte de uso:
<fetch endpoint="/api/users" v-slot="{ data, error }"> <div v-if="data"> <!-- Show the response data --> </div> <div v-if="error"> {{ error.message }} </div> </fetch>
Hasta ahora todo bien. ¿Pero qué hay del estado de carga (loading state)? Si seguimos el mismo camino, terminamos con algo como esto:
<fetch endpoint="/api/users" v-slot="{ data, error, loading }"> <div v-if="data"> <!-- Show the response data --> </div> <div v-if="error"> {{ error.message }} </div> <div v-if="loading"> Loading.... </div> </fetch>
Genial. Ahora, asumamos que necesitamos tener soporte de paginación:
<fetch endpoint="/api/users" v-slot="{ data, error, loading, nextPage, prevPage }"> <div v-if="data"> <!-- Show the response data --> </div> <div v-if="!loading"> <button @click="prevPage">Prev Page</button> <button @click="nextPage">Next Page</button> </div> <div v-if="error"> {{ error.message }} </div> <div v-if="loading"> Loading.... </div> </fetch>
Ves a dónde va esto, ¿verdad? Estamos agregando demasiadas propiedades a nuestro scoped slot predeterminado. En su lugar, vamos a dividirlo en múltiples slots:
<fetch endpoint="/api/users"> <template #default="{ data }"> <!-- Show the response data --> </template> <template #pagination="{ prevPage, nextPage }"> <button @click="prevPage">Prev Page</button> <button @click="nextPage">Next Page</button> </template> <template #error="{ message }"> <p>{{ message }}</p> </div> <template #loading> Loading.... </template> </fetch>
Si bien el número de caracteres que tenemos es en su mayoría el mismo, es mucho más limpio en el sentido de que utiliza múltiples slots para mostrar diferentes UI durante los diferentes ciclos de funcionamiento del componente. Incluso nos permite exponer más datos por cada slot, en lugar del componente en su conjunto.
Por supuesto, hay espacio para mejorar aquí. Pero decidamos que estas son las características que quieres para ese componente. Nada funciona todavía. Todavía tienes que implementar el código real que hará que esto funcione.
Empezando con la plantilla, sólo tenemos 3 slots que se muestran usando v-if
:
<template> <div> <slot v-if="data" :data="data" /> <slot v-if="!loading" name="pagination" v-bind="{ nextPage, prevPage }" /> <slot v-if="error" name="error" :message="error.message" /> <slot v-if="loading" name="loading" /> </div> </template>
Usando v-if
con múltiples slots aquí es una abstracción, por lo que los consumidores de este componente no tienen que rendir su UI condicionalmente. Es una característica conveniente de tener en su lugar.
El API de composición permite oportunidades únicas para construir mejores HOC, que es de lo que trata este artículo en primer lugar.
El JavaScript
Con la plantilla fuera del camino, la primera implementación ingenua será en una sola función de setup
:
import { ref, onMounted } from 'vue'; export default { props: { endpoint: { type: String, required: true, } }, setup({ endpoint }) { const data = ref(null); const loading = ref(true); const error = ref(null); const currentPage = ref(1); function fetchData(page = 1) { // ... } function nextPage() { return fetchData(currentPage.value + 1); } function prevPage() { if (currentPage.value <= 1) { return; } fetchData(currentPage.value - 1); } onMounted(() => { fetchData(); }); return { data, loading, error, nextPage, prevPage }; } };
Esa es una visión general de la función de setup
. Para completarlo, podemos implementar la función de fetchData
como aqui:
function fetchData(page = 1) { loading.value = true; // I prefer to use fetch // you cause use axis as an alternative return fetch(`${endpoint}?page=${page}`, { // maybe add a prop to control this method: 'get', headers: { 'content-type': 'application/json' } }) .then(res=> { // a non-200 response code if (!res.ok) { // create error instance with HTTP status text const error = new Error(res.statusText); error.json = res.json(); throw error; } return res.json(); }) .then(json=> { // set the response data data.value = json; // set the current page value currentPage.value = page; }) .catch(err=> { error.value = err; // incase a custom JSON error response was provided if (err.json) { return err.json.then(json=> { // set the JSON response message error.value.message = json.message; }); } }) .then(() => { // turn off the loading state loading.value = false; }); }
Con todo eso en su lugar, el componente está listo para ser usado. Sin embargo, este componente HOC es similar al que tendrías en el Vue 2. Sólo lo reescribiste usando el API de composición, que, si bien es limpio, no es muy útil.
Hemos descubierto que, para construir un mejor componente HOC para el Vue 3 (especialmente un componente orientado a la lógica como este), es mejor construirlo de una manera “Composition-API-first”. Incluso si sólo planeas enviar un HOC.
Verás que ya lo hemos hecho. La función de setup delcomponente fetch
puede ser extraída a su propia función, que se llama useFetch
:
export function useFetch(endpoint) { // same code as the setup function }
Y en su lugar nuestro componente se verá así:
import { useFetch } from '@/fetch'; export default { props: { // ... }, setup({ endpoint }) { const api = useFetch(endpoint); return api; } }
Este enfoque permite algunas oportunidades. Primero, nos permite pensar en nuestra lógica mientras estamos completamente aislados de la UI. Esto permite que nuestra lógica se exprese completamente en JavaScript. Puede ser enganchada más tarde a la interfaz de usuario, que es la responsabilidad de la componente fetch
.
En segundo lugar, permite que nuestra función useFetch
para descomponer su propia lógica en funciones más pequeñas. Piense en ello como "agrupar" cosas similares, y tal vez crear variaciones de nuestros componentes incluyendo y excluyendo esas características más pequeñas.
Descomponiéndolo
Aclaremos eso extrayendo la lógica de la paginación a su propia función. El problema se convierte en: ¿cómo podemos separar la lógica de paginación de la lógica de fetching? Ambas parecen estar entrelazadas.
Puedes averiguarlo centrándote en lo que hace la lógica de la paginación. Una forma divertida de averiguarlo es quitándolo y comprobando el código que has eliminado.
Actualmente, lo que hace es modificar el endpoint
añadiendo un parámetro de query page
, y manteniendo el estado de currentPage
mientras se exponen las funciones de siguiente y anterior. Eso es literalmente lo que se está haciendo en la iteración anterior.
Creando una función llamada usePagination
que sólo hace la parte que necesitamos, obtendrás algo como esto:
import { ref, computed } from 'vue'; export function usePagination(endpoint) { const currentPage = ref(1); const paginatedEndpoint = computed(() => { return `${endpoint}?page=${currentPage.value}`; }); function nextPage() { currentPage.value++; } function prevPage() { if (currentPage.value <= 1) { return; } currentPage.value--; } return { endpoint: paginatedEndpoint, nextPage, prevPage }; }
Lo bueno de esto es que hemos escondido la referencia currentPage
fde los consumidores externos, que es una de mis partes favoritas de la Composición API. Podemos ocultar fácilmente detalles no importantes a los consumidores de la API.
Es interesante actualizar el useFetch
para reflejar esa página, ya que parece que necesita hacer un seguimiento del nuevo punto final expuesto por usePagination
. Afortunadamente, watch
has us covered.
En lugar de esperar que el argumento endpoint
pase a ser una cadena regular, podemos permitir que sea un valor reactivo. Esto nos da la capacidad de verlo, y cada vez que la página de paginación cambie, resultará en un nuevo valor final, desencadenando un re-fetch.
import { watch, isRef } from 'vue'; export function useFetch(endpoint) { // ... function fetchData() { // ... // If it's a ref, get its value // otherwise use it directly return fetch(isRef(endpoint) ? endpoint.value : endpoint, { // Same fetch opts }) // ... } // watch the endpoint if its a ref/computed value if (isRef(endpoint)) { watch(endpoint, () => { // refetch the data again fetchData(); }); } return { // ... }; }
Notese que useFetch
y usePagination
no se conocen en absoluto, y ambos se implementan como si el otro no existiera. Esto permite una mayor flexibilidad en nuestro HOC.
También notarán que al construir para Composition API primero, creamos un JavaScript ciego que no es consciente de su UI. En nuestra experiencia, esto es muy útil para modelar los datos correctamente sin pensar en la UI o dejar que la UI dicte el modelo de datos.
Otra cosa genial es que podemos crear dos variantes diferentes de nuestro HOC: una que permite la paginación y otra que no. Esto nos ahorra unos pocos kilobytes.
Aquí hay un ejemplo de uno que sólo hace la paginación:
import { useFetch } from '@/fetch'; export default { setup({ endpoint }) { return useFetch(endpoint); } };
Aquí hay otro que hace ambas cosas:
import { useFetch, usePagination } from '@/fetch'; export default { setup(props) { const { endpoint, nextPage, prevPage } = usePagination(props.endpoint); const api = useFetch(endpoint); return { ...api, nextPage, prevPage }; } };
Mejor aún, se puede aplicar condicionalmente la función usePagination
basada en un accesorio para una mayor flexibilidad:
import { useFetch, usePagination } from '@/fetch'; export default { props: { endpoint: String, paginate: Boolean }, setup({ paginate, endpoint }) { // an object to dump any conditional APIs we may have let addonAPI = {}; // only use the pagination API if requested by a prop if (paginate) { const pagination = usePagination(endpoint); endpoint = pagination.endpoint; addonAPI = { ...addonAPI, nextPage: pagination.nextPage, prevPage: pagination.prevPage }; } const coreAPI = useFetch(endpoint); // Merge both APIs return { ...addonAPI, ...coreAPI, }; } };
Esto podría ser demasiado para sus necesidades, pero permite que sus HOC sean más flexibles. De lo contrario, serían un cuerpo rígido de código que es más difícil de mantener. También es definitivamente más amigable con las pruebas de unidad.
Luigi Nori
He has been working on the Internet since 1994 (practically a mummy), specializing in Web technologies makes his customers happy by juggling large scale and high availability applications, php and js frameworks, web design, data exchange, security, e-commerce, database and server administration, ethical hacking. He happily lives with @salvietta150x40, in his (little) free time he tries to tame a little wild dwarf with a passion for stars.
Artículos relacionados
Crear PDF con Javascript y jsPDF
El formato PDF es muy útil para descargar datos de forma masiva en una aplicación web. Ayuda a los usuarios a descargar contenido dinámico en forma de archivo para que…
Como hacer tu propio cursor personalizado para tu web
Cuando empecé a ojear webs distintas y originales para aprender de ellas, de las primeras cosas que me llamaron la atención fue que algunas de ellas tenían sus propios cursores,…
Explorando la API de CSS Paint: Redondeo de formas parte 1
Añadir bordes a las formas complejas es un auténtico rollo (a veces), pero redondear las esquinas de las formas complejas es un suplicio jejeje. Por suerte, la API de pintura…
Cómo enviar un correo electrónico desde un formulario de contacto HTML
En el artículo de hoy vamos a escribir sobre cómo hacer un formulario que funcione y que al pulsar ese botón de envío sea funcional y envíe el correo electrónico…
Cómo hacer un sitio web multilingüe sin redireccionamiento
Hoy, vamos a hablar de cómo implementar un simple selector de idioma en el sitio web estático o básico, sin necesidad de ningún backend o llamadas a la base de…
Comenzando con Bootstrap-Vue paso a paso
Hoy te mostraremos cómo usar BootstrapVue, describiremos el proceso de instalación y mostraremos la funcionalidad básica. El proyecto está basado en el framework CSS más popular del mundo - Bootstrap, para…
Por qué los desarrolladores de JavaScript deberían preferir Axios a Fetch
Por qué los desarrolladores de JavaScript deberían preferir Axios a Fetch En mi artículo anterior, "Usando la Api Fetch Para Hacer Llamadas Ajax", hablé de los fundamentos de la API Fetch.…
Creación de un sencillo spinner-loader CSS
En el artículo de hoy mostraremos cómo animar un loader básico que gira cuando se define alguna acción predefinida, como cargar una imagen. Eso se puede utilizar en un sitio…
Los mejores selectores de fechas para Bootstrap y tu aplicación
Los selectores de fecha son widgets que permiten a los usuarios elegir una sola fecha o rango de fechas y horas. Es un elemento habitual para todo usuario de Internet,…
Validación de formularios HTML usando BULMA y vanilla JavaScript
Hoy vamos a escribir sobre los formularios de contacto y cómo validarlos usando JavaScript. El formulario de contacto parece ser una de las características principales de toda página web básica. Es…
Cómo usar el efecto Parallax.Js en tu sitio web
Hoy vamos a escribir sobre el efecto de parallax, similar al desplazamiento de parallax, y cómo implementarlo para mejorar su página de aterrizaje. En webdev, dicen que primero el móvil…
Usando la API FETCH para hacer llamadas AJAX - Una promesa cumplida
En este artículo hablamos sobre lo que son las llamadas AJAX y cómo utilizarlas de forma tradicional, utilizando el objeto XMLHttpRequest (XHR). En resumen, gracias a las llamadas AJAX una…