Home »

¿Qué no nos ha convencido de Vue? Un repaso tras nuestra experiencia

Tras terminar un proyecto, en Kaleidos solemos preparar unas sesiones de postmortem donde revisamos con calma las decisiones tanto tecnológicas como de equipo que tomamos al inicio, con el objetivo de mejorar de cara a los próximos proyectos.

En el caso del equipo de front, era la primera vez que nos embarcábamos como equipo en un proyecto de cierta envergadura con Vue.Js, si bien algunos de nosotros habíamos tenido experiencias previas con el framework. Así pues, al final del proyecto hemos hecho una puesta en común de aquellos aspectos del framework que nos han convencido y aquellos que no.

Kaleidos vs Vue

Las críticas que leeréis a continuación no son más que eso, aspectos del framework que creemos que se podrían mejorar basados en nuestra experiencia en este proyecto y en otros frameworks: desde React a Angular, pasando por LitElement o Svelte.

Por supuesto, podemos habernos equivocado o encontrar casos donde exista un workaround que no hemos descubierto para solventar el problema (¡si conocéis ese workaround, contádnoslo!). No somos ni pretendemos dar una lección sobre Vue. Simplemente, nos ha sido difícil encontrar una alternativa, lo que ya es suficiente motivo para nombrarlo.

Esperamos que pueda resultar útil a aquellos equipos que comiencen un nuevo proyecto con Vue.

Componentes

Uno de los aspectos claves de un framework es el desarrollo por componentes, y cada uno lo hace a su manera. En este proyecto hemos detectado algunos defectos en comparación con los custom elements nativos o el sistema de creación de componentes de otros frameworks.

Al instanciar un componente de Vue, en el HTML no se mantiene el selector con el que se instancia sino que se sustituye por el elemento root del componente y concatena las clases de la instancia y del root. Si el desarrollador no conoce este comportamiento, puede ser muy confuso. Por ejemplo, si tratase de encontrar en las developer tools el componente para debuggearlo, simplemente no existe. Ni siquiera sería sencillo detectar el root de un componente a no ser que el mismo desarrollador lo haya creado o lo conozca bien.

En nuestro caso, recurríamos a usar como clase del root de cada componente una convención class=prefijo-root-nombre de modo que era sencillo reconocer dónde estaba cada componente con un solo vistazo al código.

Esto también impide aprovechar toda la potencia del IDE. En VSCode, por ejemplo, nada diferencia un componente de un tag de HTML. No se puede (o al menos no es intuitivo) navegar directamente a un componente desde otro componente desde el template haciendo un click en el navegador o validar si está recibiendo las props requeridas. En caso de error, no se detecta generalmente hasta runtime.

Otro detalle es que cada vez que se usa el componente es necesario indicar en el padre que se va a usar, o cargar todos los componentes de forma global, que puede que no sea la mejor idea pensando en la eficiencia. En otros frameworks basta con importar los componentes sin necesidad de tener que explicitar que componentes hijos van a usar.

En Vue, además de explicitarlos los tags de los componentes no son inmutables, sino que un mismo componente puede tener diferentes tags en función del componente que lo instancia. Por ejemplo:

1
2
3
4
5
6
@Component({
components: {
'kld-button': KldButtonComponent,
'utx-card':KldCardComponent,
},
})

Y si en otro componente les ponemos otro nombre, la confusión está servida.

1
2
3
4
5
6
@Component({
components: {
'kld-main-button': KldButtonComponent,
'utx-pending-card':KldCardComponent,
},
})

La solución más sencilla es evitar nombrar a los componentes y usarlos tal como se importan:

1
2
3
4
5
6
@Component({
components: {
KldButtonComponent, // <kld-button-component>
KldCardComponent, // <kld-card-component>
},
})

Más específicamente, de los templates hemos echado en falta sobre todo que el root de nuestro componente siempre tenga que ser un elemento. En ocasiones esto nos ha obligado a incluir elementos innecesarios en nuestro HTML para poder crear un componente que tuviese más de un elemento root. Sí, se puede tener más de un root con componentes funcionales, pero entonces ¿por qué no se comportan del mismo modo los componentes normales?

Por otro lado tenemos la sensación que, si bien cuando Vue nace el framework acertó especialmente cogiendo lo mejor del templating de AngularJS y del conocimiento de React y añadiendo pequeños detalles, lo cierto es que durante estos años no ha evolucionado apenas. Los templates de Vue se han quedado un poco obsoletos. En el caso de Angular, que no usa JSX, los templates son ahora mucho más potentes y sencillos. Específicamente hemos visto que sería muy útil:

  • Disponer de optional chaining.
  • Reutilizar bloques de HTML
  • La integración de los templates con Typescript

Por último, creemos que el sistema de transiciones y animaciones, que claramente beben de AngularJS, han quedado obsoletas. Es engorroso envolver un elemento en un tag para posteriormente trabajar sobre clases auto-generadas en el CSS. Con la potencia que tiene el data binding y las Web Animations API debería ser mucho más explícito y directo trabajar animaciones complejas. A mi, personalmente, me gusta mucho más el nuevo modelo de Angular.

