Frameworks

Vue 3: Composition API y otras novedades

Descubre las importantes novedades que de Vue 3, tanto la nueva Composition API como el resto de cambios que se incluyen en este framework de JavaScript.

Publicado el 08 de Febrero de 2021
Compartir

En el mundo del desarrollo web existen actualmente varias soluciones para el desarrollo basado en JavaScript de grandes aplicaciones o de aplicaciones de página única, y Vue es una de las más potentes, de hecho, en este blog ya hablé de él anteriormente unas cuantas veces e incluso lo comparé con otro de sus actuales competidores.

Ahora su creador, Evan You, ha decidido lanzar una versión preliminar para la tercera versión de este framework, y viene cargada de suculentas novedades que proponen nuevas soluciones y mejoras basadas en la larga experiencia recopilada durante el uso de la versión actual.

Novedades en Vue 3

En Vue 3 encontramos un gran número de mejoras, tanto a nivel de rendimiento como a nivel de usabilidad del framework y de características muy requeridas que anteriormente se añadían como plugins y ahora forman parte nativa de esta nueva versión. Sin embargo, y sin lugar a dudas, la composition API es el mayor cambio y de mayor impacto en Vue 3, por eso le dedicaremos la siguiente sección, tratando en esta primeramente algunos de los cambios importantes que suelen obviarse en favor de esta nueva forma de hacer Vue.

Soporte para Fragments

En Vue 2 se hacía mandatorio que el template de cualquier componente tuviera un único elemento raíz, de forma que este bloque era un template válido de Vue:

# UnNodoRaiz.vue
<template>
    <div>
        <!-- Un contenido -->
    </div>
</template>

Y sin embargo, este otro template sería considerado inválido y no transpilaría correctamente:

# DosNodosRaiz.vue
<template>
    <div>
        <!-- Un contenido -->
    </div>
    <div>
        <!-- Otro contenido -->
    </div>
</template>

Por ello en Vue 3 han introducido soporte para fragments, algo que ya hizo React en su anterior versión, y que permite precisamente lo anterior, es decir, renderizar varios nodos en un template sin necesidad de que se encuentren dentro de uno superior que los contenga.

El único problema que esto introduce es el binding de los atributos heredados o atributos no-props, es decir, aquellos atributos asignables como id o class a la hora de introducir el componente en el template de otro componente. De forma que sin fragments al utilizar nuestro anterior componente UnNodoRaiz.vue:

# Ejemplo.vue
<template>
    <un-nodo-raiz class="una-clase"/>
</template>

Al introducir la clase una-clase en la llamada al componente, este se trata como un atributo no-prop, y automáticamente se inyectará en el nodo raíz del componente UnNodoRaiz.vue, produciendo la renderización en el dom:

<div class="una-clase">
    <!-- Un contenido -->
</div>

Sin embargo, cuando usamos fragments esta asignación automática no es posible, ya que al no disponer de una única raíz Vue no sabe dónde debe de ser inyectado, de forma que no las inyectará en ninguno de los dos y lanzará un aviso similar a este por consola:

Imagen 0 en Vue 3: Composition API y otras novedades

La solución propuesta por Vue 3 para este problema concreto consiste en utilizar v-bind="$attrs" en el nodo en el que queramos que estas se asignen automáticamente, descargando así en el desarrollador la responsabilidad de esta herencia. En nuestro caso esto se arreglaría así:

# DosNodosRaiz.vue
<template>
    <div v-bind="$attrs">
        <!-- Un contenido -->
    </div>
    <div>
        <!-- Otro contenido -->
    </div>
</template>

de forma que la renderización a HTML de este componente usado en el anterior componente de ejemplo será finalmente esta:

<div class="una-clase">
    <!-- Un contenido -->
</div>
<div>
    <!-- Otro contenido -->
</div>

Teleport

Uno de los problemas de la jerarquía de componentes en soluciones web como Vue o React es que en ocasiones nos interesa que la lógica siga dicha jerarquía de anidamiento, pero la renderización no nos interesa que se encuentre al mismo nivel.

Un ejemplo de esto sería, por ejemplo, que decidiéramos introducir un modal de forma aislada, sin crear un sistema complejo de modales, y que fuera para un componente concreto, sea el componente modal:

# Modal.vue
<template>
    <div class="overlay">
        <div class="modal">
            <slot></slot>
        </div>
    </div>
