Skip to content
En esta página

Methods, computed, watchers

Una de las cosas que suele resultar más confusa cuando uno está empezando a aprender Vue es el tema de los methods, las computed y los watchers. ¿Cuándo, cómo y por qué hay que usar cada uno de los tres?

TL;DR: 🦥️

Los methods son usados (casi siempre) para modificar la información dentro de data, mientras que las computed sirven para obtener algún valor a partir de data pero sin modificarla. En cuanto a los watchers, en la mayoría de los casos pueden ser reemplazados por una solución más simple.

Methods

A primera vista los methods de Vue parecen bastante simples, son como las funciones de JavaScript nativo:

js
export default {

  data: () => ({
    contador: 0
  }),

  methods: {
    incrementar() {
      this.contador++
    }
  }
}

Pero hay una diferencia importante entre los methods y las funciones: los methods no usan return. (Aunque hay algunos pocos casos en que los methods sí usan return como vamos a ver más adelante).

Pero, ¿por qué no tienen return? Porque los methods son funciones que pertenecen a un objeto y por lo tanto pueden modificar las propiedades de ese objeto en forma directa mediante el uso del this.

Métodos en un objeto de JavaScript

Veamos cómo funcionan los métodos dentro de un objeto en JavaScript nativo:

js
const objeto = {
  
  numero: 0,

  incrementar() {
    this.numero++
  }
}

objeto.incrementar()
console.log(objeto.numero) // 1
objeto.incrementar()
console.log(objeto.numero) // 2

incrementar() no es una función, es un método, porque pertenece a un objeto y puede acceder a las propiedades del objeto con el this, que hace referencia al objeto mismo.

Entonces, no es necesario que el método incrementar retorne nada para modificar this.numero porque puede acceder a las propiedades del objeto al cual pertenece en forma directa.

Funciones puras

Esto es muy distinto a las así llamadas funciones puras, que según los evangelistas de la progrmación funcional deberían ser el único tipo de funciones usadas en JavaScript 🙄️

Las funciones puras simplemente reciben un input (los parámetros) y retornan un output (el return):

js
function incrementar(numero) {
  return numero + 1
}
console.log(incrementar(1)) // 2
console.log(incrementar(1)) // 2

A diferencia de un método, una función pura siempre devuelve el mismo valor cada vez que es llamada.

Un ejemplo claro de función pura es el algoritmo SHA-256, el usado por Bitcoin para crear hashes y que está disponible en forma nativa en Node.js dentro de su módulo crypto (no es necesario instalar un módulo de npm para usarlo):

js
import { createHash } from 'crypto'

const hash = data => createHash('sha256').update(data).digest('hex')

const plainText = 'satoshi' 

console.log( hash(plainText) ) // da2876b3eb31edb4436fa4650673fc6f01f90de2f1793c4ec332b2387b09726f
console.log( hash(plainText) ) // da2876b3eb31edb4436fa4650673fc6f01f90de2f1793c4ec332b2387b09726f

Si el param que se le pasa es el mismo, el resultado del algoritmo siempre es igual.

Funciones impuras

Cuando una función depende de un valor externo a ella deja de ser pura:

js
let numero = 0

function incrementar() {
  return numero + 1
}

Este tipo funciones impuras puede dar lugar a resultados inesperados como en el ejemplo del IVA infinito 😬️

js
let numero = 0

function calcularIva(precio) {
  return precio * 21 / numero
}

console.log( calcularIva(400) ) // Infinity 🤦‍♂️️

Pero los métodos, a diferencia de las funciones, son impuros por definición, ya que dependen de los valores del objeto al cual pertenecen, como en el ejemplo del contador dentro del objeto acá arriba.

Y esto no es algo malo, bien usados los métodos tienen mucha utilidad. Por ejemplo, para generar reactividad Vue usa internamente objetos con métodos, a diferencia de React que genera reactividad en forma funcional, invocando expresamente el setter de useState() (y por eso hay gente que dice que React no es realmente reactivo 😅️).

Funciones en Vue

Pero entonces, ¿en Vue los methods reemplazan a las funciones? No, para nada, en Vue podemos usar tanto methods como funciones puras, y también objetos normales de JavaScript con métodos dentro.

Es cierto que muchas veces van a ver que los methods de Vue son usados (innecesariamente 🤷‍♂️️) como funciones:

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