Gestión de estados

Dada la naturaleza del proyecto, la gestión de estado ha sido muy simple, sin grandes necesidades. Sólo existe un formulario y la comunicación con la API es prácticamente unidireccional. Por ello hemos optado por seguir las recomendaciones habituales del framework y utilizar Vuex junto con una biblioteca (vuex-module-decorators), que ofrece una interfaz más propia de un lenguaje otrientado a objetos.

No obstante, hubo dos problemas que tuvimos que solventar:

  • las propiedades del store no inicializados (o undefined) no son reactivas, hay que tener siempre en cuenta la regla de la reactividad de Vue para declarar propiedades reactivas e inicializar las propiedades de nuestro estado a algún valor (null, por ejemplo) o utilizar Vue.set(). Si no, aparecerán errores en el renderizado de las templates o experimentamos pérdida de la reactividad en algunas propiedades que serán difíciles de trazar a simple vista. Aquí se puede ver un claro ejemplo descrito en StackOverflow. La inicialización del estado queda de la siguiente manera:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    @Module({
    stateFactory: true,
    dynamic: true,
    namespaced: true,
    name: moduleName, store
    })
    export class SampleModule extends VuexModule {
    public pan: Plan | null = null;
    public statsData: StatsData | null = null;
    public totalAnalysis: TotalAnalysis | null = null;
    ...
    }
  • no se puede activar el strict mode con un volumen de datos elevado lo que imposibilitaba la utilización de herramientas de debug/logging para Vuex. Nuestra aplicación tenía una alta carga de representación de datos, por ello algunas llamadas a la API podían llegar a devolver unos cientos de KBs de información. Con el strict mode activado la aplicación se quedaba congelada durante unos minutos cada vez que se realizaba una mutación del store. Esto imposibilitaba el uso del logger de vuex, por ejemplo. Hay más información al respecto en la documentación del strict mode de Vuex.

Como bola extra, las directivas no soportan estado ni métodos internos por lo que no son tan flexibles ni potentes como las de otros frameworks. Existe una issue en la que se relata la necesidad de crear una directiva para implementar la funcionalidad del drag-and-drop; en ella, además de hablar de la simplificación que se realizó sobre la interfaz con el salto a la versión 2.x, recomiendan el uso de un componente, lo que generaría un <div> innecesario en nuestra página y está restringida al scope al del propio componente.

Otra opción sería almacenar la información en el propio elemento del DOM (mediante el uso de la interfaz de dataset) como se puede ver en el artículo Vue: Cross Directive Data Sharing de Chris Washington. No obstante, esta solución no deja de ser un “workaround”.

Typescript

Una de las cosas que nos hubiese gustado ver en Vue es una mejor integración con Typescript que esperamos mejore con Vue 3 al estar siendo escrito en Typescript.

Empezamos con la instalación: montarnos un proyecto Vue + Typescript es bastante sencillo. Cuando ejecutamos vue create elegimos Manually select features, seleccionamos Typescript y si queremos, como fue nuestro caso, podemos añadir también class-style component syntax. Vue también nos configurará e instalará en el proyecto vue-class-component y vue-property-decorator.

La primera funcionalidad que echamos en falta es poder usar el tipado de Typescript en las Props y que sea suficiente para obtener los Warning en runtime de Vue.

En el siguiente ejemplo esperaríamos que al pasar un número al componente Test este nos diese el warning de Invalid prop pero no lo hace. Para arreglarlo tendríamos que añadir también el tipo en Prop @Prop(String) por que lo que tendríamos el tipo repetido.

1
2
3
4
@Component
export default class Test extends Vue {
@Prop() private msg!: string;
}
1
<Test :msg="4" />

Por suerte podemos arreglarlo instalando reflect-metadata y añadiendo emitDecoratorMetadata: true al tsconfig.json, Entonces, importando reflect-metadata, tendríamos el Invalid prop warning sin necesidad de tipar dos veces la Prop.

Pero hay partes donde la integración no es perfecta como en los campos por defecto. Normalmente en typescript haríamos algo así, hemos puesto un valor por defecto a msg y Typescript infiere el tipo de msg. Con esto Vue nos daría el siguiente Warning Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders.

1
2
3
4
@Component
export default class Test extends Vue {
@Prop() private msg = 'default text';
}

La solución es seguir poniendo default sin inferencia de tipos.

1
2
3
4
@Component
export default class Test extends Vue {
@Prop(default: 'default text') private msg!: string;
}

Con tipos complejos, como una interfaz, tampoco funcionan todo lo bien que nos gustaría. En el siguiente ejemplo, en el que usamos una interfaz, la única comprobación que se realiza es en runtime, simplemente para comprobar si es un Objeto o no.