</template>
<script>
export default {
    name: 'Modal',
    props: {
        isOpen: Boolean
    }
}
</script>

Y sea el componente contenedor:

# Parent.vue
<template>
    <div class="una-clase">
        <div>
            <!-- Un contenido -->
            <modal :isOpen="modalOpen"><!-- Contenido del modal --></modal>
        </div>
        <div>
            <!-- Otro contenido -->
        </div>
    </div>
</template>
<script>
import Modal from './Modal.vue'
export default {
    name: 'Parent',
    components: { Modal },
    data() {
        return {
            modalOpen: false
        }
    }
}
</script>

Como este componente se encontrará en una jerarquía que pondrá su renderización por debajo del body del HTML, el modal se renderizará al mismo nivel que dicho componente, quedando en profundidad por debajo del resto de elementos. Por lo tanto, al abrir el modal, suponiendo que el padre esté a nivel de body, la renderización quedará así:

<body>
    <div class="una-clase">
        <div>
            <!-- Un contenido -->
            <div class="overlay">
                <div class="modal">
                    <!-- Contenido del modal -->
                </div>
            </div>
        </div>
        <div>
            <!-- Otro contenido -->
        </div>
    </div>
</body>

Aunque este caso podríamos solucionarlo con unas dosis de CSS y algún que otro truquillo, Vue 3 nos permite utilizar una solución tan sencilla como es el teleport, que, tal y como indica su nombre, teletransporta un nodo de nuestro template al lugar que le indiquemos. De tal forma que modificando nuestro componente modal para utilizar el nuevo componente Teleport:

# Modal.vue
<template>
    <teleport to="body">
        <div class="overlay">
            <div class="modal">
                <slot></slot>
            </div>
        </div>
    </teleport>
</template>
<script>
export default {
    name: 'Modal',
    props: {
        isOpen: Boolean
    }
}
</script>

Al indicarle en la propiedad to del Teleport la etiqueta <body> (También puede indicársele un id en otro caso), la renderización final de la anterior estructura quedará así:

<body>
    <div class="overlay">
        <div class="modal">
            <!-- Contenido del modal -->
        </div>
    </div>
    <div class="una-clase">
        <div>
            <!-- Un contenido -->
        </div>
        <div>
            <!-- Otro contenido -->
        </div>
    </div>
</body>

Desplazando el renderizado de nuestro componente al lugar del DOM en el que nos interesa que se encuentre.

Múltiples v-model

Otra de las características realmente interesantes que nos proporciona Vue 3, y que sin duda se adoptará de manera rápida, es la posibilidad de realizar double-binding o asignación de doble sentido para más de una propiedad por componente.

Anteriormente, en Vue 2, un double-bind utilizando v-model de la siguiente manera:

<un-componente v-model="titulo" />

Era una abreviación de esta asignación de propiedad y recepción de evento:

<un-componente :value="titulo" @input="titulo = $event" />

En Vue 3 sin embargo, v-model pasa a ser una abreviación de la siguiente forma:

<un-componente :modelValue="titulo" @update:modelValue="titulo = $event" />

De forma que pasa a ser un alias de una variable interna y un evento interno del propio Vue que, lo mejor de todo, no es exclusivo para modelValue, podemos crear tantas variables como queramos y enlazarlas con este método.

De forma que pensemos que, por ejemplo, queremos sacar una variable color de un selector de color:

# ColorSelector.vue
<template>
     <input
        type="color"
        :value="color"
        @input="$emit(update:color, $event.target.value)"
      />
</template>
<script>
export default {
    name: 'ColorSelector',
    props: {
        color: String
    }
}
</script>

Hemos definido una propiedad color, y el cambio del input emitirá un evento de la forma update:nombredelapropiedad, que en este caso es update:color. Este simple proceso nos permitirá hacer el double-bind con v-model para cualquier componente que lo use, como por ejemplo:

# Parent.vue
<template>
     <color-selector v-model:color="selectedColor"/>
</template>
<script>
import ColorSelector from './ColorSelector.vue';
export default {
    name: 'ColorSelector',
    components: { ColorSelector }
    data() {
        return {
            selectedColor: '#79c120'
        }
    }
}
</script>

