Skip to content
En esta página

¿Vue 2 vs. Vue 3?

En las redes de Vue como r/vuejs o @vuejs se pueden encontrar muchos posts con títulos como 'Vue 2 vs. Vue3', 'Vue 2 Is Dead', 'Stop Using Vue 2'. Estos títulos pueden sonar un tanto dramáticos pero tienen algo de sentido porque a mediados del 2022 Evan You, el creador de Vue, anunció que a partir del 31 de Diciembre de 2023 Vue 2 va a pasar al estado End Of Life 😳️

Lo que los autores de esos posts no tienen en cuenta es que Vue 2 y 3 son solamente las versiones de la librería de Vue (o mejor dicho, de la biblioteca de Vue, o sea, la library) pero todo lo que estaba disponible en Vue 2 sigue disponible en Vue 3 (salvo algunas excepciones).

El malentendido se produce porque cuando los developers dicen Vue 2 en realidad se están refiriendo a la Options API y cuando dicen Vue 3 se refieren a la Composition API. Pero por costumbre se sigue diciendo Vue 2 y Vue 3 (y yo también lo hago, como habrán visto en distintos posts de este sitio 🤷‍♂️️).

Options API vs. Composition API

Pero, ¿qué son la Options API y la Composition API?

La Options API es la forma de crear la lógica de los componentes de Vue en base a las opciones data, props, computed, methods, etc.

js
// OPTIONS API

export default {

  data: () => ({
    numero: 0
  }),
  computed: {
    doble() {
      return this.numero * 2
    }
  },
  methods: {
    incrementar() {
      this.numero++
    }
  }
}

Es un tipo de estructura basado en el paradigma de la programación orientada a objetos (lo que se suele llamar, por sus siglas en inglés, OOP) que se caracteriza por el uso del this para acceder a las propiedades de un objeto.

Mientras que en la Composition API la lógica está organizada de una forma más acorde al paradigma de la programación funcional y no usa this:

js
// COMPOSITION API

import { ref, computed } from 'vue'

const numero = ref(0)
const doble = computed(() => numero.value * 2)
const incrementar = () => numero.value++

Como ven, el código de la Composition API es más corto y conciso. Pero que sea corto y conciso no es necesariamente algo bueno, porque en componentes más complejos esto puede hacer que el código termine siendo más enroscado 😕️

Agrupar la lógica por funcionalidad, no por su relación al estado

Por ejemplo, esta es la lógica de una lista de tareas en la Options API:

js
export default {

  components: {
    InputTask, TaskItem
  },
  
  data: () => ({
    input: '', 
    items: []
  }),

  computed: {
    totalItems() {
      return this.items.length
    },
    isCompleted() {
      return this.items.filter(item => item.completed).length
    }
  },

  methods: {
    addItem() {
      if (this.input) {
        this.items.push({
          text: this.input, 
          completed: false
        })
      }
    },
    checkItem({index, checked}) {
      this.items[index].completed = checked
    },
    deleteItem(index) {
      this.items.splice(index, 1)
    }
  }
}

Aunque a primera vista tal vez no se entienda bien cómo funciona cada computed y method está claro que las computed obtienen algún valor a partir del estado del componente (o sea, data) mientras que los methods provocan cambios en el estado.

Y esta es la misma lógica en la Composition API:

js
import { ref, computed } from 'vue'

const input = ref('')
const items = ref([])

const addItem = () => input.value && items.value.push({text: input.value, completed: false})
const deleteItem = index => items.value.splice(index, 1)
const totalItems = computed(() => items.value.length)

const checkItem = ({index, checked}) => items.value[index].completed = checked
const isCompleted = computed(() => items.value.filter(item => item.completed).length)

Sí, es más conciso, pero también es más confuso porque en la Composition API las funciones ya no están organizadas en relación al estado del componente sino que se agrupan por funcionalidad: las que tienen que ver con agregar, borrar y obtener el total de tareas van juntas, no importa si son getters (obtener algo del estado) o setters (mutar el estado). Y lo mismo las que tienen que ver con chequear si el item fue completado: checkItem es un setter mientras que isCompleted es un getter, pero se las agrupa juntas por su funcionalidad.

Para muchos programadores esto es una mejora notable en comparación con la Options API, y ésta fue la principal motivación del creador de Vue para introducir este cambio, como él mismo explica acá.

El problema es que esta agrupación por funcionalidad de la Composition API no es obligatoria, queda a criterio del programador. Perfectamente uno podría agruparlas de otra forma. En cambio en la Options API uno está obligado a poner los methods dentro de methods, las computed dentro de computed, etc. Esta imposición nos obliga a tener un código más organizado mientras que en la Composition API la estructura del código depende del criterio del programador, que en cada caso puede ser distinto, y eso hace que sea más difícil trabajar en equipo.

En definitiva, el código de la Options API es más organizado mientras que el de la Composition API parece una ensalada de funciones de JavaScript desparramadas por ahí 😒️