export default {

  data: () => ({
    products: [],
    error: null,
    loading: true
  }),
  created() {
    this.getData(url)
  },
  methods: {
    async getData(url) {
      // Instanciar un AbortController
      const controller = new AbortController()
      const { signal } = controller
      try {
        // Si la llamada a la API se demora más de 5 segundos, abortar
        setTimeout(() => controller.abort(), 5000)
        await this.delayResponse(200) // Delay artificial para evitar 
        // que el spinner haga un "flash" si la API responde muy rápido
        const res = await fetch(url, { signal })
        const products = await res.json()
        // Cambiar nombre de la propiedad de la imagen
        // para evitar un error en <img src="producto.imgsrc">
        this.products = this.normalizeImgProp(products)
      } catch (error) {
        // Mostrar mensaje de error al usuario 
        this.error = 'Error de conexión!'
        console.log(error)
      } finally {
        // Luego de la respuesta de la API, ocultar el spinner
        this.loading = false
      }
    },
    // Delay artifical
    delayResponse(miliseconds) {
      return new Promise(resolve => setTimeout(resolve, miliseconds))
    },
    normalizeImgProp(array) {
      // Método para evitar un error en caso de que la API
      // tenga un formato distinto (avatar, image, etc) para la prop imgsrc 
      // que es usada en el tag <img src="product.imgsrc">
      return array.map(item => ({
        ...item,
          imgsrc: item.avatar || item.image || item.thumbnail ||
            item.imgurl || item.imgUrl|| item.imgSrc || item.imgsrc
      }))
    }
  }
}

Pero esto es considerado una mala práctica. Primero porque estamos abultando el código del componente, haciéndolo más difícil de entender. Y segundo porque cualquiera de estos tres métodos (getData, delayResponse y normalizeImgProp) podría llegar a ser necesario en otro componente. Pero al estar declarados dentro de methods, para poder usarlos en otros componentes habría que implementear un provider, que de todas formas sólo funciona si los componentes están emparentados... 🥱️

Sería mucho mejor tenerlos en un archivo .js (no .vue) aparte e importarlos en los componentes que los necesiten.

Por ejemplo, usando una factory function que retorna un objeto nuevo cada vez que es instanciada (por eso los paréntesis envolviendo las llaves):

js
// src/services/fetchService.js

export const fetchService = () => ({

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

  async getData(url) {

    // Instanciar el AbortController
    const controller = new AbortController()
    const { signal } = controller

    try {
      // Si la llamada a la API se demora más de 5 segundos
      // que sea cancelada por el AbortController
      setTimeout(() => controller.abort(), 5000)
      // Delay artificial para evitar que el spinner haga un "flash"
      // si la llamada a la API es demasiado rápida
      await delayResponse(200)
      // Llamada a la API
      const res = await fetch(url, { signal })
      const data = await res.json()
      // Normalizar la propiedad imgsrc de la API:
      const normalizedData = normalizeImgProp(data)
      this.data = normalizedData
      // En algunos casos es más conveniente que la data sea retornada
      // por ejemplo, si el método es accedido desde una store
      return normalizedData

    } catch (error) {
      console.log(error)
      this.error = error
      // Si es accedido desde una store, 
      // es conveniente que el método retorne un objeto de error:
      return { error }

    } finally {
      // Al finalizar la llamada ocultar el spinner
      this.loading = false
    }
  }
})

// Función para generar un delay artificial
// en la llamada a la API
export function delayResponse(ms) {
  return new Promise(resolve => setTimeout(resolve, ms))
}

// Función para para evitar un error en caso de que la API
// tenga un formato distinto (avatar, image, etc) para la prop imgsrc 
// que es usada en el tag <img src="product.imgsrc">
export function normalizeImgProp(array) {
  return array.map(item => ({
    ...item,
      imgsrc: item.avatar || item.image || item.thumbnail ||
        item.imgurl || item.imgUrl|| item.imgSrc || item.imgsrc
  }))
}

Y luego en el componente:

js
const { VITE_API_URL: url } = import.meta.env
import { fetchService } from '@/services/fetchService'
// Desestructurar las propiedades de la factory function:
const { getData, data, error, loading } = fetchService()

export default {

  data: () => ({ 
    // Declarar las propiedades en data para que se vuelvan reactivas
    getData, data, error, loading
  }),

  created() {
    this.getData(url)
  }
}

Y así el componente quedó mucho más conciso y los métodos son reutilizables en cualquier otro componente 🥳️