1
2
3
4
5
6
7
8
9
export interface Complex {
name: string,
value: number
}

@Component
export default class Test extends Vue {
@Prop() private obj!: Complex;
}
1
2
3
<div id="app">
<Test :obj="{otherObj: true}" />
</div>

Hemos tenido otros problemas menores que nos obligaron a seguir teniendo que poner el tipo el el Prop pero parece que en las últimas versiones se han ido arreglando.

Dentro del tag <template> no funciona la comprobación de tipos de typescript, por lo que una gran parte del código del proyecto se quedará sin aprovechar esta funcionalidad. Vemos un ejemplo.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<template>
<div>
<button @click="test('xx')">Click me</button>
</div>
</template>


<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';

@Component
export default class Test extends Vue {
public test(num: number) {
return num * num;
}
}
</script>

Aquí claramente hay un bug en el click porque estamos llamando a test con un string cuando espera un number y no veriamos el problema hasta llegar a runtime. Para prevenir el error sin llegar a runtime ni pasarnos a JSX tendríamos que usar vetur y ejecutar vti diagnostics; no es tan cómodo como tener un soporte directo de Typescript en los templates pero es mejor que nada.

Componentes funcionales

Los componentes funcionales son componentes “ligeros” que no poseen estado (no hay datos reactivos) y no existe instancia asociada (no hay this) por lo que no aparecerán en el árbol de componentes (de las DevTools).

A pesar de su utilidad, hay ciertos aspectos que no nos han gustado:

  • no se pueden definir como clases si utilizas Typescript, como comentan en esta issue de GitHub. La reutilización mediante la extensión o composición se vuelve algo más compleja.

  • la clase asignada desde el padre se comporta de forma diferente a como lo hace en los otros componentes, como por ejemplo si quieres pasarle custom properties ya que no se propagan como en los componentes normales. Esto dificulta notablemente la personalización del aspecto de componentes funcionales mediante el uso de custom properties. Habría que utilizar las props para ello. En esta issue de GitHub hay una explicación más exhaustiva al respecto.

  • no se llevan bien con vue-i18n o con otros servicios inyectados, debido a que no existe instancia asociada; respecto a la biblioteca de internacionalización, estos componentes no tienen acceso a $t directamente. Más info en esta issue de GitHub o en esta otra issue.

  • no poseen la propiedad components por lo que no se pueden utilizar otros componentes fácilmente. Para ello habría que inyectarlos como se explica en esta issue. Actualmente existe un Pull Request abierto.

Existe un artículo de Vinícius Hoyer en el que se muestras todas las diferencias que existen y que han de tener en cuenta para evitar los errores más comunes con los componentes funcionales de Vue.

Cosas que nos siguen gustando

A pesar de estos “pains”, Vue nos ha dejado un buen sabor de boca:

  • Gracias a su baja curva de aprendizaje, ha facilitado la entrada de nuevos miembros al equipo que se han puesto al día muy rápidamente y han sido productivos desde el primer momento.
  • Disfrutamos con la posibilidad de gestionar la lógica de negocio, los estilos y la representación de datos en capas separadas, algo que con React, por ejemplo, es imposible.
  • El DSL de las templates es más simple que en otros frameworks.
  • Podemos trabajar con CSS puro, sin mezclarlo con javascript.
  • Posee un ciclo de vida menos complejo que otros frameworks.
  • Está muy bien integrado con Webpack y es muy fácil su configuración y personalización.
  • Vue es menos dogmático que otros frameworks, lo que en ocasiones facilita aplicar soluciones más sencillas/específicas para solucionar ciertos problemas, sin la necesidad de añadir capas de abstracción.
  • Errores claros, que facilitan las labores de debug y correción de bugs.
  • Gestión sencilla de componentes dinámicos (mediante :is).
  • Facilidad de acceso entre componentes padres e hijos

Conclusiones

Tomar la decisión sobre cuál es el mejor framework para nuestro proyecto es más complejo de lo que a simple vista pueda parecer. Es absurdo tratar de buscar el mejor; aquel que te ofrece absolutamente todo lo que vas a necesitar para resolver tu problema, en el menor tiempo posible y que mejor se adaptará a los posibles cambios que puedan venir en el futuro. Muchas veces esa elección se reduce a optar por aquello con la que el equipo se sienta más cómodo (por su madurez, expertise), o aquello que tenga esa característica que sabes que destaca frente al resto para resolver un aspecto relevante de tu proyecto, aunque flaquee en otros.

Las convenciones que se tomen durante el desarrollo, el flujo de trabajo del equipo o la gestión de la deuda técnica serán aspectos más relevantes que la elección entre Vue, Angular, React o cualquier otro.

Veremos cómo evoluciona Vue: la mayoría de artículos que hablan sobre el futuro del framework nos hace pensar que mucho de lo comentado en este post se solucionará en la próxima versión. Esperemos que así sea.

Este artículo lo firmamos Juanfran Alcántara, David Barragán y Xavier Julián.