Es como si de repente Vue se hubiese convertido en... React 😭️

Reutilización del código

Otra de las motivaciones del creador de Vue al introducir la Composition API fue la de evitar las repeticiones cuando distintos componentes necesitan usar la misma lógica, tal como explica acá.

Por ejemplo, es muy común que distintos componentes necesiten tener un method que haga un fetch a una API REST:

js
methods: {
  async getData(url) {
    try {
      const res = await fetch(url)
      this.products = await res.json()

    } catch (error) {
      console.log(error)
      this.error = error

    } finally {
      this.loading = false
    }
  }
}

Si ponemos esto dentro de un method hay que volver a repetir el method en cada componente que necesite hacer un fetch: para productos, para órdenes de compra, para información del usuario, etc.

En cambio, en la Composition API es posible poner la lógica del fetch por fuera del componente, en un archivo .js aparte, y luego importarla en cada componente que la necesite:

js
// src/composables/useFetch.js

import { ref } from 'vue'

export const useFetch = () => {

  const data = ref(null)
  const error = ref(null)
  const loading = ref(true)

  const getData = async url => {
    try {
      data.value = await (await fetch(url)).json()
    } catch (error) {
      console.log(error)
      error.value = error
    } finally {
      loading.value = false
    }
  }
  return { getData, data, error, loading }
}

Y luego, en el componente:

html
<script setup>

import { onMounted } from 'vue'
import ProductCard from '@/components/ProductCard.vue'
import { useFetch } from '@/composables/useFetch'

const { VITE_API_URL: url } = import.meta.env
const { getData, data, error, loading } = useFetch()

onMounted(() => getData(url))

</script>

Esto es lo que se suele llamar una composable, es decir, una función externa a los componentes pero que tiene estado y reactividad propias.

Sí, funciona, pero tampoco resulta muy convincente porque en la Options API se puede lograr algo muy similar, por ejemplo, usando una factory function que retorne un objeto con sus propiedades y métodos:

js
// src/services/useFetch.js

export const useFetch = () => ({

  data: null,
  error: null,
  loading: true,

  async getData(url) {
    try {
      this.data = await (await fetch(url)).json()
    } catch (error) {
      console.log(error)
      this.error = error
    } finally {
      this.loading = false
    }
  }
})

Y luego en el componente que necesite usarla:

html
<script>

import ProductCard from '@/components/ProductCard.vue'
import { useFetch } from '@/services/useFetch'
const { getData, data, error, loading } = useFetch()

const { VITE_API_URL: url } = import.meta.env

export default {

  components: {
    ProductCard
  },
  data: () => ({
    // Declarar las propiedades de useFetch()
    // en data para que se vuelvan reactivas:
    getData, data, error, loading
  }),
  created() {
    // Ejecutar el fetch al crear el componente:
    this.getData(url)
  }
}
</script>

La principal diferencia es que en este caso hay que declarar las propiedades de useFetch() dentro del objeto data para que se vuelvan reactivas mientras que en la Composition API se puede generar reactividad por fuera de los componentes usando ref.

Creando stores con la Composition API

La facilidad para generar reactividad por fuera de los componentes en la Composition API también puede aprovecharse para crear stores simples usando el método reactive:

js
import { reactive } from 'vue'