Además, cuando se trata de métodos que no hacen uso del this (como es el caso de delayResponse y normalizeImgProp) no es necesario tenerlos dentro del componente porque no necesitan acceder a ninguna propiedad de data. Esto es porque son funciones puras: simplemente se les pasa un parámetro y retornan un valor.

Uso de this en data

Como habrán visto en el ejemplo de acá arriba (y en todos los ejemplos de este sitio) para declarar las propiedades de data estoy usando una arrow function que retorna un objeto (por eso los paréntesis envolviendo las llaves):

js
data: () => ({
  products: [],
  error: null,
  loading: true
}),

Admito que esto puede llegar a ser riesgoso, ya que en el caso de que necesitemos usar this dentro de data tendríamos un problema porque, como ya saben, en las arrow functions el this pierde su contexto y al intentar usarlo daría un error:

⚠️

[Vue warn]: Error in data(): "TypeError: undefined has no properties"

Pero... en realidad es muy poco probable que necesitemos usar this dentro de data.

De hecho, en la documentación de Vue hay varios ejemplos en los que declaran data con una arrow function.

Si por ejemplo necesitamos cambiar el valor de una prop que llega del componente padre:

js
export default {

  props: {
    precio: Number
  },
  data() { 
    return {
      precioProducto: this.precio,
      precioConIva: 0
    }
  },
  methods: {
    sumarIva() {
      this.precioConIva = this.precioProducto * 1.21
    }
  }
}

Se puede hacer y funciona, pero no sería una buena idea, porque haciendo esto se pierde la conexión reactiva con el componente padre y el valor en data pasa a tener un estado independiente, lo cual complejiza la aplicación innecesariamente.

Sería mucho mejor usar una computed:

js
export default {

  props: {
    precio: Number
  },
  computed: {
    precioConIva() {
      return this.precio *= 1.21
    }
  }
}

💡️

Si se les ocurre algún caso en que sea útil y necesario usar this dentro de data escríbanme para contarme!

Methods, funciones y reactividad

Otra gran diferencia entre los methods y las funciones es que los methods son accesibles dentro del template del componente mientras que las funciones normales no.

Si se fijan en este ejemplo de la lista de alumnos, la única forma de pasar parámetros desde un v-for en el template a un método es teniendo un method dentro del script que reciba ese parámetro y retorne algo:

html
<tr v-for="alumno in alumnos" :key="alumno.id"
  @click="presenteAusente(alumno.id)"
  :class="colorFondo(alumno.presente)"
>

Y en el script:

js
methods: {
  presenteAusente(id) {
    const alumno = this.alumnos.find(alumno => alumno.id === id)
    alumno.presente = !alumno.presente
  },
  colorFondo(presente) {
    return presente && 'fondo-verde'
  },
}

En cada fila <tr> de la tabla el method colorFondo es invocado, tomando como parámetro la propiedad booleana presente. Si presente es true el color de fondo de la fila pasa a ser verde.

Esto no podría ser logrado con una computed porque las computed no reciben parámetros.

En realidad hay algunos casos en que las computed sí reciben params, como vamos a ver más adelante, pero son muy excepcionales.

Pero entonces, ¿para qué sirven las computed?

Propiedades computadas

Mientras que los methods se usan (casi siempre) para cambiar un valor en data las computed se usan para obtener algún valor a partir de data (o de una prop) pero sin modificar data.

Por ejemplo, en un componente de un e-commerce puedo tener una prop que viene del componente padre con los datos de un producto, incluyendo una propiedad booleana (inCart) que es true si el producto ya está en el carrito de compras. Y lo que quiero es que el botón para agregar el producto al carrito muestre un texto distinto dependiendo de si ya está en el carrito o no:

js
export default {

  props: {
    product: {
      id: String,
      name: String,
      price: Number,
      inCart: Boolean
    }
  },

  computed: {
    buttonText() {
      return this.product.inCart ? 'Agregado' : 'Agregar al carrito'
    }
  }
}

Para el texto del botón tengo que usar una computed, no un method, ya que no quiero cambiar el estado de la propiedad inCart, sólo retornar un texto distinto dependiendo de su valor.

Otro ejemplo es el caso de la prop con el precio y la computed para calcular el IVA acá arriba. Mutar el valor del precio sería una mala idea, porque perderíamos la información sobre el precio original.

Entonces, en líneas generales se podría decir que los methods actúan como setters (provocan mutaciones) mientras que las computed actúan como getters (obtienen un valor).