Con este simple arreglo tanto el padre como el propio componente podrán cambiar el estado y se asignará automáticamente el nuevo valor al selectedColor del estado reactivo del componente como con cualquier v-model pero con la ventaja añadida de poder tener cuantos queramos en cada uno de nuestros componentes.

Vue: Composition API

Sin duda la mayor adición de Vue 3 es la composition API. Inspirada profundamente en los, también recientes, hooks de React, es una propuesta optativa que ofrece una nueva forma completamente funcional de introducir la lógica en un componente de Vue.

Motivación

La razón de ser principal de esta nueva API la dejan bien claro sus propios creadores: Conseguir simplificar la lógica en componentes de gran tamaño, aunarlas en región únicas, evitar la duplicidad de código y de esta manera mejorar la legibilidad del código.

Anteriormente, en Vue 2, usando el método habitual (la llamada options API) la única manera de crear un componente con una lógica con ciertos métodos, valores computados, propiedades, estado y watchers dependía de distribuir estos entre cada una de las secciones methods, computed, props, data y watch, independientemente de que dicha lógica perteneciera o no a una misma feature. De forma que, utilizando el siguiente simple (Y sin sentido) ejemplo, nos veríamos obligados a hacer una distribución de la siguiente manera:

# OptionsComponent.vue
<template>
     <!-- mi template -->
</template>
<script>
export default {
    name: 'OptionsComponent',
    props: {
        myFeatureProp: String
    },
    data() {
        return {
            myFeatureData: this.prop
        }
    },
    watch: {
        myFeatureProp: function(val) {
            this.myFeatureData = val;
        }  
    },
    computed: {
        myFeatureComputed: function() { return this.myFeatureData + '!'}
    },
    methods: {
        myFeatureMethod: function(val) {
            this.myFeatureData = val;
        }
    }
}
</script>

Esto, aunque perfectamente organizado por opciones, distribuye altamente el código de cada feature única, lo que en componentes muy extensos provoca que sea necesario navegar entre grandes bloques con frecuencia. Sin embargo, utilizando la nueva composition API podemos reducirlo a esto:

# CompositionComponent.vue
<template>
     <!-- mi template -->
</template>
<script>
import { ref, computed, watch } from 'vue';
export default {
    name: 'CompositionComponent',
    setup(props) {
        const myFeatureData = ref(props.myFeatureProp);
        const myFeatureComputed = computed(() => myFeatureData.value + '!');
        watch(
            props.myFeatureProp, 
            (val, oldVal) => { myFeatureData.value = val }
        )
        const myFeatureMethod = (val) => {
            myFeatureData.value = val;
        }
        return {
            myFeatureData,
            myFeatureComputed,
            myFeatureMethod
        }
    }
}
</script>

De esta forma vemos que todo nuestro código relacionado con una única feature queda más compacto y delimitado a una única región de nuestro componente, lo que hace más sencillo navegar por él.

Composables: Aislando funcionalidades

Esta no es la única ventaja que nos deja esta nueva manera de orientar nuestros componentes en Vue. Además de poder aunar nuestras features en áreas reducidas y mejorar la legibilidad de nuestro código, la Composition API también nos ofrece poder crear composables; Funciones que se insertan en el ciclo de vida de Vue y que nos permiten extraer y reusar funcionalidades.

Pensemos en el ejemplo sencillo del caso anterior, gracias a los composables es posible extraer esto en un fichero aparte:

# useFeature.js
import { ref, watch } from 'vue';
export default function useFeature(initialValue) {
    const myFeatureData = ref(initialValue);
    const myFeatureComputed = computed(() => myFeatureData.value + '!');
    watch(
        props.myFeatureProp, 
        (val, oldVal) => { myFeatureData.value = val }
    )
    const myFeatureMethod = (val) => {
        myFeatureData.value = val;
    }
    return {
        myFeatureData,
        myFeatureComputed,
        myFeatureMethod
    }
}

De forma que esta función (Que Vue requiere que siempre empiece por “use”) puede utilizarse en cualquier otro componente, que use composition, de la siguiente manera:

# AnotherCompositionComponent.vue
<template>
     <!-- mi template -->
</template>
<script>
import useFeature from './useFeature.js';
export default {
    name: 'AnotherCompositionComponent',
    setup(props) {
        const [myData, myComputed, myMethod] = useFeature(props.myFeatureProp);
        return {
            myData,
            myComputed,
            myMethod
        }
    }
}
</script>

