Como Construir Mejores Componentes de Alto Nivel (Higher-Order Components) con el Vue 3

by Luigi Nori Date: 07-08-2020 javascript vue vue3 hoc template


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 useFetchpara reflejar esa página, ya que parece que necesita hacer un seguimiento del nuevo punto final expuesto por usePagination. Afortunadamente, watchhas 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 usePaginationno 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.

 

 
by Luigi Nori Date: 07-08-2020 javascript vue vue3 hoc template visitas : 860  
 
Luigi Nori

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

Modo oscuro en el sitio web usando CSS y JavaScript

En el artículo de hoy vamos a aprender a construir más o menos estándar en estos días en las páginas web y que es el modo de color alternativo y…

Javascript: los operadores Spread y Rest

En el artículo de hoy vamos a hablar de una de las características de la versión ES6 (ECMAScript 2015) de JavaScript que es el operador Spreadasí como el operador Rest. Estas…

Cookies HTTP: cómo funcionan y cómo usarlas

Hoy vamos a escribir sobre la forma de almacenar datos en un navegador, por qué los sitios web utilizan cookies y cómo funcionan en detalle. Continúa leyendo para averiguar cómo implementarlas…

Todas las funciones de javascript y los métodos para manipular los arrays

Este artículo mostrará las funciones prominentes de las arrays de JavaScript, entre ellas .map() , .filter() , y .reduce() , y luego pasará a través de ejemplos de casos en los que .every() …

Node.js: herramientas de código abierto para los desarrolladores

De la amplia gama de herramientas disponibles para simplificar el desarrollo de Node.js, aquí están las mejores. Una encuesta en StackOverflow afirma que el 49% de los desarrolladores usan Node.js para…

Renderización lado servidor de Vue.js on Php

¿Intentas que el renderizado del lado del servidor funcione con PHP para renderizar tu aplicación Vue.js pero se atasca? Hay un montón de grandes recursos por ahí, pero hasta ahra no…

Integración de Bootstrap 4 con Vue.js usando Bootstrap-Vue

React y Vue.js son dos marcos de trabajo líderes y modernos de JavaScript para el desarrollo del front-end. Mientras que React tiene una curva de aprendizaje considerable, y un proceso…

Cómo crear un aplicación Vue.js en 5 minutos

Vue.js se está volviendo cada vez más popular, convirtiéndose en un competidor significativo de frameworks como Angular o React.js. Como front-end framework para principiantes, conquista con éxito los corazones de…

Vue.js 3, la programación orientada al futuro que no hay que perderse!

Si estás interesado en Vue.js, probablemente conozcas la 3ª versión de este framework, que se publicará en breve. La nueva versión de momento está en desarrollo, pero todas las características…

Vue.js - Cómo usar (con seguridad) un plugin de jQuery con Vue.js

No es una buena idea usar jQuery y Vue.js en la misma interfaz. No lo hagas si puedes evitarlo, pero probablemente si estás leyendo esto no porque quieras usar jQuery…

Como construir una web application con Styled Components en Vue.js

Styled-Components es una biblioteca muy popular dentro de la comunidad React y React Native, y ahora puedes usarla en Vue.js. Para aquellos que no lo saben, Styled-Components es una biblioteca muy…

10 librerías para machine learning en JavaScript

JavaScript es actualmente uno de los lenguajes de programación más populares. Su principal aplicación se encuentra en las aplicaciones web, utilizándose para dar funcionalidad a las páginas web dinámicas. Otro…

Utilizamos cookies propias y de terceros para mejorar nuestros servicios, elaborar información estadística y analizar tus hábitos de navegación. Esto nos permite personalizar el contenido que ofrecemos y mostrarte publicidad relacionada con tus preferencias. Clicando en ‘Acepta todas’ aceptas el almacenamiento de cookies en tu dispositivo para mejorar la navegación en el sitio web, analizar el tráfico y ayudar en nuestras actividades de marketing. También puedes seleccionar ‘Sólo cookies de sistema’ para aceptar sólo las cookies necesarias para que la web funcione, o puedes seleccionar las cookies que quieres activar clicando en ‘Configuración’

Acepta todas Sólo cookies de sistema Configuración