Otra ventaja de las computed es que se almacenan en la memoria caché lo cual evita que sean re-ejecutadas en cada nuevo renderizado del componente.

Usando una computed como getter y setter

Hay algunos casos (muy excepcionales) en los que es necesario dividir una computed entre su función como getter y su función como setter.

Por ejemplo, en el caso de que necesitemos sincronizar el estado de un input que usa v-model con el estado global de Vuex o de una store. Podría ser que quisiéramos usar la store para comunicar la información de ese v-model entre distintos componentes. El problema es que v-model genera mutaciones en data, porque cada vez que el usuario ingresa algo en el input la propiedad correpondiente en data va cambiando sincronizada con el valor del input (para eso sirve v-model). Y según las reglas del patrón Flux no es conveniente que un componente genere mutaciones en la store en forma directa.

Entonces, ¿cómo resolver este problema? Dividiendo la computed en get y set:

js
import { formStore } from '@/stores/formStore'

export default {

  data: () => ({
    formStore
  }),

  computed: {
    nombre: {
      get() {
        return this.formStore.form.nombre
      },
      set(newValue) {
        this.formStore.setNewValue('nombre', newValue)
      } 
    }
  }
}

Y en el template:

html
<input
  type="text" 
  v-model="nombre" 
  placeholder="Nombre(s)"
/>

Y en la store:

js
// Setter para v-model
setNewValue(field, newValue) { 
  this.form[field] = newValue
}

Watchers

Los watchers sirven para reaccionar a los cambios y disparar algún método ante el cambio.

Por ejemplo, este watcher dispara un nuevo fetch (que está declarado por fuera del componente en el archivo fetchService.js) cada vez que la URL cambia:

js
const { 
  VITE_API_URL1: url1,
  VITE_API_URL2: url2,
  VITE_API_URL3: url3 
} = import.meta.env

const urls = { url1, url2, url3 }

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

export default {

  data: () => ({
    url: '', 
    getData, data, error
  }),
  watch: { 
    url(newUrl) { 
      this.getData(urls[newUrl]) 
    } 
  },
  methods: {
    fetchData(url) {
      this.url = url 
    }
  }
}

En principio esto parecería ser muy útil, pero lo cierto es que en la mayoría de los casos los watchers pueden ser reemplazados por alguna solución más simple.

Por ejemplo, si el cambio fue producido por una interacción del usuario, simplemente se puede disparar la acción desde el v-on:

html
<button @click="getData(url1)">Fetch URL 1</button>

O este ejemplo de watcher en la documentación de Vue: funcionaría exactamente igual borrando el watcher y reemplazándolo con un @change en el <select>:

html
<template>
  <div>

    <select size="3" v-model="selected" @change="changeName">
      <option v-for="name in names" :key="name">{{ name }}</option>
    </select>

    <p>Nombre: {{ firstName }}</p>

    <p>Apellido: {{ lastName }}</p>

  </div>
</template>

<script>

export default {

  data: () => ({
    // Evan Vue, Dan React y Rich Svelte ☺️
    names: ['You, Evan', 'Abramov, Dan', 'Harris, Rich'],
    selected: '',
    firstName: '',
    lastName: ''
  }),

  watch: {
    // No es necesario usar watch
    // selected(newValue) {
    //   [this.lastName, this.firstName] = newValue.split(', ')
    // }
  },

  methods: {
    changeName() {
      [this.lastName, this.firstName] = this.selected.split(', ')
    }
  },
}

</script>

Hasta ellos mismos recomiendan evitar los watchers:

...a menudo es una mejor idea usar una propiedad computada en lugar de una imperativa llamada a watch.

Conclusión

Los methods sirven para cambiar el estado del componente (o sea, la información en data) sin necesidad de usar un return para esto.

El único caso en que los methods retornan algo es cuando se necesita obtener algún valor en base a un parámetro que se les pase desde el template.

Y los methods no reemplazan a las funciones. Una aplicación de Vue puede contener tanto methods como funciones normales. Y cuanto más lógica podamos tener por fuera del componente mejor, así el componente queda más claro y conciso, y además podemos reutilizar esa lógica en cualquier otro componente.

En cuanto a las computed, sirven para obtener algún valor a partir de data sin modificar data.

Y los watchers... hay muy pocos casos en que sean realmente necesarios. Si se les ocurre alguno escríbanme para contarme!