Y esto nos dará acceso al estado, al computed y al método indicado previamente, ejecutándose también los métodos de ciclo de vida que tenga indicado el composable, así como sus watchers y efectos reactivos.

Esta manera de extracción de la lógica resulta muy potente, y librerías tan famosas y utilizadas como Vuex, Vue Router y Vue i18n, ya han adaptado sus nuevas versiones para comenzar a depender casi por completo de esta nueva característica.

¿Merece la pena cambiar ahora?

A día de hoy, en la fecha en la que se escribe este artículo, Vue 3 ni siquiera ha sido lanzado oficialmente y se encuentra en una beta, que, aunque estable, sigue teniendo algunos pequeños errores y está sujeta a cambios finales.

Sin embargo, en esta sección os hablaré desde un punto de vista personal como desarrollador principal en Stargazr, una empresa alemana cuyo producto principal es una aplicación web de análisis financiero basado en técnicas de aprendizaje automático, y de nuestra experiencia al decidirnos a hacer el cambio de Vue 2 a 3 y las ventajas e inconvenientes que encontramos.

El impacto de los cambios

Lo primero que nos preguntamos a la hora de elegir entre una versión de un framework y otra es siempre algo bastante trivial: ¿Qué librerías dejan de ser compatibles con esta versión?. Para nuestra sorpresa un gran número de las que catalogaríamos como esenciales han adaptado una versión, y bastante completa y funcional, únicamente para Vue 3, incluso en ciertos casos cambiando sus paradigmas para usar Composables, como mencionamos previamente.

Sin embargo, es obvio que algunas librerías no habrán hecho aún el salto, sobre todo aquellas mantenidas por pequeños equipos e individuales, que no sienten la misma presión para mantener el ritmo de actualizaciones (Y mucho menos para soportar versiones beta).

Pero a mi parecer, y a menos que queramos hacer una solución rápida basada en muchas dependencias, no hay una pérdida sensible por utilizar esta versión en cuanto a funcionalidades se refiere, ya que librerías muy habituales y con grandes equipos como fontAwesome o Vuex, ya disponen de versión actualizada y muy funcional.

Aprendiendo a pensar de nuevo

Una de las razones que, generalmente, motiva el cambio es la Composition API, ya que al desarrollar una aplicación de gran calado la lógica tiende a crecer de manera alarmante incluso componentizando profundamente cada característica de nuestro código.

Uno de mis temores era que, una vez lanzada esta nueva API, la forma habitual de crear componentes fuera dejada de lado. Sin embargo, los desarrolladores ya han afirmado en varias ocasiones que esta API es completamente opcional, de forma que permita que la adaptación de los desarrolladores sea más progresiva. Así, y para ir tanteando, podemos estructurar nuestra aplicación eligiendo qué componentes utilizarán esta nueva característica, partiendo por ejemplo del criterio de qué componentes se esperará que aglutinen más código.

Curiosamente, y a pesar de la importancia de la nueva API, una de las primeras adiciones de Vue 3 para la que pensamos rápidamente un uso fue para el teleport, ya que esto nos permitía tener en el componente principal algo tan sencillo como:

# app.vue
<template>
    <teleport to="head">
        <title>{{ myTitle }}</title>
    </teleport>
    <!-- contenido -->
</template>

Lo que nos permite actualizar dinámicamente el título del documento cuando sea necesario, como por ejemplo cuando se produzca un cambio de ruta y quiera mostrarse en la pestaña.

Pero es en el caso de los composables donde las opciones resultan infinitas, y actualmente ya experimento sacando todas las características reusables que se me ocurren y que pueden, en el futuro, “re-engancharse” al ciclo de vida de cualquier futuro componente.

Veredicto

A pesar de estar en Beta Vue 3 sin duda merece la expectación que está causando, e incluso usarlo ahora en tu nuevo proyecto (Si no tienes inconveniente en trabajarlo un poco más) es una buena idea y simplificará algunos de los procesos habituales que hacías hasta entonces.

Si te has quedado con ganas de saber más, puedes leer el artículo introductorio ¿Qué es Vue? o si prefieres algo más avanzado puedes atreverte con nuestro completo Curso de Vue.js.


Compartir este post

También te puede interesar...

Tecnología

React vs Vue

17 Septiembre 2020 Pablo Huet
Artículos
Ver todos