export const cartStore = reactive({

  cart: [],
  
  cartTotal() {
    return this.cart.reduce((total, item) => total + item.qty, 0)
  },

  cartTotalPrice() {
    return this.cart.reduce((total, item) => total + item.total, 0)
  }
}

Y luego en el componente que necesite usar la store:

html
<script setup>

import { computed } from 'vue'
import { cartStore } from '@/stores/cartStore'

const total = computed(() => cartStore.cartTotal())
const totalPrice = computed(() => cartStore.cartTotalPrice())

</script>

Pero en la Options API también se pueden crear stores:

html
<script>
import cartStore from '@/stores/cartStore'

export default {

  data: () => ({
    cartStore
  }),
  computed: {
    total() {
      return this.cartStore.cartTotal()
    },
    totalPrice() {
      return this.cartStore.cartTolalPrice()
    }
  }
}
</script>

Nuevamente, la diferencia es que al no poder usar reactive en la Options API hay que declarar la store en data para poder usarla en forma reactiva. Pero funciona exactamente igual.

Watchers por fuera de los componentes

Pero, ¿qué pasa si queremos usar un watcher para que, cada vez que la URL cambia, el fetch se vuelva a ejecutar?

Usando la Options API no es posible usar un watcher por fuera de un componente, pero con la Composition API sí:

js
// src/composables/useFetch.js

import { ref, watchEffect } from 'vue'

export const useFetch = () => {

  const data = ref(null)
  const error = ref(null)

  const getData = async url => {
    
    data.value = error.value = null
    
    try {
	  data.value = await (await fetch(url)).json()
    } catch (err) {
      console.log(err)
      error.value = err
    }
  }
  // Esto con la Options API no se podría hacer:
  watchEffect(getData)

  return { data, error, getData }
}

Lo que sí se podría hacer para replicar esto en la Options API es poner el watcher dentro de cada componente que lo necesite:

js
const { 
  VITE_API_URL1: url1,
  VITE_API_URL2: url2,
  VITE_API_URL3: url3 
} = import.meta.env
// Crear un objeto que contenga las urls:
const urls = { url1, url2, url3 }

import { useFetch } from '@/services/useFetch'
const { getData, data, error } = useFetch()

export default {

  data: () => ({
    url: '', 
    getData, data, error
  }),

  watch: {
    // Cada vez que la url cambia se dispara un nuevo fetch
    // con el método getData() de useFetch()
    // que toma como parámetro una propiedad del objeto urls
    // referenciándola por su clave: url1, url2, url2
    url(newUrl) {
      this.getData(urls[newUrl])
    } 
  },

  methods: {
    changeUrl(url) {
      this.url = url 
    }
  }

Y en el template poner botones que al ser clickeados ejecutan el método changeUrl(url):

html
<template>
  <div>
    
    <h1>URL Watcher</h1>

    <button @click="changeUrl('url1')">URL 1</button>
    <button @click="changeUrl('url2')">URL 2</button>
    <button @click="changeUrl('url3')">URL 3</button>

    <pre v-if="data">{{ data }}</pre>
    <pre v-else>{{ error }}</pre>

  </div>
</template>

No es tan consiso como con la Composition API, pero tampoco es imposible de hacer.

Tree shaking

Hay una sola cosa que sí es imposible en la Options API: las importaciones selectivas de los métodos nativos de Vue que facilitan la ejecución del tree shaker de Vite, que se dispara al hacer el build con el comando npm run build.

Lo que hace el tree shaker es descartar todo lo que no se use de la librería de Vue y no incuirlo en el bundle final, lo que permite que las apps hechas con Vue 3 (perdón, con la Composition API 🤷‍♂️️) sean mucho más livianas que las hechas con Vue 2.

Por ejemplo, importar en un componente únicamente los métodos ref y computed (en este ejemplo usando TypeScript con el atributo lang="ts"):

html
<script setup lang="ts">

import { ref, computed } from 'vue'

const counter = ref<number>(0)
const decrement = () : boolean | number => (counter.value > 0) && counter.value--
const increment = () : number => counter.value++
const double = computed<number>(() => counter.value * 2)

</script>

En cambio, los componentes de la Options API necesitan toda la librería de Vue para funcionar. Es como si internamente en cada componente se estuviese haciendo un import Vue from 'vue'.

Sin embargo, hay una forma de hacer que los componentes de la Options API sean más livianos haciendo uso del tree shaking. Esto se puede lograr importando el método nativo de Vue defineComponent():

html
<script>

import { defineComponent } from 'vue'

export default defineComponent({
  
  data: () => ({
    counter: 0
  }),
  methods: {
    increment() {
      this.counter++
    },
    decrement() {
      this.counter > 0 && this.counter--
    }
  },
  computed: {
    double() {
      return this.counter * 2
    }
  }
})
</script>

Y de esta forma se puede usar la Options API en Vue 3 aprovechando el tree shaking de Vite y logrando bundles mucho más chicos 🥳️

Vue CLI 2 vs. Vue CLI 3

Otra confusión que surge al comparar las distintas versiones de Vue es la de referirse a la Options API como Vue CLI 2 y a la Composition API como Vue CLI 3, como una forma de diferenciarlas de Vue CDN, es decir, el uso de Vue sin bundler, solamente como un script importado desde una CDN dentro de un archivo html:

html
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>

Esto es aún más confuso que llamarlas Vue 2 y Vue 3 porque Vue CLI es solamente una interfaz de línea de comandos (Command Line Interface) para crear aplicaciones de Vue con Webpack pero no tiene nada que ver con la versión 2 o 3 de Vue. De hecho, la última versión de Vue CLI es la 5, no la 3.

Y si entran la página de Vue CLI van a ver un cartel que dice:

⚠️ Vue CLI is in Maintenance Mode!

For new projects, it is now recommended to use create-vue to scaffold Vite-based projects.

O sea, Vue CLI fue reemplazado por Vite que en lugar de usar Webpack como bundler usa esbuild que es mucho más rápido y eficiente que Webpack.

En la documentación oficial de Vue lo opuesto de Vue CDN no es llamado Vue CLI sino Vue SFC, es decir: Single File Components, porque son componentes que agrupan la vista (html), los estilos (css) y la lógica (js) dentro de un solo archivo .vue

Conclusión

En definitiva: si están leyendo esto en 2023 Vue CLI ya fue deprecado (es decir, ya no es sostenido por el equipo de Vue) y si están leyendo esto en 2024 Vue 2 también fue deprecado.

Pero la Options API no fue deprecada y nunca lo será 💪️

Bueno... en realidad la palabra nunca en programación es demasiado fuerte, pero piensen nada más en JQuery... ¡es inmortal! 😅️