Introducción a Vue.js
1 Introducción
Vue.js es un framework progresivo de JavaScript orientado principalmente al desarrollo de interfaces de usuario y aplicaciones web. Fue creado por Evan You y publicado por primera vez en 2014. Desde entonces ha evolucionado hasta convertirse en una de las herramientas más utilizadas para desarrollar aplicaciones web modernas.
Vue permite construir interfaces a partir de componentes reutilizables, de manera que una aplicación puede dividirse en pequeñas piezas independientes. Cada componente puede contener su propia estructura HTML, lógica JavaScript y estilos CSS.
Cada componente puede contener su propia estructura HTML, lógica JavaScript y estilos CSS independientes.
Una de las características más importantes de Vue es su sistema de reactividad. Cuando los datos utilizados por una interfaz cambian, Vue detecta esos cambios y actualiza automáticamente las partes de la página que dependen de ellos. Imaginemos un contador, en JavaScript tradicional tendríamos que modificar manualmente el contenido del elemento HTML cada vez que cambia el contador. Con Vue podemos declarar la relación entre el dato y la interfaz, y Vue se encarga de actualizar el DOM cuando sea necesario.
Vue no sustituye a JavaScript. Al contrario, se construye sobre JavaScript y proporciona herramientas y abstracciones que facilitan la creación de interfaces complejas. Por tanto, es importante entender Vue como una capa de abstracción sobre JavaScript, no como un lenguaje de programación independiente.
1.1 Características principales de Vue.js
Vue incorpora diferentes características que facilitan el desarrollo de aplicaciones web.
- Componentes: Un componente representa una parte de la interfaz y puede reutilizarse en diferentes lugares de la aplicación. Una cabecera, menú de navegación, un formulario, lista de productos…
- Reactividad: La reactividad permite que la interfaz se actualice automáticamente cuando cambian los datos de los que depende. Esta característica es fundamental en las aplicaciones web modernas, donde la información de la pantalla cambia constantemente sin necesidad de recargar toda la página.
- Data Binding: Vue facilita la conexión entre los datos de la aplicación y los elementos HTML. Esta conexión permite mostrar datos en la interfaz, modificar atributos HTML dinámicamente, cambiar clases CSS, …
- Directivas: Vue proporciona directivas que permiten controlar el comportamiento de los elementos HTML. Por ejemplo v-if, v-for, v-bind, …
- Composition API: Vue 3 introduce y potencia la Composition API, que permite organizar la lógica de los componentes de una forma más flexible.
- <script setup>: Introducido en Vue 3, simplifica considerablemente la escritura de componentes. Con ella podemos utilizar directamente las funcionalidades de la Composition API sin necesidad de una estructura adicional.
- Integración progresiva: Una de las características históricas de Vue es que puede incorporarse progresivamente a un proyecto. No es necesario convertir una aplicación completa a Vue desde el principio. Puede utilizarse para controlar inicialmente una pequeña parte de una página y posteriormente aumentar su presencia.
- Ecosistema: Vue no se limita al núcleo del framework. Existe un ecosistema de herramientas que permite construir aplicaciones completas. Entre las herramientas más importantes se encuentran:
- Vite para la creación y compilación de proyectos.
- Vue Router para gestionar la navegación.
- Pinia para gestionar el estado compartido.
- Vue DevTools para depurar aplicaciones.
- Vitest para realizar pruebas.
Estas herramientas se estudiarán a lo largo del libro.
1.2 Arquitectura de una aplicación Vue
Una aplicación Vue está formada normalmente por diferentes componentes que se organizan formando un árbol de componentes. Por ejemplo:
- Cabecera con logotipo, barra de búsqueda y botón de login
- Menú de navegación
- Lista de productos
- Tarjeta de producto
- Pie de página
Cada uno de estos elementos puede convertirse en un componente Vue y de esta manera permite separar responsabilidades. “ProductList.vue” puede encargarse de mostrar una lista de productos, mientras que “ProductCard.vue” puede encargarse de representar individualmente cada producto. De esta forma, si posteriormente necesitamos modificar cómo se representa un producto, podemos hacerlo en “ProductCard.vue” sin tener que modificar toda la aplicación.
1.3 Reactividad y componentes
La combinación de componentes y reactividad es uno de los conceptos fundamentales de Vue. En una aplicación tradicional podemos modificar directamente el DOM utilizando las API proporcionadas por JavaScript, pudiendo seleccionar un elemento y modificar su contenido.
En Vue, en cambio, normalmente trabajaremos con un estado de la aplicación y declararemos cómo debe representarse ese estado en la interfaz. Podemos pensar en el funcionamiento de la siguiente forma:
Esto permite que el programador se preocupe principalmente de qué debe mostrar la interfaz en función de los datos, en lugar de tener que indicar manualmente cada modificación del DOM. Esta forma de trabajar se conoce habitualmente como renderizado declarativo.
2 Herramientas del ecosistema Vue
Para desarrollar aplicaciones Vue 3 utilizaremos varias herramientas.
- Node.js: Proporciona el entorno necesario para ejecutar herramientas de desarrollo basadas en JavaScript fuera del navegador. En un proyecto Vue (y en otros frameworks de desarrollo) se utiliza principalmente para ejecutar las herramientas de desarrollo y gestionar las dependencias.
- npm: Es el gestor de paquetes incluido habitualmente con Node.js. Permite instalar las diferentes dependencias utilizadas por nuestro proyecto. Existen alternativas como pnmp, yarm y bun.
- Vite: Es la herramienta utilizada habitualmente para crear y desarrollar proyectos Vue modernos. Proporciona:
- Servidor de desarrollo.
- Recarga rápida durante el desarrollo.
- Compilación para producción.
- Gestión de módulos.
- Integración con Vue.
- Vue Router: Permite implementar la navegación entre diferentes vistas de una aplicación Vue. El navegador puede cambiar entre distintas vistas sin necesidad de cargar una página HTML completamente nueva en cada navegación.
- Pinia: Es la solución oficial de gestión de estado para el ecosistema Vue. Permite compartir datos entre diferentes componentes cuando mantenerlos únicamente dentro de un componente ya no resulta adecuado.
- Vue DevTools: Proporciona herramientas para inspeccionar y depurar aplicaciones Vue. Permite observar componentes, datos reactivos y diferentes aspectos del funcionamiento de la aplicación.
3 Vue y JavaScript
Vue no elimina la necesidad de conocer JavaScript. Al contrario, para trabajar correctamente con Vue es necesario dominar JavaScript. Durante el desarrollo de una aplicación Vue utilizaremos conceptos que ya deberían resultar familiares como funciones felcha, arrays, promesas, programación asíncrona, fetch, eventos… Por este motivo, antes de comenzar a trabajar con Vue es importante tener una base sólida de JavaScript moderno.
3.1 Vue frente a JavaScript sin framework
JavaScript permite crear interfaces web directamente utilizando las API del navegador. Por ejemplo, podemos seleccionar elementos del DOM, modificar su contenido, añadir eventos y crear nuevos elementos.
Cuando una aplicación es pequeña, este enfoque puede ser suficiente. Sin embargo, a medida que aumenta la complejidad aparecen problemas relacionados con:
- Organización del código.
- Actualización manual del DOM.
- Reutilización de componentes.
- Gestión del estado.
- Comunicación entre diferentes partes de la aplicación.
- Mantenimiento del código.
Para una aplicación compleja con muchos componentes, datos dinámicos y diferentes vistas, un framework como Vue puede facilitar considerablemente el desarrollo y mantenimiento.
3.2 Vue y TypeScript
TypeScript (TS) es un lenguaje de código abierto desarrollado por Microsoft que se basa en JavaScript (realmente es un superconjunto de este), añadiendo tipado estático y características propias de lenguajes orientados a objetos como clases, interfaces o módulos.
El código que generamos se transpila a JavaScript (el código fuente se convierte a código JavaScript), por lo que puede ejecutarse en cualquier navegador o entorno que soporte JS.
Vue está creado con TypeScript y por tanto también podemos desarrollar aplicaciones Vue con TypeScript, de esta manera ganamos la detección de errores en tiempo de desarrollo.
Preparación del entorno
4 Introducción
Para desarrollar aplicaciones con Vue 3 necesitaremos algunas herramientas que se ejecutan fuera del navegador. La principal es Node.js.
5 Node.js y npm
Node.js es un entorno de ejecución que permite ejecutar JavaScript fuera de un navegador. Tradicionalmente, JavaScript se ejecutaba principalmente dentro del navegador. Node.js permite utilizar JavaScript también para desarrollar herramientas que se ejecutan en el sistema operativo.
En un proyecto Vue, Node.js no se utiliza normalmente para ejecutar la aplicación que verá el usuario. Su función principal durante el desarrollo es proporcionar el entorno necesario para ejecutar las herramientas que permiten:
- Crear proyectos.
- Instalar dependencias.
- Ejecutar el servidor de desarrollo.
- Compilar la aplicación.
- Ejecutar herramientas de testing.
- Automatizar tareas.
La aplicación Vue terminará ejecutándose en el navegador del usuario, mientras que Node.js nos ayudará a construir y preparar esa aplicación.
La aplicación Vue terminará ejecutándose en el navegador del usuario, mientras que Node.js nos ayudará a construir y preparar esa aplicación.
Al instalar Node.js se incluye habitualmente npm (Node Package Manager). npm es un gestor de paquetes que permite instalar y administrar las dependencias de nuestros proyectos JavaScript.
Los proyectos JavaScript gestionados mediante npm utilizan normalmente un archivo denominado package.json que contiene información sobre el proyecto y, entre otras cosas, sus dependencias (con versión incluida) y los comandos disponibles.
Ejemplo de package.json
{
"name": "prueba-vue",
"version": "0.0.1",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
// ...
},
"dependencies": {
"vue": "^3.5.40"
},
"devDependencies": {
//...
},
"engines": {
"node": "^22.18.0 || >=24.12.0"
}
}
En el fichero se puede comprobar que para distintas dependencias aparece al lado una versión, que puede contener distintos caracteres. Existe una documentación sobre ello, pero en la siguiente tabla se resumen:
| Valor | Descripción |
|---|---|
| ~version | Aproximadamente equivalente a la versión indicada; solo acepta nuevas versiones patch. Las versiones patch añaden «correcciones de errores compatibles hacia atrás» e incrementan el tercer dígito. Por ejemplo ~1.2.3 incluye cualquier versión entre la 1.2.3 y la 1.3.0. |
| ^version | Compatible con la versión indicada; acepta nuevas versiones minor y patch. Las versiones minor añaden «nuevas funcionalidades compatibles hacia atrás», incrementan el dígito intermedio y reinician el último dígito a cero. Por ejemplo ^3.5.40 aceptaría hasta versiones menores a 4.0.0 |
| version | Debe coincidir exactamente con la versión indicada. |
| >version | Debe ser superior a la versión indicada. |
| >=version | Debe ser igual o superior a la versión indicada. |
| <version | Debe ser inferior a la versión indicada. |
| <=version | Debe ser igual o inferior a la versión indicada. |
| 1.2.x | Acepta 1.2.0, 1.2.1, etc., pero no 1.3.0. |
| * | Coincide con cualquier versión. |
| latest | Obtiene la última versión publicada. |
5.1 Instalación
En la página web de descarga de Node.js podemos ver cómo instalar Node para distintas plataformas, de distintas formas, y podemos elegir distintas versiones y con distintos gestores de paquetes.
Es importante destacar que Node cuenta con tres tipos de versiones:
- current: es la versión más nueva actualmente.
- LTS: versión long time support o soporte a largo plazo.
- EOL: son las versiones end of life, cuya vida a finalizado y debería ser necesario actualizar de versión.
Uno de los métodos de instalación de Node más habituales es mediante nvm o Node Version Manager, ya que permite tener distintas versiones instaladas en el mismo equipo.
En cambio, para este libro se ha decidido hacer uso de mise, que está explicado en el anexo adjunto.
Instala mise y realiza las siguientes tareas:
- Instala la última versión de Node.js
- Instala la versión LTS de Node.js
- Aprende a cambiar entre versiones
6 Vite
Para crear aplicaciones Vue modernas utilizaremos Vite. Vite es una herramienta de desarrollo frontend que proporciona, entre otras funciones:
- Servidor de desarrollo.
- Recarga rápida durante el desarrollo.
- Compilación para producción.
- Integración con Vue.
- Gestión de módulos y recursos.
Una de sus características más importantes es que proporciona un entorno de desarrollo muy rápido.
6.1 Servidor de desarrollo
Durante el desarrollo gracias a Vite podemos ejecutar un servidor local. Habitualmente accederemos al servidor a través de la dirección local http://localhost:5173/
El número de puerto puede variar dependiendo de la configuración y de si el puerto está disponible. Este servidor permite abrir nuestra aplicación en el navegador mientras estamos desarrollándola.
Cuando modificamos el código, Vite puede detectar los cambios y actualizar rápidamente la aplicación que estamos visualizando.
6.2 Desarrollo frente a producción
Debemos distinguir entre el entorno de desarrollo y el resultado final que desplegaremos.
- Durante el desarrollo: Generamos el código fuente, Vite hace de servidor de desarrollo y lo vemos a través de nuestro navegador.
- Para producción: Generamos el código fuente, usamos Vite para generar archivos optimizados (unifica ficheros CSS, JS, minimiza…), debemos subir el código generado a un servidor web (Apache, Neginx, …) y lo visualizamos a través del navegador.
La aplicación que finalmente recibe el usuario no necesita el servidor de desarrollo de Vite.
No debemos usar el servidor de Vite para producción.
7 Crear proyecto Vue
A la hora de crear un proyecto Vue es habitual hacer uso de una herramienta para crear el “esqueleto” de la aplicación, de esta manera nos generará los ficheros principales necesarios y la jerarquía de directorios habitual.
Podemos usar Vue con un proyecto ya existente, y de esta manera ir añadiendo pequeños módulos a la aplicación, pero se va a explicar cómo crear un proyecto desde cero:
Crear proyecto Vue
ruben@archy:~$ npm create vue@latest
Este comando nos hará distintas preguntas para así poder añadir distintas características extra a las opciones por defecto:
- Nombre del proyecto: nos creará un directorio con este nombre, y será el nombre de la aplicación.
- Añadir TypeScript: por defecto Vue se programa con JavaScript, por lo que si queremos añadir la opción de poder usar TypeScript, debemos seleccionar esta opción.
- Soporte JSX: añade soporte para “JavaScript XML”, un sistema creado por React.
- Router: el sistema de enrutado para crear Single Page Applications.
- Pinia: permite tener mejor control sobre estados. Es el sustituto de Vuex de versiones anteriores.
- Vitest: sistema de testing unitario.
- End-to-end testing: permite testear una aplicación de varias páginas que usa peticiones de red.
- Linter: para analizar el código fuente a medida que escribimos.
- Prettier: para formatear nuestro código.
- Características experimentales: podemos añadir nuevas características que no son estables.
- Añadir código de ejemplo.
De momento vamos a seleccionar NO menos a Linter y Prettier.
7.1 Instalar dependencias
Tal como se ha dicho previamente, gracias a Node.js vamos a poder desarrollar y mediante npm controlar las distintas dependencias de nuestro proyecto, ya que es un gestor de paquetes.
Tras crear el proyecto en el paso anterior, se nos ha generado distintos ficheros, entre los que se encuentra package.json que contiene las dependencias que necesita nuestro proyecto.
Todavía no hemos instalado las dependencias.
Para que podamos desarrollar en nuestro proyecto, y posteriormente ponerlo en producción, necesitamos instalar las dependencias que aparecen en el fichero. Para ello debemos entrar al directorio de nuestro proyecto e instalar las dependencias:
Instalar dependencias
ruben@archy:~$ cd vue-project
ruben@archy:~/vue-project/ $ npm install
npm leerá la información de package.json e instalará las dependencias que ahí aparecen y la versión correspondiente. Estas dependencias se instalan dentro del directorio node_modules en nuestro proyecto. Este directorio puede ocupar bastante espacio y contiene código que normalmente no debemos modificar directamente.
El directorio node_modules está excluido del proyecto GIT, y no debería ser incluido nunca.
7.2 Estructura del proyecto
Una vez creado el proyecto e instalado las dependencias encontraremos diferentes archivos y directorios. No todos los proyectos tendrán exactamente la misma estructura, ya que podemos modificarla según nuestras necesidades, pero en todos ellos nos encontraremos con lo siguiente:
index.html: Aunque Vue permite construir la interfaz mediante componentes, sigue existiendo un documento HTML inicial. La parte importante de este documento será similar al siguiente, que es donde se monta la aplicación:
Elemento de montaje
<div id="app"></div>
node_modules: directorio donde se guardan las dependencias instaladas. Este directorio puede ocupar bastante espacio y contiene código que normalmente no debemos modificar directamente.
package-lock.json: tras realizar la instalación de dependencias este fichero registra las versiones concretas de las dependencias instaladas y de sus dependencias internas. Su objetivo principal es conseguir que diferentes instalaciones del mismo proyecto utilicen versiones compatibles y reproducibles de los paquetes.
public: puede utilizarse para almacenar recursos estáticos que deban estar disponibles directamente. Estos recursos no tienen por qué pasar por el mismo proceso que los recursos gestionados dentro de src.
src: este directorio contiene el código fuente de nuestra aplicación, lo que lo hace el directorio más importante del proyecto. En él encontraremos los componentes Vue, los archivos JavaScript, las hojas de estilo y otros recursos utilizados por nuestra aplicación.
App.vue: suele actuar como componente principal de la aplicación. Más adelante veremos que App.vue no tiene por qué contener toda la aplicación. Su función será normalmente coordinar otros componentes.
assets: se utiliza habitualmente para recursos que forman parte del código de la aplicación como imágenes, hojas de estilo generales u otros recursos estáticos. A diferencia de [public]{configdir}, estos recursos pueden ser procesados por las herramientas de construcción.
components: se utiliza normalmente para almacenar componentes Vue reutilizables. No es obligatorio utilizar exactamente esta estructura, pero es una organización habitual. Podemos organizar también en subcarpetas si son componentes para formularios, productos, tablas, …
views: una aplicación web puede tener distintas “vistas” (login, vista de productos, configuración, …) que pueden contener distintos componentes.
main.js: es el punto de entrada habitual de una aplicación Vue. Es el archivo desde el que se crea la aplicación y se monta en el documento HTML. Primero importa el CSS, después createApp desde Vue, y por último el componente raíz App.vue. El último paso es crear la aplicación y la monta sobre el elemento HTML cuyo identificador es app.
Fichero main.js
import './assets/main.css' import { createApp } from 'vue' import App from './App.vue' createApp(App).mount('#app')
Este código realiza varias operaciones:
- Importa el código CSS global.
- importa createApp desde Vue.
7.3 Servidor de desarrollo
Una vez creado el proyecto podemos iniciar el servidor mediante:
Arrancar servidor de desarrollo
ruben@archy:~$ npm run dev
VITE v8.3.0 ready in 303 ms
⇒ Local: http://localhost:5173/
⇒ Network: use --host to expose
⇒ Vue DevTools: Open http://localhost:5173/__devtools__/ as a separate window
⇒ Vue DevTools: Press Alt(⌥)+Shift(⇧)+D in App to toggle the Vue DevTools
⇒ press h + enter to show help
Este comando ejecuta uno de los scripts definidos en package.json. En el proyecto que acabamos crear, debido a las opciones seleccionadas, tenemos las siguientes opciones:
Scripts en la configuración
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"lint": "run-s \"lint:*\"",
"lint:oxlint": "oxlint . --fix",
"lint:eslint": "eslint . --fix --cache",
"format": "prettier --write --experimental-cli src/"
},
Al ejecutar npm run dev podemos ver que el comando que ejecuta por debajo es vite, y viendo la salida del comando nos indica que que tenemos dos URLs a las que poder acceder:
- http://localhost:5173/: es la dirección localhost y el puerto por defecto donde vamos a poder ver nuestra aplicación.
- http://localhost:5173/devtools/: si abrimos esta URL en una nueva ventana podemos acceder a las herramientas de desarrollo.
Gracias a vite como servidor de desarrollo, mientras esté funcionando, podremos modificar nuestros archivos y comprobar los cambios desde el navegador.
Con el servidor arrancado, si modificamos los ficheros aparecen los cambios en el navegador.
Esto permite desarrollar de forma mucho más rápida que teniendo que detener y volver a iniciar manualmente todo el proceso después de cada modificación.
Es importante entender que el servidor lo estamos ejecutando “a mano”, no es un servicio de nuestro sistema operativo, por lo que si reiniciamos (o incluso cerramos sesión), tendremos que volver a arrancar el servicio.
Si reiniciamos nuestro equipo tendremos que volver a levantar el servidor.
Crea un proyecto Vue con código de ejemplo y realiza las siguientes tareas:
- Instala las dependencias.
- Levanta el servidor.
- Cambia el título de la plantilla HTML.
- Busca el fichero App.vue y analiza las distintas partes de las que consta.
- Modifica el fichero de App.vue y comprueba el cambio.
- Modifica el componente HelloWorld.vue y comprueba el cambio.
7.4 Vue DevTools
Además de las herramientas generales del navegador, Vue dispone de una herramienta específica denominada Vue DevTools, que permite inspeccionar aplicaciones Vue y proporciona información específica sobre sus componentes y estado reactivo. Entre otras posibilidades, permite:
- Examinar el árbol de componentes.
- Seleccionar componentes.
- Inspeccionar sus datos.
- Analizar props.
- Analizar eventos.
- Inspeccionar el estado.
- Facilitar la depuración.
La herramienta resulta especialmente útil cuando las aplicaciones comienzan a tener muchos componentes.
7.5 Desarrollo con Visual Studio Code
Al crear el proyecto el asistente automático ha creado un directorio .vscode que es el utilizado para añadir configuración propia para el IDE Visual Studio Code.
Este directorio no es obligatorio, y se podría llegar a borrar, pero nos añade información si usamos este editor. En el directorio nos encontramos con dos ficheros:
- extensions.json: configuración de extensiones recomendadas. Al abrir por primera vez el proyecto, si no tenemos las extensiones instaladas el editor nos preguntará si queremos instalarlas.
- settings.json: Son configuraciones propias del editor para los proyectos. Por ejemplo, mezcla varios ficheros de configuración en la jerarquía como si package.json fuese un directorio.
Podemos usar cualquier otro editor, pero la integración con VSCode viene por defecto.
Crea un proyecto Vue activando Linter y Prettier y sin código de ejemplo. Será la base que vamos a usar a partir de ahora.
Componentes Vue
8 Concepto de componente
Uno de los conceptos fundamentales de Vue es el componente, que se puede definir como una parte independiente y reutilizable de la interfaz de una aplicación. En lugar de construir toda la aplicación dentro de un único archivo, podemos dividirla en pequeñas piezas que tengan una responsabilidad concreta.
Una aplicación Vue puede contener decenas, cientos o incluso miles de componentes dependiendo de su tamaño: cabecera, menú, lista de productos, producto, carrito, formulario de creación, pie de página, …
8.1 Ventajas de utilizar componentes
La utilización de componentes proporciona varias ventajas, entre las que podemos destacar:
- Reutilización: Un mismo componente puede utilizarse varias veces. Por ejemplo, si tenemos un componente ProductCard.vue, podemos utilizarlo para representar todos los productos de una tienda. Por lo tanto, cada vez que se visualiza un producto, se llamará a ese componente.
- Mantenimiento: Si toda la lógica relacionada con una parte de la interfaz está concentrada en un componente, modificarla resulta más sencillo. Por ejemplo, si queremos cambiar el aspecto de todas las tarjetas de producto, sólo necesitamos modificar el ProductCard.vue y se reflejará en todo.
- Organización: Los componentes permiten dividir una aplicación compleja en unidades más pequeñas. Esto facilita que diferentes desarrolladores puedan trabajar en distintas partes de una aplicación.
- Encapsulación: Un componente puede contener su propia estructura HTML, lógica JavaScript y estilos CSS. De esta forma, podemos mantener agrupado el código relacionado con una determinada funcionalidad.
8.2 Responsabilidad al crear componentes
Una buena aplicación no debería convertir cada pequeño elemento HTML en un componente sin una razón clara, ni tampoco tener componentes de cientos de líneas. Es por eso que a la hora de hacer una aplicación debemos realizar un análisis para dividir la aplicación en unidades con sentido.
Cada componente representa una parte con una responsabilidad concreta que tiene que tener sentido.
9 Single File Components
Vue proporciona un formato de archivo denominado Single File Component (SFC). Los componentes SFC utilizan normalmente la extensión .vue y permiten mantener en un mismo archivo las diferentes partes que forman un componente.
Un componente sencillo puede tener la siguiente estructura:
Componente Vue
<template>
<h1>Hola Vue</h1>
</template>
<script setup>
const mensaje = 'Bienvenido a Vue 3'
</script>
<style>
h1 {
color: blue;
}
</style>
Este componente contiene tres partes:
- <template>: define la estructura visual del componente.
- <script setup>: contiene la lógica.
- <style>: contiene los estilos.
No es obligatorio utilizar siempre los tres bloques, por lo que la estructura dependerá de las necesidades del componente.
10 Estructura de un componente
Tal somo se ha visto, un componente consta de tres apartados diferenciados, y de esta manera poder tener diferenciado cada lógica/estructura.
10.1 <template>
En su interior utilizaremos HTML junto con la sintaxis de plantillas proporcionada por Vue. Aparte de poder utilizar cualquier etiqueta o elemento HTML, Vue nos proporciona mecanismos que permiten relacionar este HTML con datos y lógica JavaScript.
Scripts en la configuración
<template>
<h1>Hola {{nombre}}</h1>
<p>Texto de la página.</p>
</template>
En este ejemplo {{nombre}} es una variable que debe ser definida dentro del bloque <script setup>. Esta técnica se denomina interpolación y se estudiará más adelante.
En versiones anteriores de Vue dentro de <template> sólo permitía tener un único elemento raíz.
10.2 <script setup>
Dentro de este bloque se escribe la lógica del componente usando JavaScript. La palabra setup hace referencia a la Composition API de Vue, que aparece en Vue 3.0 en 2020.
Scripts en la configuración
<script setup>
const nombre = 'Bob'
function saludar() {
console.log(`Hola ${nombre}`)
}
</script>
<template>
<h1>Hola {{ nombre }}</h1>
<button @click="saludar">
Saludar
</button>
</template>
Las variables y funciones declaradas en <script setup> están disponibles automáticamente dentro del <template>. No es necesario hacer nada para registrar nombre y saludar, ya que Vue se encarga de hacerlos disponibles dentro de la plantilla.
10.3 <style>
Este bloque contiene los estilos CSS del componente, pero hay que entender que por defecto, los estilos definidos en un componente pueden tener un ámbito que no se limita necesariamente al componente.
10.3.1 Estilo global
En el siguiente ejemplo, las reglas CSS creadas dentro del componente no se van a limitar sólo al contenido del componte. Cualquier otro h1 de la aplicación podrá tener el color rojo, teniendo en cuenta las reglas de especificidad.
Estilo global
<template>
<h1>Hola mundo</h1>
</template>
<style>
h1 {
color:red;
}
</style>
10.3.2 Estilos encapsulados
Vue proporciona el atributo scoped para limitar los estilos a los elementos del componente. De esta manera, Vue transforma internamente los estilos para evitar que se apliquen de forma accidental a elementos equivalentes de otros componentes.
Estilo encapsulado
<template>
<h1>Hola mundo</h1>
</template>
<style scoped>
h1 {
color:red;
}
</style>
11 Registro y uso de componentes
Por defecto tenemos un componente “raíz” que es App.vue, y este puede tener componentes hijos que a su vez tienen otros. El componente que utiliza otros componentes se denomina componente padre, mientras que el que es utilizado por otro se denomina componente hijo.
Para utilizar un componente dentro de otro componente debemos importarlo y utilizarlo en el <template>. Imaginemos que tenemos el componente components/CabeceraH1.vue que queremos usarlo en App.vue:
CabeceraH1.vue
<template>
<header>
<h1>Mi aplicación</h1>
</header>
</template>
Importarlo en App.vue
<script setup>
import CabeceraH1 from './components/CabeceraH1.vue'
</script>
<template>
<CabeceraH1 />
</template>
Tenemos que entender qué esta sucediendo en el ejemplo anterior:
- Tenemos el componente CabeceraH1.vue dentro del directorio components/
- Dentro del componente App.vue queremos usarlo, por lo que:
- Dentro de <script setup> lo importamos
- En la plantilla <template> lo usamos como una etiqueta HTML nueva, teniendo en cuenta el nombre del fichero del componente.
Los componentes deben ser multi-palabra, para evitar confusiones con etiquetas HTML o componentes propios de Vue. No es obligatorio, pero de no ser así, el linter dará warnings.
11.1 Reutilización de componentes
Una de las principales ventajas de los componentes es que podemos utilizar el mismo componente varias veces. Imaginemos que tenemos un componente para visualizar productos de una tienda, podemos reutilizarlo varias veces:
Reutilizar ProductCard.vue
<template>
<ProductCard nombre="Teclado" />
<ProductCard nombre="Ratón" />
<ProductCard nombre="Monitor" />
</template>
Aunque todavía no hemos visto cómo pasar parámetros a los componentes, en el ejemplo anterior se usan los conocidos como props para proporcionar diferentes datos a cada instancia.
Esta separación entre el componente y los datos que recibe será fundamental para desarrollar interfaces reutilizables.
11.2 Crear componente
Vamos a crear un componente en nuestro proyecto vacío. Aunque podemos crear el componente directamente en el directorio src, tal como se ha dicho previamente vamos a organizarlos en la carpeta src/components:
El archivo src/components/CabeceraH1.vue tendrá:
Crear nuevo componente
<template>
<h1>Bienvenido a Vue 3</h1>
</template>
Ahora podemos utilizarlo en el componente raíz App.vue.
Usar nuevo componente
<script setup>
import CabeceraH1 from './components/CabeceraH1.vue';
</script>
<template>
<h1>Mi aplicación</h1>
<Saludo />
</template>
11.3 Componente con estructura, lógica y estilos
Vamos a crear un componente que utilice las tres partes principales de un Single File Component.
Script
<script setup>
const titulo = 'Clicka'
function saluda() {
console.log('Hola!')
}
</script>
Estilo
<style scoped>
.tarjeta {
padding: 1rem;
border: 1px solid #ccc;
}
button {
padding: 0.5rem 1rem;
}
</style>
Plantilla
<template>
<article class="tarjeta">
<h2>{{ titulo }}</h2>
<button @click="saluda">
Mostrar mensaje
</button>
</article>
</template>
Este ejemplo reúne varios conceptos:
- Variables JavaScript en <script setup>.
- Funciones JavaScript.
- HTML en <template>.
- Interpolación.
- Eventos.
- CSS en <style>.
- Estilos encapsulados mediante scoped.
Más adelante profundizaremos en cada apartado, pero de momento sirve como ejemplo completo.
Analiza una página web que suelas visitar y divide su interfaz en componentes.
Crea un componente, con estilo propio, para productos y úsalo 3 veces. Debe contener:
- Nombre del producto.
- Descripción.
- Precio.
- Un botón.
De momento contendrá la misma información, más adelante usaremos variables.
12 Plantillas y renderizado
Las plantillas (<template>) de Vue permiten definir la estructura visual de un componente utilizando HTML junto con una serie de características propias de Vue.
A primera vista, una plantilla Vue se parece mucho a un documento HTML convencional. La diferencia es que Vue permite incorporar expresiones JavaScript, directivas y enlaces de datos para que el contenido de la interfaz pueda cambiar de forma dinámica.
12.1 Interpolación de expresiones
La forma más sencilla de mostrar datos dinámicos en una plantilla es mediante la interpolación. La interpolación utiliza una pareja de llaves dobles {{ expresión }}. Por ejemplo:
Interpolación
<script setup>
const nombre = 'Bob'
const edad = 20
</script>
<template>
<h1>{{ nombre }}</h1>
<p>{{ edad }}</p>
</template>
Interpolación
<script setup>
const nombre = 'Ane'
const edad = 20
</script>
<template>
<p>{{ 10 + 5 }}</p>
<p>{{ nombre.toUpperCase() }}</p>
<p>{{ edad >= 18 ? 'Mayor' : 'Menor' }}</p>
</template>
12.1.1 ¿Qué podemos utilizar dentro de {{ }}?
Dentro de {{ }} podemos utilizar expresiones JavaScript que produzcan un valor:
Ejemplos
<p>{{ nombre }}</p>
<p>{{ edad + 1 }}</p>
<p>{{ nombre.toUpperCase() }}</p>
<p>{{ usuario.nombre }}</p>
<p>{{ productos.length }}</p>
Sin embargo, no debemos utilizar la interpolación para ejecutar bloques completos de JavaScript. Por ejemplo, no tendría sentido escribir:
Este ejemplo no debería hacerse
{{ if (edad >= 18) { ... } }}
if es una sentencia y no una expresión que produzca directamente un valor. Para este tipo de situaciones Vue proporciona directivas como v-if.
12.2 Enlace de atributos con v-bind
La interpolación permite introducir datos dentro del contenido de un elemento, pero no permite utilizar directamente {{ }} para establecer atributos HTML. Para enlazar un atributo HTML con un valor JavaScript utilizamos la directiva v-bind. Por ejemplo, para establecer dinámicamente los atributos src y alt hacemos lo siguiente:
Uso de v-bind
<script setup>
const imagen = '/imagenes/vue.png'
const descripcion = 'Logotipo de Vue.js'
</script>
<template>
<img v-bind:src="imagen" v-bind:alt="descripcion">
</template>
Dado que v-bind se utiliza con mucha frecuencia, Vue proporciona una sintaxis abreviada utilizando únicamente :. Las siguientes dos etiquetas son equivalentes:
Equivalencia
<img v-bind:src="imagen">
<img :src="imagen">
La segunda forma es la más habitual en aplicaciones Vue. También podemos utilizarla con otros atributos:
Otros ejemplos
<a :href="url">Visitar página</a>
<input :value="nombre">
<button :disabled="deshabilitado">
Guardar
</button>
12.2.1 Atributos dinámicos
Una de las ventajas de v-bind es que el atributo puede depender de los datos de la aplicación.
Atributo dinámico
<script setup>
const formularioBloqueado = true
</script>
<template>
<button :disabled="formularioBloqueado">
Enviar
</button>
</template>
Cuando formularioBloqueado cambie, Vue actualizará automáticamente el atributo correspondiente.
12.2.2 Enlace de clases CSS
Las clases CSS también pueden cambiar dinámicamente en Vue. Podemos hacer que en nuestro código JavaScript se pueda modificar la clase que está asignada a un elemento HTML:
Atributo dinámico
<script setup>
const claseMensaje = 'importante'
</script>
<template>
<p :class="claseMensaje">
Mensaje importante
</p>
</template>
<style>
.importante {
font-weight: bold;
}
</style>
Vue añadirá la clase indicada al elemento.
Crea un componente que visualice una alerta con un color de fondo y bordes redondeados con un color un poco más intenso. Puede ser de los siguientes tipos:
- Info: muestra mensajes de aviso en color azul.
- Sucess: para mostrar mensajes de confirmación en color verde.
- Warning: para mensajes de aviso con fondo naranja.
- Error: para mensajes de error en color rojo.
Para inspiraros en los alerts de Bootstrap.
También podemos utilizar un objeto para controlar varias clases:
Atributo dinámico
<template>
<p :class="{ activo: estaActivo, error: hayError }">
Estado del usuario
</p>
</template>
En este caso:
- activo se aplica cuando estaActivo es true.
- error se aplica cuando hayError es true.
Esta técnica resulta especialmente útil cuando la apariencia de un componente depende de su estado.
12.3 Renderizado condicional con v-if
Una aplicación frecuentemente necesita mostrar unos elementos u otros dependiendo de determinadas condiciones. Vue proporciona la directiva v-if para realizar un renderizado condicional.
Renderizado condicional
<script setup>
const estaConectado = true
</script>
<template>
<p v-if="estaConectado">
Usuario conectado
</p>
</template>
Cuando estaConectado sea true, Vue mostrará el elemento. En cambio, si es false el elemento no se renderizará. Por tanto, v-if no se limita a ocultar visualmente el elemento. Vue decide si el elemento debe formar parte del DOM renderizado.
12.3.1 Condiciones más complejas
La condición puede utilizar expresiones, e incluso expresiones que dependan de varios valores:
Condicional con expresión
<template>
<p v-if="edad >= 18">
Puedes acceder.
</p>
</template>
Expresión con varios valores
<template>
<p v-if="estaConectado && tienePermisos">
Panel de administración
</p>
</template>
12.3.2 Condicionales con v-else y v-else-if
Vue permite definir alternativas mediante v-else y v-else-if y funcionan igual en otros lenguajes de programación.
En el siguiente ejemplo sólo uno de los dos elementos será renderizado.
Condicional y alternativa
<template>
<p v-if="estaConectado">
Bienvenido.
</p>
<p v-else>
Debes iniciar sesión.
</p>
</template>
Cuando tenemos varias posibilidades podemos utilizar v-else-if.
Condicional con varias opciones
<template>
<p v-if="nota >= 9">
Sobresaliente
</p>
<p v-else-if="nota >= 7">
Notable
</p>
<p v-else-if="nota >= 5">
Aprobado
</p>
<p v-else>
Suspenso
</p>
</template>
Vue comprobará las condiciones en orden y renderizará la primera que sea verdadera.
Crea un componente que tenga una variable “edad” que visualice distintas partes en la plantilla dependiendo de si la edad:
- Es menor a 10
- Menor que 18
- Menor que 35
- Mayor de 65
12.4 Renderizado con v-show
Otra directiva relacionada con la visibilidad es v-show. A diferencia de v-if, v-show no elimina el elemento del DOM.
Elemento oculto
<p v-show="mostrarMensaje">
Este mensaje puede ocultarse.
</p>
Cuando la condición es falsa, Vue modifica el CSS del elemento para ocultarlo mediante display: none.
12.4.1 Diferencias entre v-if y v-show
En la siguiente tabla se puede ver una comparativa para saber cuándo usar v-if o v-show.
| v-if | v-show |
|---|---|
| El elemento puede aparecer o desaparecer con poca frecuencia. | El elemento se muestra y oculta frecuentemente. |
| El contenido es relativamente costoso de crear. | Queremos mantener el elemento creado. |
| Queremos que el elemento no exista en el DOM cuando no se utiliza. | El coste de cambiar su visibilidad es menor que crear y destruir continuamente el elemento. |
Por ejemplo, un panel que el usuario abre y cierra continuamente podría utilizar v-show.
12.5 Renderizado de listas con v-for
Una de las operaciones más habituales en una aplicación web es mostrar una lista de elementos. Vue proporciona la directiva v-for para ello, similar a cómo funciona un bucle for.
Renderizar una lista
<script setup>
const nombres = ['Alice', 'Bob', 'Carol', 'David']
</script>
<template>
<ul>
<li v-for="nombre in nombres">
{{ nombre }}
</li>
</ul>
</template>
Vue generará un elemento <li> para cada elemento del array, por lo que generará una lista HTML. También podemos utilizar of:
Alternativa
<li v-for="nombre of nombres">
{{ nombre }}
</li>
12.5.1 Obtener el índice
Podemos obtener también la posición del elemento dentro del array de elementos. El índice comienza en 0, como ocurre normalmente con los arrays de JavaScript.
Obtener el índice
<li v-for="(nombre, indice) in nombres">
{{ indice }} - {{ nombre }}
</li>
El resultado será
0 - Alice
1 - Bob
2 - Carol
3 - David
12.6 key en las listas
Cuando utilizamos v-for, normalmente debemos proporcionar una propiedad key que identifique de forma única cada elemento.
Añadir key
<script setup>
const usuarios = [
{ id: 1, nombre: 'Ane' },
{ id: 2, nombre: 'Mikel' },
{ id: 3, nombre: 'Jon' }
]
</script>
<template>
<ul>
<li v-for="usuario in usuarios" :key="usuario.id">
{{ usuario.nombre }}
</li>
</ul>
</template>
La propiedad key ayuda a Vue a identificar cada elemento de la lista cuando esta cambia. Esto permite que Vue pueda actualizar el DOM de una forma más eficiente y predecible.
No deberíamos utilizar como key un valor que pueda cambiar durante la vida del elemento.
Tampoco se debe usar el índice como key cuando la lista puede cambiar de orden, insertar elementos o eliminar elementos. Aunque técnicamente puede funcionar, puede provocar comportamientos inesperados cuando la lista cambia. Por ello, siempre que sea posible, utilizaremos un identificador único y estable.
Crea un componente que visualice una lista de objetos con datos de usuario en una tabla. Cada objeto de usuario debe tener al menos un “id” único y otros 4 campos personalizados. Si la lista está vacía que muestre un mensaje en lugar de la tabla.
Reactividad y eventos con Vue
13 Introducción
La reactividad es uno de los conceptos fundamentales de Vue. Es el mecanismo que permite que la interfaz de usuario se actualice automáticamente cuando cambian los datos de una aplicación.
En JavaScript tradicional, si modificamos una variable, el navegador no actualiza automáticamente los elementos HTML que dependen de ella. Vue añade el sistema relaciona los datos y la interfaz. Cuando un dato reactivo cambia, Vue detecta el cambio y actualiza las partes de la interfaz que dependen de ese dato.
14 El sistema de reactividad de Vue
Previamente hemos visto parte de cómo funciona la reactividad entre variables y las plantillas, pero en el siguiente ejemplo vamos a crear un botón reactivo:
Crear botón reactivo
<script setup>
import { ref } from 'vue'
const contador = ref(0)
function incrementar() {
contador.value++
}
</script>
<template>
<p>Contador: {{ contador }}</p>
<button @click="incrementar">
Incrementar
</button>
</template>
En este ejemplo contador es una variable reactiva. Cuando el valor se incrementa, Vue detecta el cambio y actualiza automáticamente el elemento p de la plantilla.
La reactividad permite que los datos y la interfaz permanezcan sincronizados.
14.1 ref()
Vue tiene la función ref() que es una de las herramientas más importantes de la Composition API. Permite crear una referencia reactiva de la siguiente manera:
Referencia reactiva
<script setup>
const variable = ref(valorInicial)
</script>
Ejemplo
<script setup>
import { ref } from 'vue'
const contador = ref(0)
</script>
En el ejemplo contador contiene un valor reactivo, lo que permite acceder o modificar su valor desde JavaScript utilizamos la propiedad .value, pero para la plantilla no lo tenemos que escribir.
Acceder al valor
<script setup>
console.log(contador.value)
contador.value++
contador.value = 10
</script>
Mostrar en la plantilla
<template>
<p>{{ contador }}</p>
</template>
14.2 reactive()
Otra herramienta de Vue para crear datos reactivos es reactive(), que está pensado principalmente para trabajar directamente con objetos y arrays, mientras que ref() es más para variables “simples”.
Uso de reactive()
<script setup>
import { reactive } from 'vue'
const usuario = reactive({
nombre: 'Alice',
edad: 20
})
usuario.nombre = 'Bob'
usuario.edad = 21
</script>
Uso en la plantilla
<template>
<p>{{ usuario.nombre }}</p>
<p>{{ usuario.edad }}</p>
</template>
Tal como se puede ver, se ha creado un objeto, que es reactive, se le han modificado los atributos y luego en la vista se accede a los atributos, sin tener que usar .value. Los atributos se pueden modificar dinámicamente desde el interfaz.
14.3 Valores computados con computed()
En muchas aplicaciones necesitamos obtener un valor a partir de otros datos, como puede ser la suma de datos, o la concatenación de textos. Para cuando el cálculo es complejo, o se utiliza en varios lugares, Vue proporciona computed().
Uso de computed()
<script setup>
import { ref, computed } from 'vue'
const nombre = ref('Alice')
const apellidos = ref('Doe')
const nombreCompleto = computed(() => {
return `${nombre.value} ${apellidos.value}`
})
</script>
Uso en la plantilla
<template>
<p>{{ nombreCompleto }}</p>
</template>
Una propiedad computada es un valor que Vue calcula a partir de otros valores reactivos. Otro posible ejemplo:
Uso de computed()
<script setup>
import { ref, computed } from 'vue'
const precio = ref(25)
const cantidad = ref(3)
const total = computed(() => {
return precio.value * cantidad.value
})
</script>
Uso en la plantilla
<template>
<p>Precio: {{ precio }} €</p>
<p>Cantidad: {{ cantidad }}</p>
<p>Total: {{ total }} €</p>
</template>
En este ejemplo, típico de un carrito de la compra, si cantidad cambia, el valor computado de total también cambiará.
Podemos conseguir un resultado parecido utilizando una función pero la diferencia fundamental es que una propiedad computada almacena en caché su resultado mientras sus dependencias no cambien.
Una función se ejecuta cada vez que se solicita, mientras que un valor computed se ejecuta una vez y se cachea. No se volverá a ejecutar mientras no cambien sus dependencias.
14.4 Reaccionando con watch()
watch() permite ejecutar código cuando cambia un dato reactivo concreto. Gracias a watch podemos obtener el valor antes del cambio y el valor cuando ha cambiado:
Uso de watch
<script setup>
import { ref, watch } from 'vue'
const contador = ref(0)
watch(contador, (nuevoValor, valorAnterior) => {
console.log('El contador ha cambiado')
console.log('Anterior:', valorAnterior)
console.log('Nuevo:', nuevoValor)
})
</script>
En cuanto contador se modifique, Vue ejecutará la función proporcionada a watch(), y tal como se puede ver, el callback recibe dos valores:
- nuevoValor: valor después del cambio.
- valorAnterior: valor anterior al cambio.
watch() es especialmente útil cuando el cambio de un dato debe provocar una acción secundaria. Por ejemplo:
- Realizar una petición a una API;
- Guardar información;
- Ejecutar una operación costosa;
- Reaccionar ante un cambio determinado;
- Sincronizar información con otro sistema.
14.5 watchEffect()
watchEffect() también permite ejecutar código reactivo, pero funciona de una forma diferente.
Uso de watchEffect
<script setup>
import { ref, watchEffect } from 'vue'
const nombre = ref('Alice')
watchEffect(() => {
console.log(`El nombre es ${nombre.value}`)
})
</script>
Vue ejecuta inmediatamente la función y detecta automáticamente qué valores reactivos se utilizan dentro de ella. Si nombre cambia, la función volverá a ejecutarse.
La diferencia principal es que en watch() debemos indicar explícitamente qué queremos observar, mientras que con watchEffect() Vue detecta automáticamente las dependencias.
Crea un componente por cada uno de los sistemas reactivos explicado. Los valores se deben cambiar mediante botones para comprobar cómo funciona el sistema reactivo y para que se modifique la vista.
15 Eventos
Los eventos permiten que una aplicación Vue responda a las acciones realizadas por el usuario. Vue proporciona mecanismos para asociar estos eventos con funciones de nuestro componente.
15.1 Manejo de eventos
En JavaScript podemos escuchar eventos utilizando addEventListener(), mientras que Vue proporciona una forma mucho más integrada con sus plantillas, por ejemplo con la directiva v-on
Eventos en JavaScript
const boton = document.querySelector('#boton')
boton.addEventListener('click', () => {
console.log('Botón pulsado')
})
Evento en Vue
<script setup>
function saludar() {
console.log('Botón pulsado')
}
</script>
<button v-on:click="saludar">
Saludar
</button>
Cuando el usuario pulsa el botón, Vue ejecuta la función saludar(). De esta forma, el evento y la lógica del componente quedan relacionados directamente en la plantilla.
La función que responde a un evento se llama event handler (manejador de eventos).
15.2 v-on y la sintaxis @
La sintaxis completa para escuchar un evento se ha visto previamente, pero v-on tiene una abreviatura mucho más utilizada usando @ y el evento que queremos usar. Por lo tanto, los siguientes códigos son equivalentes:
Eventos en Vue
<button v-on:click="saludar">
Saludar
</button>
Código equivalente
<button @click="saludar">
Saludar
</button>
En código Vue moderno es habitual utilizar @.
Otros ejemplos:
Ejemplos de eventos
<template>
<button @click="guardar">Guardar</button>
<input @input="actualizar">
<form @submit="enviarFormulario">
...
</form>
<div @mouseover="mostrarInformacion">
...
</div>
</template>
15.3 Parámetros en los eventos
Los event handlers de eventos pueden recibir parámetros. A continuación dos ejemplos con distintos números de parámetros:
Eventos con parámetro
<script setup>
function saludar(nombre) {
console.log(`Hola, ${nombre}`)
}
</script>
<template>
<button @click="saludar('Bob')">
Saludar
</button>
</template>
Evento con parámetros
<script setup>
function mostrarUsuario(id, nombre) {
console.log(id)
console.log(nombre)
}
</script>
<template>
<button @click="mostrarUsuario(10, 'Bob')">
Usuario
</button>
</template>
Esto resulta especialmente útil cuando trabajamos con listas. Por ejemplo:
Evento con parámetros de una lista
<template>
<button
v-for="usuario in usuarios"
:key="usuario.id"
@click="seleccionarUsuario(usuario.id)"
>
{{ usuario.nombre }}
</button>
</template>
15.4 El objeto Event
Los eventos del navegador proporcionan información adicional mediante el objeto Event.
Evento con parámetros de una lista
<script setup>
function mostrarEvento(evento) {
console.log(evento)
console.log(evento.target)
}
</script>
<template>
<button @click="mostrarEvento">
Pulsar
</button>
</template>
El objeto recibido contiene información sobre el evento que se ha producido. Por ejemplo, event.target representa el elemento que originó el evento.
event.target representa el elemento que originó el evento.
15.4.1 Evento de entrada
En un campo de texto podemos obtener el valor introducido mediante evento.target.value:
Evento con parámetros de una lista
<script setup>
function mostrarValor(evento) {
console.log(evento.target.value)
}
</script>
<template>
<input @input="mostrarValor">
</template>
Por ejemplo, si el usuario escribe dentro del campo de texto obtendremos ese valor.
15.4.2 Pasar argumentos y recibir el evento
Cuando necesitamos pasar nuestros propios argumentos y también queremos acceder al objeto Event, podemos utilizar $event.
Evento con parámetros de una lista
<script setup>
function mostrarUsuario(id, evento) {
console.log(id)
console.log(evento)
}
</script>
<template>
<button @click="mostrarUsuario(10, $event)">
Usuario
</button>
</template>
$event representa el evento generado por el navegador. Este mecanismo resulta especialmente útil cuando necesitamos combinar información propia de nuestra aplicación con información proporcionada por el navegador.
Crea una pequeña lista de objetos con datos de usuarios. Con esta lista:
- Muestra todos los usuarios en una tabla.
- La última columna de la tabla que tenga 3 botones para:
- Saludar usuario: Haga un alert recibiendo el usuario como parámetro.
- Ver evento: muestra qué ha generado el evento.
- Función que reciba el ID y el evento.
- Crea también un <input> con evento @input y muestre lo que se va escribiendo.
15.5 Modificadores de eventos
Vue proporciona modificadores de eventos para realizar operaciones habituales sin tener que escribirlas manualmente dentro de las funciones. Los modificadores se añaden después del nombre del evento utilizando un punto.
15.5.1 .prevent
Por defecto, cuando se envía un formulario, el navegador puede intentar recargar la página. Podemos evitar este comportamiento con .prevent:
Modificador .prevent
<script setup>
function enviarFormulario() {
console.log('Formulario enviado')
}
</script>
<template>
<form @submit.prevent="enviarFormulario">
<input type="text">
<button type="submit">
Enviar
</button>
</form>
</template>
La función solamente se encarga de procesar los datos. El modificador .prevent hace que Vue ejecute internamente la función JavaScript .preventDefault(). Por lo tanto, este modificador simplifica la operación.
15.5.2 .stop
Detiene la propagación del evento, y es equivalente conceptualmente a la función JavaScript evento.stopPropagation():
Modificador .stop
<template>
<button @click.stop="accion">
Pulsar
</button>
</template>
15.5.3 .once
Hace que el manejador se ejecute una sola vez. Después de la primera ejecución, el evento deja de ejecutar ese manejador.
Modificador .once
<template>
<button @click.once="mostrarMensaje">
Mostrar
</button>
</template>
15.5.4 .self
Hace que el evento se procese únicamente cuando se produce directamente sobre el propio elemento. Es especialmente útil en determinados componentes como ventanas modales:
Modificador .self
<template>
<div @click.self="cerrar">
...
</div>
</template>
15.5.5 .capture
Permite utilizar la fase de captura del evento. Los eventos del navegador pueden recorrer el DOM durante una fase de captura y otra de propagación. Este modificador permite controlar la fase utilizada por el listener.
Modificador .capture
<template>
<div @click.capture="procesar">
...
</div>
</template>
15.6 Eventos de teclado
Podemos reaccionar a teclas concretas, lo que permite implementar interacciones de teclado sin tener que comprobar manualmente las propiedades del objeto KeyboardEvent.
- keydown: al pulsar una tecla.
- keyup: al dejar de pulsar una tecla.
Un ejemplo en el que obtenemos la tecla pulsada, donde hemos puesto una condición:
Evento de teclado
<script setup>
function teclaPulsada(evento) {
if (evento.key === 'Enter') {
console.log('Se ha pulsado Enter')
} else {
console.log(evento.key)
}
}
</script>
<template>
<input @keydown="teclaPulsada">
</template>
Para situaciones sencillas, se puede tener en cuenta si se ha pulsado una tecla concreta. A continuación varios ejemplos para distintas teclas:
Modificador de teclado
<template>
<input @keyup.enter="enviar">
<input @keyup.esc="cancelar">
<input @keyup.tab="siguiente">
<input @keyup.delete="borrar">
<input @keyup.space="accionSpace">
<button @click.ctrl="accionCtrl">Ctrl + clic</button>
<button @click.shift="accionShift">Shift + clic</button>
<button @click.alt="accionAlt">Alt + clic</button>
<button @click.meta="accionMeta">Meta + clic</button>
</template>
Crea un <input> y si se introduce el código Konami salte un alert.
16 Formularios en Vue
Los formularios son una parte fundamental de las aplicaciones web. En Vue podemos utilizar los elementos HTML habituales:
- <input>
- <textarea>
- <select>
- <option>
- <button>
- <form>
- <input type="checkbox">
- <input type="radio">
Por ejemplo:
Modificador de teclado
<script setup>
function enviar() {
console.log('Formulario enviado')
}
</script>
<template>
<form @submit.prevent="enviar">
<label>
Nombre:
<input type="text">
</label>
<button type="submit">
Enviar
</button>
</form>
</template>
Sin embargo, todavía no estamos almacenando el valor introducido por el usuario. Para establecer una relación entre un control de formulario y una variable reactiva podemos utilizar v-model.
16.1 v-model
v-model permite crear un enlace bidireccional entre un elemento de formulario y un dato reactivo. Por ejemplo:
Modificador de teclado
<script setup>
import { ref } from 'vue'
const nombre = ref('')
</script>
<template>
<input v-model="nombre">
<p>Hola, {{ nombre }}</p>
</template>
Cuando el usuario escribe en el campo, nombre la variable se actualizará, y a su vez se visualizará también en la plantilla.
16.1.1 v-model y los eventos
Aunque v-model parece un mecanismo especial, conceptualmente combina distintas operaciones. Puede entenderse de forma simplificada como una combinación entre el valor del control y un evento que actualiza ese valor.
Es importante entender que v-model no sustituye al sistema de eventos de JavaScript, sino que proporciona una forma cómoda de utilizarlo con formularios.
16.1.2 v-model con distintos controles
v-model puede utilizarse con diferentes tipos de controles. A continuación algunos ejemplos para distintos tipos como textarea, select, …
V-model y controles
<template>
<textarea v-model="descripcion"></textarea>
<select v-model="pais">
<option value="es">España</option>
<option value="fr">Francia</option>
<option value="pt">Portugal</option>
</select>
<input type="checkbox" v-model="aceptaCondiciones">
<input type="radio" value="basico" v-model="nivel">
<input type="radio" value="avanzado" v-model="nivel">
</template>
Los checkbox devuelven un boolean, y la variable nivel contendrá el value correspondiente al radio seleccionado.
16.2 Modificadores de v-model
Vue proporciona varios modificadores para v-model.
16.2.1 .trim
Elimina espacios en los extremos del texto. Esto nos permite realizar esta operación en el frontend antes de realizar el envío al backend
V-model y trim
<template>
<input v-model.trim="nombre">
</template>
16.2.2 .number
Intenta convertir el valor introducido a un número. Esto resulta útil porque los valores obtenidos de un <input> de tipo texto son normalmente cadenas.
V-model y number
<template>
<input v-model.number="edad">
</template>
16.2.3 .lazy
.lazy hace que la sincronización se realice en determinados momentos del ciclo del control, en lugar de hacerlo en cada evento de entrada. Puede resultar útil cuando no necesitamos reaccionar a cada carácter que escribe el usuario.
V-model y lazy
<template>
<input v-model.lazy="nombre">
</template>
16.2.4 Combinación de modificadores
Los modificadores pueden combinarse:
Combinar modificadores
<template>
<input v-model.trim.number="valor">
</template>
16.3 Validación de formularios
Los formularios deben validar la información introducida antes de enviarla o procesarla. Podemos utilizar las capacidades de validación de HTML y también realizar validaciones mediante JavaScript.
Validación
<script setup>
import { ref } from 'vue'
const nombre = ref('')
const errorNombre = ref(false)
function enviar() {
errorNombre.value = nombre.value === ''
if (errorNombre.value) {
return
}
console.log('Formulario correcto')
}
</script>
<template>
<form @submit.prevent="enviar">
<label>
Nombre:
<input v-model.trim="nombre">
</label>
<span v-if="errorNombre">
El nombre es obligatorio.
</span>
<button type="submit">
Enviar
</button>
</form>
</template>
En aplicaciones reales podemos realizar validaciones mucho más completas, para por ejemplo:
- campos obligatorios;
- longitud mínima y máxima;
- formato de correo electrónico;
- valores numéricos;
- rangos;
- coincidencia entre campos;
- validaciones dependientes de otros campos;
- validaciones contra una API.
16.4 Separar los errores de la interfaz
Es conveniente almacenar el estado de validación en datos reactivos.
Validación
<script setup>
const errores = ref({
nombre: '',
email: ''
})
</script>
<template>
<input v-model="nombre">
<span v-if="errores.nombre">
{{ errores.nombre }}
</span>
</template>
La plantilla puede mostrar los mensajes de error correspondientes, lo que permite que los mensajes de error formen parte del estado de la aplicación y que Vue actualice automáticamente la interfaz.
Crea un formulario y utiliza los distintos modificadores vistos y un sistema de validación que muestre (u oculte) los avisos.
Comunicación entre componentes
17 Comunicación entre componentes
Tal como se ha dicho previamente, una aplicación Vue es habitual dividir la interfaz en múltiples componentes, en la que cada uno tiene su propia responsabilidad y puede necesitar comunicarse con otros componentes. Por ejemplo:
- Un componente padre puede proporcionar información a un hijo.
- Un componente hijo puede informar a su padre de que el usuario ha realizado una acción.
- Varios componentes pueden necesitar compartir información.
- Un componente puede permitir que su contenido sea proporcionado desde el exterior.
Vue sigue, como regla general, un flujo de datos unidireccional. Los datos normalmente fluyen desde el componente padre hacia sus componentes hijos. Por ejemplo, un componente App puede proporcionar un nombre a un componente Usuario:
Vue proporciona diferentes mecanismos para resolver todas las situaciones posibles:
- Props para enviar datos de padre a hijo.
- Eventos personalizados para enviar información de hijo a padre.
- v-model para crear una comunicación bidireccional entre componentes.
- Slots para proporcionar contenido a un componente.
- Estado compartido para situaciones en las que varios componentes necesitan acceder a los mismos datos. Se verá más adelante.
18 Comunicación padre a hijo: Props
Las props (properties) permiten que un componente padre proporcione datos a un componente hijo. Supongamos los siguientes componentes:
Componente Usuario
<script setup>
defineProps({
nombre: String
})
</script>
<template>
<p>Usuario: {{ nombre }}</p>
</template>
Componente padre
<script setup>
import Usuario from './components/Usuario.vue'
</script>
<template>
<Usuario nombre="Bob" />
</template>
El componente Usuario recibe nombre como una prop desde el componente padre.
18.1 Props dinámicas
Las props no tienen por qué contener valores escritos directamente, podemos enlazarlas con datos mediante dos puntos :. Por ejemplo:
Componente padre
<script setup>
import Usuario from './components/Usuario.vue'
import { ref } from 'vue'
const nombreUsuario = ref('Alice')
</script>
<template>
<Usuario :nombre="nombreUsuario" />
<Producto :precio="precio * 1.21" />
</template>
En este caso, el valor de la prop depende de nombreUsuario. Si el valor reactivo cambia, el componente hijo recibirá el nuevo valor. En el segundo componente se ha enviado una expresión.
18.2 Pasar objetos mediante props
Las props también pueden contener objetos completos. Por ejemplo, en el componente padre se ha creado un objeto que después se pasa a un componente hijo:
Componente padre
<script setup>
const usuario = {
id: 1,
nombre: 'Alice',
edad: 20
}
</script>
<template>
<Usuario :usuario="usuario" />
</template>
Componente usuario
<script setup>
defineProps({
usuario: Object
})
</script>
<template>
<h2>{{ usuario.nombre }}</h2>
<p>Edad: {{ usuario.edad }}</p>
</template>
18.3 Declaración de props con defineProps()
En <script setup> utilizamos defineProps() para declarar las propiedades que puede recibir un componente. Vue permite crear una variable para declarar los props y de esta manera acceder en la plantilla a través de esta variable, pero también a través de los nombres de las props.
Uso de variable props
<script setup>
const props = defineProps({
nombre: String,
edad: Number
})
</script>
<template>
<h2>{{ props.nombre }}</h2>
<p>{{ props.edad }} años</p>
</template>
Uso de variables propias
<script setup>
const props = defineProps({
nombre: String,
edad: Number
})
</script>
<template>
<h2>{{ nombre }}</h2>
<p>{{ edad }} años</p>
</template>
Cualquiera de las dos opciones es posible.
Crea un componente que sea un contador:
- Tiene un botón para incrementar un número.
- Llama a un componente hijo y le pasa el número.
- El componente hijo muestra ese valor.
18.4 Validación y valores por defecto de las props
Podemos especificar qué tipo de dato esperamos recibir, de esta manera podemos ayudar a documentar el componente y permite que Vue pueda detectar determinados usos incorrectos. También se puede especificar si una prop es obligatoria:
Especificar tipo y obligatoriedad
<script setup>
const props = defineProps({
nombre: {
type: String,
required: true
},
edad: Number,
activo: Boolean
})
</script>
En este caso, el componente espera que se proporcione nombre y que sea de tipo String.
Todas las props son opcionales salvo que se indique required: true.
También podemos establecer un valor por defecto:
Especificar valor por defecto
<script setup>
const props = defineProps({
nombre: {
type: String,
default: 'Usuario'
}
})
</script>
Si el padre no proporciona nombre, el componente utilizará el valor por defecto "Usuario"
18.5 Las props son de solo lectura
Una regla fundamental de Vue es que un componente hijo no debe modificar directamente una prop recibida. Las props pertenecen conceptualmente al componente padre.
El siguiente código no se debería hacer
Especificar valor por defecto
<script setup>
const props = defineProps({
contador: Number
})
props.contador++
</script>
El hijo puede solicitar un cambio mediante un evento y será el padre quien modifique el dato. Esto ayuda a evitar cambios difíciles de rastrear en aplicaciones grandes.
Un componente hijo no debe modificar directamente una prop recibida.
19 Comunicación hijo a padre: eventos personalizados
Los eventos personalizados permiten que un componente hijo comunique al padre que algo ha ocurrido. Por ejemplo, podemos crear un componente Contador:
Componente padre
<script setup>
function incrementar() {
console.log('El hijo emite')
}
</script>
<template>
<Contador
@inc="incrementar"
/>
</template>
Componente hijo
<script setup>
const emit = defineEmits(['inc'])
function incrementar() {
emit('inc')
}
</script>
<template>
<button @click="incrementar">
Incrementar
</button>
</template>
El componente padre puede escuchar el evento que emite el componente hijo con emit().
19.1 emit
En <script setup>, los eventos personalizados se declaran mediante defineEmits() y emitirlos mediante emit():
Declarar eventos
<script setup>
const emit = defineEmits([
'guardar',
'cancelar'
])
</script>
Emitir evento
<script setup>
// ...
emit('guardar')
// ...
emit('cancelar')
</script>
El padre puede escuchar esos eventos indicando las funciones que se van a ejecutar al llamar al hijo:
Recibir evento
<script setup>
function guardarDatos() {
//...
}
function ejecutarCancelar() {
//...
}
</script>
<template>
<MiFormulario @guardar="guardarDatos" @cancelar="ejecutarCancelar" />
</template>
De esta forma, el hijo no necesita conocer qué hará el padre con el evento. Solo comunica “ha ocurrido esta acción” y el padre decide cómo responder. Esto también sirve para reaprovechar código.
Imagina un componente que dibuja botones “editar” y “borrar” que emite esos eventos. Se puede utilizar para distintos apartados de la aplicación: usuarios, productos, … Es el componente padre quien se encarga de llevar a cabo esas acciones.
Crea un componente padre que:
- Tiene una variable dinámica contador.
- Recibe los eventos incrementar y decrementar que llame a funciones para realizar dicha operación sobre contador y lo muestre en pantalla.
Un componente hijo que tenga:
- Dos botones: uno que emita el evento incrementar y otro decrementar.
19.2 Enviar datos mediante eventos
Un evento también puede transportar información. Podemos generar desde un componente padre una lista de usuarios (cada uno es un componente hijo), y al seleccionar uno se recibe en el padre el identificador.
Emitir evento en el hijo
<script setup>
const emit = defineEmits(['seleccionar'])
function seleccionarProducto(id) {
emit('seleccionar', id)
}
</script>
<template>
<!-- Listado e información -->
<button
@click="seleccionarProducto(10)">
Seleccionar producto 10
</button>
</template>
Recibir datos en el padre
<script setup>
function seleccionarUsuario(id) {
console.log(id)
}
</script>
<template>
<Usuario
v-for="usuario in usuarios"
:key="usuario.id"
:usuario="usuario"
@seleccionar="seleccionarUsuario"
/>
</template>
Cada componente puede informar al padre de qué usuario ha seleccionado el usuario de la aplicación.
20 v-model entre componentes
Anteriormente hemos visto cómo v-model se puede usar en formularios, pero no está limitado a elementos HTML como <input>. También podemos utilizarlo entre componentes, para establecer una relación entre un dato del componente padre y un componente hijo.
Antes de Vue 3.4 existía otra manera de funcionar, haciendo uso de eventos update: que todavía se puede leer en la documentación oficial, pero a continuación se va a explicar el método recomendado
En versiones anteriores a Vue 3.4 v-model se usaba con otra sintaxis a la aquí explicada.
En el componente padre tenemos que hacer que la variable use ref() como hemos visto previamente:
Componente padre
<script setup>
import { ref } from 'vue'
const contador = ref(0);
</script>
<template>
<h1>Valor de {{contador}}</h1>
<componenteVmodel v-model="contador" />
</template>
En el componente hijo se usa la macro definemodel() para declarar una variable de doble dirección.
Componente hijo
<script setup>
const contador = defineModel()
function update() {
contador.value++
}
</script>
<template>
<div>Parent bound v-model is: {{ contador }}</div>
<button @click="update">Increment</button>
</template>
Crea un componente padre que tenga una variable contador, la visualice y se la pase a un componente hijo como v-model. El hijo tiene un botón que modifica la variable.
En caso de querer pasar más de un parámetro debemos indicarlo mediante un argumento a v-model:
Componente padre
<script setup>
const contador = ref(0);
const contador2 = ref(50);
</script>
<template>
<componenteVmodel
v-model:contador="contador"
v-model:contador2="contador2"
/>
</template>
Componente hijo
<script setup>
const contador = defineModel("contador")
const contador2 = defineModel("contador2")
// ... funciones incrementar/decrementar
</script>
<template>
<!-- botones para inc/dec -->
</template>
También podemos especificar en el componente hijo qué tipo de variable esperamos, el valor por defecto en caso de que no se nos pase y si es obligatoria.
Componente hijo
<script setup>
const contador = defineModel("contador",{
type: Number,
default: 0,
requerid: true
})
</script>
21 Componentes dinámicos
En ocasiones queremos mostrar diferentes componentes dependiendo del estado de nuestra aplicación, y para ello tenemos el elemento especial <component>
Ejemplo
<script setup>
import Inicio from './components/Inicio.vue'
import Perfil from './components/Perfil.vue'
const componenteActual = ref(Inicio)
</script>
<template>
<component :is="componenteActual" />
</template>
En este ejemplo el componente que se visualiza es “Inicio”, y se puede cambiar con componenteActual.value = Perfil. También podemos utilizar directamente una variable con el componente seleccionado:
Ejemplo
<script setup>
import Inicio from './components/Inicio.vue'
import Perfil from './components/Perfil.vue'
const componentes = {
inicio: Inicio,
perfil: Perfil
}
const vistaActual = ref('inicio')
</script>
<template>
<component :is="componentes[vistaActual]" />
</template>
Los componentes dinámicos resultan útiles para interfaces con pestañas, asistentes, paneles o diferentes vistas dentro de una misma zona.
22 Slots
Los slots permiten que el componente padre introduzca contenido dentro del componente hijo.
Supongamos un componente Tarjeta.vue y cómo le llama el padre:
Componente padre
<template>
<Tarjeta>
<h2>Producto</h2>
<p>Descripción.</p>
</Tarjeta>
</template>
Componente Tarjeta.vue
<template>
<article class="tarjeta">
<slot></slot>
</article>
</template>
El contenido situado entre <Tarjeta> y </Tarjeta> en el componente padre ocupará el lugar del <slot> en el componente hijo.
Los slots permiten crear componentes visuales reutilizables sin obligar al componente a conocer exactamente qué contenido tendrá.
22.1 Slots con contenido predeterminado
Un slot puede tener contenido predeterminado. Imaginemos un componente que dibuja un botón, y que por defecto tiene un nombre:
Componente
<template>
<button>
<slot>Botón</slot>
</button>
</template>
Valor default
<template>
<MiBoton />
</template>
Nuevo texto
<template>
<MiBoton>
Guardar
</MiBoton>
</template>
Si llamamos al componente sin datos, aparecerá el valor por defecto, mientras que si le ponemos un texto como en el ejemplo anterior, aparecerá “Guardar”.
Los valores predeterminados son útiles para crear componentes que funcionen correctamente incluso cuando no se proporciona contenido personalizado.
22.2 Slots con nombre
Un componente puede tener más de un slot, por lo que tenemos que poder distinguirlos. Para ello tenemos que utilizar slots con nombre.
Componente
<Tarjeta>
<template #cabecera>
<h2>Producto</h2>
</template>
<p>Descripción del producto.</p>
<template #pie>
<button>Comprar</button>
</template>
</Tarjeta>
Componente Tarjeta
<template>
<article class="tarjeta">
<header>
<slot name="cabecera"></slot>
</header>
<main>
<slot></slot>
</main>
<footer>
<slot name="pie"></slot>
</footer>
</article>
</template>
En este ejemplo se puede ver cómo el padre puede proporcionar cada contenido utilizando #. El slot sin nombre se denomina slot por defecto.
Los slots con nombre permiten crear componentes con una estructura más compleja.
22.3 Slots con ámbito
Los slots con ámbito permiten que el componente hijo proporcione datos al contenido que ha introducido el padre.
En este ejemplo el hijo proporciona el objeto usuario:
Componente
<template>
<slot :usuario="usuario"></slot>
</template>
Y el componente padre puede recibir ese dato y decidir cómo representarlo:
Componente
<MiComponente>
<template #default="{ usuario }">
<p>{{ usuario.nombre }}</p>
</template>
</MiComponente>
Este mecanismo puede resultar inicialmente más complejo, pero es muy útil para crear componentes reutilizables que controlan una estructura pero permiten al padre decidir cómo mostrar determinados datos. Un ejemplo habitual sería un componente de lista:
Componente
<template>
<ul>
<li v-for="usuario in usuarios" :key="usuario.id">
<slot :usuario="usuario">
{{ usuario.nombre }}
</slot>
</li>
</ul>
</template>
El componente controla la lista, mientras que el componente padre decide cómo representar cada usuario.
Composition API
23 Introducción
La Composition API es una forma de organizar la lógica de los componentes de Vue. Permite agrupar el código según su responsabilidad y facilita la reutilización de funcionalidades entre diferentes componentes.
En las unidades anteriores hemos utilizado elementos como ref(), reactive(), computed() y defineProps(). Todos ellos forman parte de las herramientas que utilizamos habitualmente al trabajar con la Composition API.
Un ejemplo visto previamente de un componente con un contador:
Componente Usuario
<script setup>
import { ref } from 'vue'
const contador = ref(0)
function incrementar() {
contador.value++
}
</script>
<template>
<p>Contador: {{ contador }}</p>
<button @click="incrementar">
Incrementar
</button>
</template>
En este ejemplo:
- ref() crea una referencia reactiva.
- incrementar() modifica el estado.
- La plantilla muestra el valor del contador.
- Vue actualiza automáticamente la interfaz cuando cambia el valor.
En esta unidad aprenderemos a organizar esa lógica, gestionar el ciclo de vida de los componentes y extraer funcionalidades reutilizables mediante los llamados composables.
23.1 Composition API frente a Options API
Vue dispone de dos estilos principales para organizar la lógica de los componentes:
- Options API: la lógica se organiza mediante propiedades de un objeto, como data, methods, computed y watch.
- Composition API: permite organizar el comportamiento del componente mediante funciones y variables de JavaScript, en lugar de distribuirlo necesariamente entre diferentes opciones de configuración.
A continuación el mismo componente creado con ambas APIs:
Options API
<script>
export default {
data() {
// estado
return {
contador: 0
}
},
methods: {
//funciones
incrementar() {
this.contador++
}
}
}
</script>
<template>
<p>Contador: {{ contador }}</p>
<button @click="incrementar">
Incrementar
</button>
</template>
Composition API
<script setup>
import { ref } from 'vue'
const contador = ref(0)
function incrementar() {
contador.value++
}
</script>
<template>
<p>Contador: {{ contador }}</p>
<button @click="incrementar">
Incrementar
</button>
</template>
En la siguiente tabla vemos una comparativa:
| Característica | Options API | Composition API |
|---|---|---|
| Organización | Opciones como data y methods | Variables y funciones |
| Estado reactivo | Habitualmente data() | ref() y reactive() |
| Acceso al estado | Mediante this en los métodos | Mediante variables y referencias |
| Reutilización de lógica | Mixins y otras técnicas | Composables |
| Componentes complejos | Puede distribuir la lógica entre opciones | Permite agrupar la lógica por funcionalidad |
| Uso con <script setup> | No es el patrón habitual | Es el patrón habitual |
Ambas APIs siguen siendo válidas en Vue 3. La Composition API no elimina la Options API. Hemos estado utilizando Composition API con <script setup>, ya que permite trabajar con una estructura sencilla y facilita la reutilización de código.
24 Ciclo de vida de los componentes
Un componente Vue pasa por diferentes fases durante su existencia. Por ejemplo, cuando una aplicación muestra un componente, Vue debe crearlo, preparar su estado y representar su plantilla.
Después, si cambian sus datos reactivos, la interfaz puede actualizarse. Finalmente, cuando el componente deja de utilizarse, Vue lo desmonta.
Este proceso se conoce como ciclo de vida del componente. Vue proporciona funciones especiales, conocidas como hooks del ciclo de vida, para ejecutar código en momentos concretos.
Entre las más importantes están:
- onMounted(): cuando el componente se ha montado en el DOM.
- onUpdated(): después de una actualización del DOM provocada por cambios reactivos.
- onUnmounted(): después de desmontar el componente.
Estos hooks son especialmente útiles cuando necesitamos interactuar con elementos del DOM, iniciar operaciones o liberar recursos.
24.1 onMounted()
onMounted() permite ejecutar una función cuando el componente se ha montado en el DOM.
Al montar el DOM
<script setup>
import { onMounted } from 'vue'
onMounted(() => {
console.log('El componente se ha montado')
})
</script>
<template>
<h1>Mi aplicación</h1>
</template>
La función registrada se ejecutará cuando Vue haya completado el montaje del componente.
Este hook resulta útil para tareas como:
- Inicializar una biblioteca que necesita elementos del DOM.
- Consultar el tamaño de un elemento.
- Iniciar determinadas operaciones asíncronas.
- Configurar recursos que dependen de que el componente esté montado.
24.2 onUpdated()
onUpdated() permite ejecutar código después de que Vue haya actualizado el DOM de un componente como consecuencia de cambios reactivos.
Al montar el DOM
<script setup>
import { ref, onUpdated } from 'vue'
const contador = ref(0)
onUpdated(() => {
console.log('El DOM del componente se ha actualizado')
})
</script>
<template>
<p>Contador: {{ contador }}</p>
<button @click="contador++">
Incrementar
</button>
</template>
Cada vez que el cambio reactivo provoca una actualización del DOM, Vue ejecutará el hook, pero debemos tener cuidado con su uso.
No es recomendable utilizar onUpdated() para modificar indiscriminadamente el estado reactivo que ha provocado la actualización, porque podríamos generar un ciclo de actualizaciones.
Por ejemplo, si dentro del hook incrementamos el contador cada vez que se actualiza la plantilla, podemos provocar actualizaciones repetidas:
Debemos evitar este tipo de patrones.
<script setup>
// ...
onUpdated(() => {
contador.value++
})
</script>
En la mayoría de las situaciones, si necesitamos calcular un valor a partir del estado, será preferible utilizar computed(). Si necesitamos reaccionar a un cambio para realizar una operación secundaria, puede ser más apropiado utilizar watch().
onUpdated() debe reservarse para los casos en los que realmente necesitamos actuar después de una actualización del DOM.
24.3 onUnmounted()
onUnmounted() se ejecuta cuando Vue ha desmontado el componente. Resulta especialmente importante para liberar recursos que el componente haya creado.
Liberar recursos en onUnmounted()
<script setup>
import { onMounted, onUnmounted } from 'vue'
let intervalo
onMounted(() => {
intervalo = setInterval(() => {
console.log('Ejecutando tarea periódica')
}, 1000)
})
onUnmounted(() => {
clearInterval(intervalo)
})
</script>
<template>
<p>Componente con una tarea periódica</p>
</template>
Cuando el componente se monta, se crea un intervalo. Cuando se desmonta, se elimina mediante clearInterval().
Si no limpiamos determinados recursos, estos pueden continuar funcionando incluso cuando el componente ya no está visible o ha dejado de utilizarse.
Entre los recursos que puede ser necesario liberar están:
- Temporizadores creados con setInterval().
- Suscripciones a eventos globales.
- Conexiones o suscripciones externas.
- Recursos de bibliotecas que requieren una limpieza explícita.
Otro ejemplo en el que generamos eventos al presionar el teclado:
Borrar eventos en onUnmounted()
<script setup>
import { onMounted, onUnmounted } from 'vue'
function detectarTecla(evento) {
console.log('Tecla pulsada:', evento.key)
}
onMounted(() => {
window.addEventListener('keydown', detectarTecla)
})
onUnmounted(() => {
window.removeEventListener('keydown', detectarTecla)
})
</script>
<template>
<p>Pulsa cualquier tecla.</p>
</template>
El evento se registra cuando el componente se monta y se elimina cuando se desmonta. De esta manera evitamos que el componente deje manejadores innecesarios activos.
25 Encapsulación de lógica
En una aplicación real, un componente puede necesitar gestionar varias funcionalidades. Por ejemplo, un componente podría encargarse de:
- Obtener datos de una API.
- Controlar un formulario.
- Gestionar un contador.
- Detectar eventos del navegador.
- Calcular estadísticas.
Si toda la lógica se escribe dentro del mismo componente, el código puede resultar difícil de entender y mantener.
La Composition API permite agrupar la lógica relacionada y extraerla a funciones reutilizables. Supongamos que tenemos una funcionalidad para controlar un contador:
Ejemplo de componente
<script setup>
import { ref, computed } from 'vue'
const contador = ref(0)
const esPositivo = computed(() => contador.value > 0)
function incrementar() {
contador.value++
}
function decrementar() {
contador.value--
}
function reiniciar() {
contador.value = 0
}
</script>
<template>
<p>Contador: {{ contador }}</p>
<p>Es positivo: {{ esPositivo }}</p>
<button @click="incrementar">+</button>
<button @click="decrementar">-</button>
<button @click="reiniciar">Reiniciar</button>
</template>
Toda la lógica relacionada con el contador está agrupada. En un componente pequeño esto es suficiente. Sin embargo, si necesitamos reutilizar esa funcionalidad en varios componentes, podemos extraerla a una función independiente.
Esta función reutilizable recibe el nombre de composable.
25.1 Composables
Un composable es una función de JavaScript que utiliza la Composition API para encapsular y reutilizar lógica relacionada con el estado de una aplicación Vue.
Por convención, los nombres de los composables suelen comenzar por use. Algunos ejemplos de nombres habituales son:
- useContador.js
- useUsuarios.js
- useAutenticacion.js
- useVentana.js
- useLocalStorage.js
No son funciones especiales del lenguaje. Son funciones de JavaScript que siguen una convención de nombres y utilizan las herramientas reactivas de Vue. Un composable puede:
- Crear referencias reactivas.
- Calcular valores derivados.
- Definir funciones para modificar el estado.
- Utilizar watch().
- Registrar hooks del ciclo de vida cuando se llama desde un contexto válido de un componente.
La principal ventaja es que permite separar la lógica de la interfaz.
25.2 Crear composable
Vamos a extraer la lógica del contador anterior. Primero creamos un archivo llamado useContador.js.
Ejemplo de composable
import { ref, computed } from 'vue'
export function useContador() {
const contador = ref(0)
const esPositivo = computed(() => contador.value > 0)
function incrementar() {
contador.value++
}
function decrementar() {
contador.value--
}
function reiniciar() {
contador.value = 0
}
return {
contador,
esPositivo,
incrementar,
decrementar,
reiniciar
}
}
La función useContador() crea el estado reactivo y las funciones necesarias para trabajar con él. Al final devuelve un objeto con los elementos que queremos ofrecer a quien utilice el composable. Después podemos utilizarlo desde un componente.
Uso de composable en un componente
<script setup>
import { useContador } from './composables/useContador.js'
const {
contador,
esPositivo,
incrementar,
decrementar,
reiniciar
} = useContador()
</script>
<template>
<p>Contador: {{ contador }}</p>
<p>Es positivo: {{ esPositivo }}</p>
<button @click="incrementar">+</button>
<button @click="decrementar">-</button>
<button @click="reiniciar">Reiniciar</button>
</template>
El componente ya no contiene la implementación de la lógica del contador, sólo importa el composable y utiliza los elementos que este devuelve.
Aunque Vue no obliga a utilizar directorios para separar los componentes y/o los composables, resulta conveniente realizar cierta separación, por lo que se ha creado la carpeta composables.
25.3 Buenas prácticas con la Composition API
A continuación se exponen ciertas buenas prácticas a la hora de hacer uso de la Composition API.
25.3.1 Agrupar el código por responsabilidad
Es preferible que las variables, funciones y cálculos relacionados estén cerca unos de otros. Por ejemplo, en un formulario podemos agrupar:
- Los datos del formulario.
- Los errores de validación.
- Las funciones de validación.
- La función de envío.
Esto facilita comprender el comportamiento del componente.
25.3.2 Evitar componentes demasiado grandes
Cuando un componente empieza a contener demasiada lógica, podemos extraer algunas funcionalidades a composables. No es necesario crear un composable para cada función. Debemos hacerlo cuando exista una responsabilidad suficientemente clara o una posibilidad real de reutilización.
25.3.3 Utilizar computed() para los valores derivados
Si un valor puede calcularse a partir de otros datos reactivos, normalmente debemos utilizar computed(). Por ejemplo:
Uso de composable en un componente
const precio = ref(100)
const iva = ref(21)
const precioConIva = computed(() => {
return precio.value * (1 + iva.value / 100)
})
No es necesario mantener precioConIva manualmente sincronizado mediante diferentes funciones.
25.3.4 Reservar los hooks para sus momentos adecuados
Debemos utilizar cada hook del ciclo de vida para el propósito correspondiente:
- onMounted() para operaciones que necesitan el componente montado.
- onUpdated() para tareas que realmente requieren actuar después de una actualización del DOM.
- onUnmounted() para liberar recursos y limpiar suscripciones.
25.3.5 Liberar los recursos creados
Si registramos eventos globales o creamos intervalos, debemos asegurarnos de eliminarlos cuando corresponda. Esto ayuda a evitar fugas de memoria y operaciones innecesarias.
25.3.6 Mantener los composables independientes de la interfaz
Siempre que sea posible, un composable debe gestionar la lógica y el estado, mientras que el componente se encarga de representar la información. Esta separación facilita reutilizar la lógica y mantener una estructura ordenada.
Componentes avanzados
26 Componentes avanzados
En las unidades anteriores hemos aprendido a crear componentes Vue, gestionar su estado reactivo, comunicar unos componentes con otros y aplicar estilos CSS.
Vue proporciona además una serie de herramientas para resolver situaciones más avanzadas, como cambiar dinámicamente el componente que se muestra, conservar su estado cuando deja de estar visible, representar elementos fuera de la estructura habitual del DOM o aplicar transiciones al mostrar y ocultar contenido.
Entre los mecanismos avanzados que ofrece Vue 3 están:
- Componentes dinámicos.
- El elemento <component>.
- <KeepAlive> para conservar el estado de los componentes.
- <Teleport> para representar contenido en otra zona del DOM.
- <Transition> y <TransitionGroup> para crear transiciones.
- Componentes asíncronos mediante defineAsyncComponent().
- La gestión de contenido asíncrono y el papel de <Suspense>.
Estas herramientas permiten construir interfaces más dinámicas y mejorar tanto la experiencia de usuario como la organización de la aplicación.
26.1 Componentes dinámicos
Un componente dinámico es aquel que se selecciona durante la ejecución de la aplicación, en función del estado o de determinadas condiciones. Hasta ahora hemos utilizado componentes de forma directa:
Uso de componentes
<script setup>
import Cabecera from './components/Cabecera.vue'
import Productos from './components/Productos.vue'
</script>
<template>
<Cabecera />
<Productos />
</template>
En este caso, ambos componentes forman parte de la plantilla y Vue los representa siguiendo su estructura. Sin embargo, podemos necesitar mostrar un componente diferente dependiendo de la acción del usuario.
Por ejemplo, una interfaz con varias pestañas podría mostrar:
- Una vista de información personal.
- Una vista de configuración.
- Una vista de notificaciones.
Podríamos implementar cada vista mediante un componente independiente y decidir cuál mostrar en cada momento.
Los componentes dinámicos permiten resolver este tipo de situaciones sin tener que escribir una condición independiente para cada componente en toda la plantilla.
27 El elemento <component>
Vue proporciona el elemento especial <component> para representar dinámicamente un componente. Este elemento utiliza el atributo is para determinar qué componente debe mostrarse.
Uso de <component>
<script setup>
import { shallowRef } from 'vue'
import Perfil from './components/Perfil.vue'
import Configuracion from './components/Configuracion.vue'
import Notificaciones from './components/Notificaciones.vue'
const vistaActual = shallowRef(Perfil)
</script>
<template>
<nav>
<button @click="vistaActual = Perfil">
Perfil
</button>
<button @click="vistaActual = Configuracion">
Configuración
</button>
<button @click="vistaActual = Notificaciones">
Notificaciones
</button>
</nav>
<component :is="vistaActual" />
</template>
En este ejemplo, vistaActual contiene una referencia al componente que queremos mostrar. Cuando el usuario pulsa uno de los botones, cambia el valor de vistaActual y Vue representa el componente correspondiente. Se usa shallowRef porque de esta manera únicamente hace reactivo el valor de referencia, sin convertir profundamente el objeto almacenado.
Hay que usar shallowRef en lugar de ref, de lo contrario obtendremos warnings en la consola.
La expresión <component :is="vistaActual" /> indica que el componente que se debe representar depende del valor de la variable reactiva.
Crea un componente que tenga tres botones. Dependiendo del pulsado se visualizará un componente hijo u otro.
27.1 Utilizar nombres de componentes
También podemos seleccionar un componente mediante una cadena de texto, siempre que el nombre pueda resolverse correctamente.
Por ejemplo, con componentes importados y registrados de forma local en <script setup>, es recomendable utilizar directamente las referencias a los componentes, como en el ejemplo anterior.
Esto evita depender de nombres de componentes registrados globalmente.
27.2 Componentes dinámicos y v-if
Podríamos resolver el ejemplo de las pestañas utilizando v-if:
Uso de v-if
<Perfil v-if="vistaActual === 'perfil'" />
<Configuracion v-else-if="vistaActual === 'configuracion'" />
<Notificaciones v-else />
Esta solución también es válida. Sin embargo, <component> resulta especialmente útil cuando la selección dinámica de componentes forma parte importante de la lógica de la interfaz.
Debemos recordar que cambiar de componente puede provocar que el componente anterior se desmonte y que el nuevo se monte. Si necesitamos conservar el estado de los componentes al alternar entre ellos, podemos utilizar <KeepAlive>.
28 <KeepAlive>
De forma predeterminada, cuando Vue deja de representar un componente dinámico y pasa a mostrar otro, el componente anterior puede desmontarse.
Al desmontarse, su estado local deja de mantenerse como el estado activo de esa instancia. Por ejemplo, imaginemos un componente que contiene un formulario:
Formulario simple
<script setup>
import { ref } from 'vue'
const nombre = ref('')
</script>
<template>
<form>
<label>
Nombre:
<input v-model="nombre">
</label>
<p>Nombre introducido: {{ nombre }}</p>
</form>
</template>
Si el usuario escribe un nombre, cambia a otra vista y después vuelve al formulario, puede encontrarse con que el componente se ha creado de nuevo y el campo está vacío. En determinadas situaciones esto no es deseable, por lo que Vue proporciona <KeepAlive> para conservar las instancias de los componentes dinámicos cuando dejan de estar activas.
Por ejemplo:
Uso de keepalive
<KeepAlive>
<component :is="vistaActual" />
</KeepAlive>
Con esta estructura, Vue puede mantener en caché la instancia del componente que deja de mostrarse, en lugar de destruirla inmediatamente. Cuando se vuelve a seleccionar ese componente, Vue puede reutilizar la instancia conservada y mantener su estado local.
28.1 Hooks relacionados con <KeepAlive>
Los componentes que se conservan mediante <KeepAlive> tienen un comportamiento especial. En lugar de desmontarse cuando dejan de estar activos, pueden desactivarse y volver a activarse posteriormente.
Vue proporciona dos hooks para detectar estas situaciones:
- onActivated(): se ejecuta cuando la instancia conservada se activa.
- onDeactivated(): se ejecuta cuando la instancia conservada se desactiva.
Por ejemplo:
Uso de onActivated() y onDeactivated()
<script setup>
import { onActivated, onDeactivated } from 'vue'
onActivated(() => {
console.log('El componente está activo')
})
onDeactivated(() => {
console.log('El componente está inactivo')
})
</script>
<template>
<p>Componente conservado en caché</p>
</template>
Estos hooks son útiles cuando necesitamos ejecutar operaciones específicas al recuperar o abandonar temporalmente una vista.
28.2 Limitar los componentes conservados
<KeepAlive> permite utilizar los atributos include, exclude y max. El parámetro max establece el número máximo de instancias que se conservan en la caché.
Uso de max
<KeepAlive :max="5">
<component :is="vistaActual" />
</KeepAlive>
También podemos limitar los componentes que se almacenan:
Uso de include
<KeepAlive :include="['Perfil', 'Formulario']">
<component :is="vistaActual" />
</KeepAlive>
El comportamiento de include y exclude depende del nombre del componente. Conviene comprobar que los componentes tienen los nombres esperados.
<KeepAlive> no debe utilizarse indiscriminadamente. Mantener demasiadas instancias en memoria puede aumentar el consumo de recursos. Debemos emplearlo cuando conservar el estado aporte una ventaja real.
29 <Teleport>
Normalmente, Vue representa el contenido de un componente en la posición del DOM que corresponde a su plantilla. Sin embargo, en ocasiones necesitamos mostrar contenido en otra zona del documento. Por ejemplo:
- Una ventana modal.
- Un cuadro de confirmación.
- Un menú emergente.
- Un aviso flotante.
- Un panel que debe situarse directamente bajo el elemento <body>.
Vue proporciona <Teleport> para resolver este problema. <Teleport> permite representar una parte de la plantilla en otro lugar del DOM sin cambiar la organización lógica de los componentes.
En el siguiente ejemplo, el contenido de <Teleport> se representa dentro del elemento body, aunque el código de la plantilla esté situado dentro de un componente Vue.
Uso de Teleport
<template>
<button>
Abrir ventana
</button>
<Teleport to="body">
<div class="modal">
<h2>Ventana modal</h2>
<p>Este contenido se representa dentro de body.</p>
</div>
</Teleport>
</template>
El atributo to indica el destino. Puede utilizarse un selector CSS (como el ejemplo anterior), o un selector que identifique otro elemento existente en el DOM (que debe existir el elemento con ese identificador):
Uso de Teleport
<Teleport to="#zona-modales">
<!-- ... -->
</Teleport>
29.1 Aspectos importantes de <Teleport>
Hay que tener ciertos aspectos importantes al usar <Teleport>:
- No crea un componente nuevo.
- No cambia la relación lógica entre el componente padre y el contenido teletransportado.
- El contenido sigue utilizando el estado y los eventos del componente que lo declara.
- El destino debe existir cuando Vue necesite representar el contenido, salvo que se utilicen las opciones específicas previstas para destinos diferidos en versiones compatibles.
- No proporciona automáticamente toda la accesibilidad de una ventana modal.
En una ventana modal real también debemos considerar el foco del teclado, la posibilidad de cerrarla mediante Escape, la gestión del foco al cerrarla y el comportamiento para lectores de pantalla.
Si no usamos <Teleport>, dependiendo de dónde se incluyese el componente, parte de su contenido podría quedar recortado, o podría afectar a su comportamiento.
Al utilizar <Teleport>, podemos representar la ventana en una zona distinta del DOM y evitar determinados problemas de posicionamiento y apilamiento.
Crea un componente que haga un modal y “teletranspórtalo” a un <div id="modals"> que esté en el index.html.
30 <Transition>
Vue permite añadir transiciones cuando un elemento aparece o desaparece de la interfaz. Por ejemplo, podemos hacer que un mensaje aparezca progresivamente en lugar de mostrarse de forma inmediata. Para ello utilizamos el componente integrado <Transition>.
Uso de Transition
<script setup>
import { ref } from 'vue'
const visible = ref(false)
</script>
<template>
<button @click="visible = !visible">
Mostrar u ocultar
</button>
<Transition>
<p v-if="visible" class="mensaje">
Hola, este mensaje tiene una transición.
</p>
</Transition>
</template>
<style scoped>
.mensaje {
padding: 16px;
background: lightblue;
border-radius: 8px;
}
.v-enter-active,
.v-leave-active {
transition: opacity 0.4s ease;
}
.v-enter-from,
.v-leave-to {
opacity: 0;
}
</style>
Cuando el elemento aparece, su opacidad pasa de 0 a su valor normal. Cuando desaparece, la opacidad pasa a 0 antes de que Vue retire el elemento del DOM.
Partiendo del ejercicio 15 añade un botón por fila que simule borrar un usuario y haga una transición eliminando la fila.
30.1 Clases de transición
Vue aplica diferentes clases de CSS durante las fases de entrada y salida tal como se puede ver en la siguiente imagen:
En la siguiente tabla aparece cada clase y el momento en el que se usan:
| Clase | Momento |
|---|---|
| v-enter-from | Estado inicial de la entrada |
| v-enter-active | Durante la transición de entrada |
| v-enter-to | Estado final de la entrada |
| v-leave-from | Estado inicial de la salida |
| v-leave-active | Durante la transición de salida |
| v-leave-to | Estado final de la salida |
Estas clases utilizan el prefijo v- por defecto pero podemos personalizarlo mediante el atributo name como aparece en el siguiente ejemplo:
Clases personalizadas
<Transition name="desvanecer">
<p v-if="visible">
Mensaje
</p>
</Transition>
<style scoped>+
.desvanecer-enter-active,
.desvanecer-leave-active {
transition: opacity 0.4s ease;
}
.desvanecer-enter-from,
.desvanecer-leave-to {
opacity: 0;
}
</style>
En el ejemplo anterior, gracias al parámetro name, las clases utilizarán el prefijo desvanecer- tal como se ve en el apartado de estilos. Gracias a esto, podemos tener diferentes tipos de transiciones en una misma aplicación.
30.2 Transiciones con desplazamiento
También podemos combinar opacidad y desplazamiento. Si utilizamos name="deslizar", el elemento aparecerá y desaparecerá con un pequeño desplazamiento vertical utilizando el siguiente ejemplo:
Clases personalizadas
<style scoped>
.deslizar-enter-active,
.deslizar-leave-active {
transition:
opacity 0.3s ease,
transform 0.3s ease;
}
.deslizar-enter-from {
opacity: 0;
transform: translateY(12px);
}
.deslizar-leave-to {
opacity: 0;
transform: translateY(-12px);
}
</style>
30.3 ¿Qué elementos pueden utilizar <Transition>?
<Transition> se utiliza habitualmente con un único elemento o componente que aparece o desaparece. Puede trabajar, entre otros, con:
- Elementos controlados por v-if.
- Elementos controlados por v-show.
- Componentes dinámicos.
- Componentes que cambian de una vista a otra.
Por ejemplo:
Clases personalizadas
<Transition name="deslizar">
<div v-if="visible">
Contenido animado
</div>
</Transition>
En cambio, si queremos animar la inserción, eliminación u ordenación de varios elementos de una lista, normalmente debemos utilizar <TransitionGroup> que veremos después.
30.4 Consideraciones de accesibilidad
Las animaciones deben mejorar la experiencia de usuario, no dificultarla. Debemos evitar movimientos excesivos y considerar la preferencia del usuario por reducir las animaciones. Podemos utilizar la consulta de medios prefers-reduced-motion:
Ejemplo para reducir transiciones
@media (prefers-reduced-motion: reduce) {
.deslizar-enter-active,
.deslizar-leave-active {
transition: none;
}
}
De esta forma, los usuarios que hayan indicado que prefieren reducir el movimiento podrán experimentar una interfaz con menos animaciones.
31 <TransitionGroup>
<TransitionGroup> permite animar la entrada, salida y movimiento de varios elementos de una lista. Es especialmente útil cuando una lista cambia como consecuencia de las acciones del usuario. Por ejemplo, podemos crear una lista en la que sea posible añadir y eliminar elementos.
Ejemplo TransitionGroup
<script setup>
import { ref } from 'vue'
const siguienteId = ref(4)
const elementos = ref([
{ id: 1, texto: 'Elemento 1' },
{ id: 2, texto: 'Elemento 2' },
{ id: 3, texto: 'Elemento 3' }
])
function agregar() {
elementos.value.push({
id: siguienteId.value,
texto: `Elemento ${siguienteId.value}`
})
siguienteId.value++
}
function eliminar(id) {
elementos.value = elementos.value.filter(
elemento => elemento.id !== id
)
}
</script>
<template>
<button @click="agregar">
Añadir elemento
</button>
<TransitionGroup name="lista" tag="ul">
<li
v-for="elemento in elementos"
:key="elemento.id"
>
{{ elemento.texto }}
<button @click="eliminar(elemento.id)">
Eliminar
</button>
</li>
</TransitionGroup>
</template>
<style scoped>
.lista-enter-active,
.lista-leave-active,
.lista-move {
transition: all 0.3s ease;
}
.lista-enter-from,
.lista-leave-to {
opacity: 0;
transform: translateX(20px);
}
.lista-leave-active {
position: absolute;
}
</style>
En este ejemplo, los elementos pueden aparecer y desaparecer con una transición. Además, si su posición cambia debido a la inserción o eliminación de otros elementos, la clase lista-move permite animar ese desplazamiento.
31.1 Diferencias entre <Transition> y <TransitionGroup>
En la siguiente tabla se puede ver las diferencias entre las dos características vistas previamente:
| Característica | <Transition> | <TransitionGroup> |
|---|---|---|
| Uso principal | Entrada y salida de un elemento o componente | Entrada, salida y movimiento de elementos de una lista |
| Número de elementos | Normalmente uno | Varios |
| Uso de key | Depende del elemento y de la situación | Cada elemento de la lista debe tener una clave única |
| Elemento contenedor | No introduce necesariamente un contenedor adicional | Puede utilizar tag para definir el elemento contenedor |
<TransitionGroup> no sustituye a v-for: normalmente se utiliza junto a esta directiva.
Es importante proporcionar claves estables para que Vue pueda identificar los elementos y aplicar las transiciones correctamente.
32 Componentes asíncronos
En una aplicación grande, no siempre es necesario cargar todos los componentes al iniciar la página. Por ejemplo, podemos tener una aplicación con:
- Una página principal.
- Un panel de estadísticas.
- Un editor avanzado.
- Un sistema de informes.
Si algunos componentes solo se utilizan ocasionalmente, cargar todo su código desde el principio puede aumentar el tiempo de carga inicial.
Vue permite definir componentes asíncronos para cargar determinados componentes cuando se necesitan. La idea consiste en dividir el código de la aplicación en diferentes fragmentos que puedan cargarse por separado. Esto suele combinarse con las importaciones dinámicas de JavaScript.
La importación dinámica devuelve una promesa que se resuelve con el módulo cargado. Sin embargo, en una plantilla Vue no debemos utilizar directamente esa promesa como si fuera un componente. Para gestionar este proceso disponemos de defineAsyncComponent().
32.1 defineAsyncComponent()
La función defineAsyncComponent() permite definir un componente cuya implementación se carga de forma asíncrona.
Ejemplo defineAsyncComponent
<script setup>
import { defineAsyncComponent } from 'vue'
const Estadisticas = defineAsyncComponent(
() => import('./components/Estadisticas.vue')
)
</script>
<template>
<Estadisticas />
</template>
Vue cargará el componente mediante una importación dinámica cuando sea necesario. Con Vite, esta importación permite que el sistema de compilación genere un fragmento de código independiente cuando corresponda. De esta forma, el componente puede no formar parte del código JavaScript inicial de la aplicación.
Entre las ventajas de usar componentes asíncronos se puede destacar:
- Permiten dividir el código en fragmentos.
- Pueden reducir el código que debe descargarse inicialmente.
- Facilitan cargar componentes pesados solo cuando se necesitan.
- Son útiles para paneles, editores, informes y otras funciones secundarias.
No obstante, dividir el código no garantiza por sí solo una mejora del rendimiento. Debemos considerar el tamaño de los componentes, cuándo se cargan y la experiencia que percibe el usuario.
32.2 Componentes asíncronos con opciones
La función defineAsyncComponent() también permite configurar diferentes opciones, como un componente de carga, un componente de error o un tiempo de espera.
Por ejemplo:
Ejemplo con opciones
<script setup>
import { defineAsyncComponent } from 'vue'
import Cargando from './components/Cargando.vue'
import ErrorCarga from './components/ErrorCarga.vue'
const Estadisticas = defineAsyncComponent({
loader: () => import('./components/Estadisticas.vue'),
loadingComponent: Cargando,
errorComponent: ErrorCarga,
delay: 200,
timeout: 10000
})
</script>
<template>
<Estadisticas />
</template>
En este ejemplo:
- loader: define cómo se carga el componente.
- loadingComponent: especifica qué componente mostrar mientras se carga.
- errorComponent: indica qué componente utilizar si se produce un error de carga.
- delay: establece el retraso antes de mostrar el componente de carga.
- timeout: establece un tiempo límite tras el cual la carga se considera fallida.
El componente de error permite ofrecer una respuesta visual si el módulo no puede cargarse y un tiempo de espera agotado no significa necesariamente que la descarga se haya cancelado. Debemos diseñar el tratamiento de errores teniendo en cuenta las limitaciones reales de la red y de la aplicación.
33 Suspense
<Suspense> es un componente integrado de Vue diseñado para coordinar estados de espera asociados a dependencias asíncronas de determinados componentes. Es decir, permite definir contenido alternativo mientras se resuelven esas dependencias.
Actualmente Suspense es una feature experimental.
Por ejemplo, podemos tener una interfaz principal y mostrar un mensaje de carga mientras se prepara un componente asíncrono.
Ejemplo con Suspense
<script setup>
import { defineAsyncComponent } from 'vue'
const Estadisticas = defineAsyncComponent(
() => import('./components/Estadisticas.vue')
)
</script>
<template>
<Suspense>
<template #default>
<Estadisticas />
</template>
<template #fallback>
<p>Cargando estadísticas...</p>
</template>
</Suspense>
</template>
La idea general es que Vue pueda mostrar el contenido de reserva mientras espera a que se resuelvan las dependencias asíncronas que reconoce dentro del árbol correspondiente.
33.1 Diferencias entre con loadingComponent
Ambos mecanismos están relacionados con la carga asíncrona, pero tienen propósitos diferentes:
- loadingComponent, configurado en defineAsyncComponent(), proporciona un componente de carga para ese componente asíncrono.
- <Suspense>, por su parte, coordina un estado de espera en un árbol de componentes y permite definir contenido alternativo mediante el slot fallback.
No debemos asumir que cualquier petición realizada con fetch() activa automáticamente <Suspense>. Una petición asíncrona ordinaria no queda coordinada automáticamente por este componente solo por ejecutarse dentro de un componente Vue.
33.2 Consideración importante sobre su uso
La API de <Suspense> debe utilizarse teniendo en cuenta la versión de Vue y las características concretas de la aplicación. Su estado y sus opciones de uso pueden evolucionar.
En proyectos sencillos, gestionar explícitamente los estados “cargando”, “error” y “datos” mediante referencias reactivas sigue siendo una solución clara y válida.
Por ejemplo:
Ejemplo con Suspense
<script setup>
import { ref, onMounted } from 'vue'
const datos = ref(null)
const cargando = ref(true)
const error = ref('')
onMounted(async () => {
try {
const respuesta = await fetch('/api/estadisticas')
if (!respuesta.ok) {
throw new Error('No se pudieron cargar las estadísticas')
}
datos.value = await respuesta.json()
} catch (err) {
error.value = err.message
} finally {
cargando.value = false
}
})
</script>
<template>
<p v-if="cargando">
Cargando estadísticas...
</p>
<p v-else-if="error">
{{ error }}
</p>
<pre v-else>{{ datos }}</pre>
</template>
Este patrón resulta especialmente útil cuando queremos controlar explícitamente los estados de carga y error de una petición HTTP.
SPA y Vue Router
34 Introducción a las aplicaciones SPA
En las aplicaciones web tradicionales, cada página suele estar asociada a una URL diferente. Cuando el usuario pulsa un enlace, el navegador solicita una nueva página al servidor y carga el documento correspondiente.
Sin embargo, muchas aplicaciones web modernas utilizan una arquitectura denominada SPA (Single Page Application). En este tipo de aplicaciones, gran parte de la navegación se realiza sin recargar completamente el documento HTML. Es decir, carga inicialmente un documento HTML y modifica posteriormente su contenido mediante JavaScript, sin necesidad de solicitar un documento HTML completo cada vez que el usuario cambia de vista.
Vue Router es la solución oficial de enrutamiento para Vue. Permite asociar diferentes URL con componentes, gestionar la navegación y construir aplicaciones con múltiples vistas utilizando Vue 3.
Imaginemos una aplicación de gestión de productos con las siguientes direcciones:
- / página principal.
- /productos: listado de productos.
- /productos/15: detalles del producto con identificador 15.
- /configuracion configuración de la aplicación.
En una aplicación tradicional, cada URL podría corresponder a un documento HTML diferente generado por el servidor. En cambio, en una SPA desarrollada con Vue, el enrutamiento permite relacionar una dirección URL con una vista o un comportamiento de la aplicación.
Siguiendo con el ejemplo anterior, al acceder a /productos, Vue Router puede seleccionar el componente ListaProductos.vue y mostrarlo dentro de la estructura principal de la aplicación. Y cuando el usuario navegue a /configuracion, Vue Router selecciona otro componente sin necesidad de recargar toda la página.
34.1 Ventajas y limitaciones de una SPA
A la hora de crear una aplicación SPA, podemos destacar las siguientes ventajas:
- La navegación puede resultar más fluida.
- Se pueden reutilizar componentes y estructuras comunes.
- Es posible mantener parte del estado de la aplicación durante la navegación.
- La interfaz puede actualizarse sin recargar el documento completo.
- Facilita la creación de aplicaciones interactivas y complejas.
En lo referente a las limitaciones, se pueden comentar las siguientes:
- El navegador necesita descargar y ejecutar JavaScript.
- Una aplicación mal optimizada puede tardar en mostrar su contenido inicial.
- La navegación directa a una URL puede requerir configuración específica en el servidor.
- El posicionamiento en buscadores depende de la arquitectura y de cómo se genere y entregue el contenido.
- El desarrollador debe gestionar correctamente los estados, las rutas y los errores de navegación.
Una SPA no implica necesariamente que toda la aplicación deba ejecutarse exclusivamente en el navegador. Existen otras arquitecturas, como la generación estática y la representación en servidor, que pueden combinarse con tecnologías del ecosistema Vue.
35 Instalación de Vue Router
En los proyectos Vue 3 creados con el asistente, podemos elegir si queremos instalar Vue Router como una dependencia del proyecto, por lo que si creamos un proyecto desde cero, los pasos explicados a continuación no serían necesarios.
Si queremos instalarlo en un proyecto existente, desde una terminal, dentro de la carpeta del proyecto, ejecutamos:
Composition API
ruben@archy:~/proyecto/ $ npm install vue-router
Este comando descarga el paquete y lo incorpora a las dependencias del proyecto. Podemos comprobar su incorporación en el archivo package.json. Una vez instalado, podremos importar las funciones y los componentes proporcionados por Vue Router.
Entre los elementos más importantes se encuentran:
- createRouter(): crea la instancia del enrutador.
- createWebHistory(): permite utilizar el historial del navegador con URL convencionales.
- RouterView: indica dónde se muestra el componente asociado a la ruta actual.
- RouterLink: crea enlaces de navegación.
- useRouter(): permite acceder al enrutador desde la lógica de un componente.
- useRoute(): permite consultar la ruta actual.
En los siguientes apartados veremos cómo se utiliza cada uno de estos elementos.
35.1 Configuración del router
Una práctica habitual consiste en crear una carpeta router y guardar en ella la configuración de las rutas. La estructura inicial del proyecto podría quedar de la siguiente manera:
Estructura del proyecto
src/
├── assets/
├── components/
├── views/
│ ├── InicioView.vue
│ ├── ProductosView.vue
│ └── ConfiguracionView.vue
├── router/
│ └── index.js
├── App.vue
└── main.js
La carpeta views suele utilizarse para almacenar los componentes que representan páginas o vistas completas. La carpeta components suele contener componentes reutilizables de menor alcance.
Esta organización es una convención, no una obligación de Vue, pero en un proyecto creado con el asistene es la organización por defecto.
35.2 Crear el archivo de rutas
El fichero donde se guardan las rutas es src/router/index.js, y una configuración básica sería la siguiente (puede ser diferente al generado por el asistente):
Fichero de rutas
import { createRouter, createWebHistory } from 'vue-router'
import InicioView from '../views/InicioView.vue'
import ProductosView from '../views/ProductosView.vue'
import ConfiguracionView from '../views/ConfiguracionView.vue'
const routes = [
{
path: '/',
component: InicioView
},
{
path: '/productos',
component: ProductosView
},
{
path: '/configuracion',
component: ConfiguracionView
}
]
const router = createRouter({
history: createWebHistory(),
routes
})
export default router
Vamos a analizar las partes principales del fichero:
- import: Es necesario importar dos funciones de vue-router que se utilizarán después. Aparte, también se importan las distintas vistas que se pueden acceder a través de distintas URLs.
- routes: Es un array de objetos. Cada objeto describe una ruta y el componente que debe mostrarse cuando la URL coincide con ella.
- createRouter(): Crea la instancia del enrutador. Recibe un objeto de configuración que contiene, entre otras propiedades, el sistema de historial y las rutas.
- createWebHistory(): Permite utilizar URL convencionales, como /productos, en lugar de depender de direcciones con el formato /#/productos (utilizadas en aplicaciones javascript puras para no necesitar recargar la página). Este sistema utiliza la API de historial del navegador. Por ello, el servidor debe estar configurado para devolver la aplicación cuando se soliciten directamente las rutas que administra Vue Router.
- export default router: Exporta la instancia del enrutador para que pueda incorporarse a la aplicación.
36 Incorporar el router a la aplicación
Crear la configuración de las rutas no es suficiente. También debemos registrar el enrutador en la aplicación Vue. Para ello modificamos src/main.js.
Fichero de rutas
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import './assets/main.css'
createApp(App)
.use(router)
.mount('#app')
La instrucción .use(router) registra Vue Router como un complemento de la aplicación. A partir de ese momento, Vue Router estará disponible para gestionar la navegación y proporcionar sus componentes y funciones.
36.1 Mostrar las vistas mediante RouterView
Ahora debemos indicar dónde queremos mostrar el componente correspondiente a la ruta actual. Para ello utilizamos RouterView en App.vue.
Fichero App.vue
<template>
<header>
<h1>Mi aplicación Vue</h1>
</header>
<main>
<RouterView />
</main>
</template>
RouterView es un componente de Vue Router. Su función consiste en representar el componente asociado a la ruta actual. Si el navegador se encuentra en /productos, se mostrará ProductosView y cuando se accede a /configuracion, se mostrará ConfiguracionView.
Podemos entender RouterView como el espacio de la interfaz reservado para la vista que corresponde a la URL actual.
Crea un proyecto Vue con el asistente marcando la opción “Router (SPA development)” y código de ejemplo. Analiza la aplicación y busca dónde aparece todo lo visto hasta ahora.
37 RouterLink
Para navegar entre las rutas, Vue Router proporciona el componente RouterLink, aunque podríamos utilizar enlaces HTML convencionales. RouterLink está diseñado específicamente para la navegación dentro de una aplicación Vue Router.
Por ejemplo, podemos modificar App.vue:
Fichero App.vue
<template>
<header>
<h1>Mi aplicación Vue</h1>
<nav>
<RouterLink to="/">Inicio</RouterLink>
<RouterLink to="/productos">Productos</RouterLink>
<RouterLink to="/configuracion">Configuración</RouterLink>
</nav>
</header>
<main>
<RouterView />
</main>
</template>
Cuando el usuario pulsa uno de estos enlaces, Vue Router actualiza la navegación y muestra la vista correspondiente sin recargar completamente el documento.
37.1 El atributo to
El atributo to indica el destino del enlace.
Atributo “to”
<RouterLink to="/productos">
Ver productos
</RouterLink>
Atributo “:to”
<RouterLink :to="rutaDestino">
Ir a la sección
</RouterLink>
También podemos utilizar una expresión JavaScript mediante :to, y de esta manera la variable rutaDestino debe contener una dirección válida o un objeto de localización admitido por Vue Router.
37.2 Estilos para el enlace activo
Vue Router aplica clases CSS a los enlaces que coinciden con la ruta actual. Entre ellas se encuentran:
- router-link-active: indica que el enlace coincide con la ruta actual según las reglas de coincidencia de Vue Router
- router-link-exact-active: identifica una coincidencia exacta de la ruta, teniendo en cuenta los parámetros relevantes
Podemos utilizar estas clases CSS para destacar el enlace correspondiente a la vista actual:
Clases CSS
nav {
display: flex;
gap: 16px;
}
nav a {
color: #444;
text-decoration: none;
}
nav a.router-link-active {
color: royalblue;
font-weight: bold;
}
nav a.router-link-exact-active {
text-decoration: underline;
}
De esta forma, el usuario puede identificar en qué sección se encuentra.
39 Parámetros de ruta
Es habitual en aplicaciones de gestión que necesitemos que una ruta incluya información variable. Por ejemplo, cada producto puede tener un identificador diferente y para acceder a ellos se usa la URL /productos/10, donde 10 es el identificador único.
No tendría sentido crear una ruta independiente para cada producto, y por eso Vue Router permite definir parámetros dinámicos. En src/router/index.js podemos añadir:
Añadir ruta con identificador
{
path: '/productos/:id',
component: ProductoDetalleView
}
El fragmento :id indica que la ruta contiene un parámetro dinámico llamado id.
39.1 Recuperar el parámetro
Para obtener el identificador tenemos que utilizar useRoute() en el componente que lo necesitemos. Por ejemplo, en ProductoDetalleView.vue:
Recuperar parámetro
<script setup>
import { useRoute } from 'vue-router'
const route = useRoute()
const idProducto = route.params.id
</script>
<template>
<h1>Detalle del producto</h1>
<p>Identificador: {{ idProducto }}</p>
</template>
Si la URL es /productos/15, el parámetro route.params.id contendrá el valor "15". Debemos tener en cuenta que los parámetros de ruta suelen recibirse como cadenas de texto. Si necesitamos realizar operaciones numéricas, debemos convertirlos y validar el resultado.
Recuperar parámetro
const id = Number(route.params.id)
if (!Number.isInteger(id) || id <= 0) {
// Gestionar el identificador no válido.
}
- Partiendo del ejercicio anterior, usa la API y que el navbar muestre para visualizar usuarios y posts.
- Crea un composable (como el del ejercicio 15) para llamar a la API y obtener todos los usuarios y un usuario concreto.
- En el componente de usuarios que visualice una tabla con todos los usuarios. Cada usuario tenga un enlace a /users/:id
- Crea la ruta para /users/:id que apunte a un nuevo componente que obtenga el :id, llame a la API y visualice todos los datos de usuario.
40 Parámetros de consulta
Las URL también pueden contener parámetros de consulta, situados después del carácter ?, por ejemplo /productos?categoria=informatica&orden=precio. En esta dirección encontramos dos parámetros:
- categoria: tiene el valor “informatica”.
- orden: tiene el valor “precio”.
Estos parámetros suelen utilizarse para filtros, búsquedas, paginación y ordenación.
A diferencia de los parámetros de ruta, los parámetros de consulta no suelen identificar la estructura principal del recurso, sino opciones relacionadas con la vista.
40.1 Recuperar los parámetros
Podemos utilizar useRoute() para consultar route.query.
Recuperar parámetro
<script setup>
import { useRoute } from 'vue-router'
const route = useRoute()
const categoria = route.query.categoria
const orden = route.query.orden
</script>
<template>
<h1>Productos</h1>
<p>Categoría: {{ categoria }}</p>
<p>Orden: {{ orden }}</p>
</template>
Hay que recordar que un parámetro de consulta puede no existir o aparecer varias veces. Por ello, no debemos asumir siempre que su valor será una única cadena.
40.3 ¿Por qué utilizar parámetros de consulta?
Los parámetros de consulta ofrecen varias ventajas:
- Permiten compartir una búsqueda mediante una URL.
- Facilitan recuperar los filtros al recargar la página.
- Permiten utilizar los botones atrás y adelante del navegador para recuperar estados de navegación.
- Facilitan enlazar directamente a una vista con determinadas opciones.
Si el usuario selecciona una categoría y ordena los productos por precio, reflejar estas opciones en la URL puede hacer que la aplicación resulte más predecible.
41 Rutas anidadas
Una aplicación puede tener vistas formadas por varias secciones relacionadas. Por ejemplo, una zona de administración podría incluir:
- Una página general.
- Una sección de usuarios.
- Una sección de productos.
- Una sección de configuración.
Todas estas vistas estarían bajo la URL /administracion y pueden compartir una estructura común, como una barra lateral y una cabecera. Las rutas anidadas permiten representar esta organización.
41.1 Definir rutas anidadas
Dentro del fichero de rutas podemos crear nuevas rutas, donde las rutas hijas utilizan rutas relativas. Por ejemplo, la ruta hija usuarios corresponde a /administracion/usuarios.
Crear URL con parámetros
const routes = [
{
path: '/administracion',
component: AdministracionView,
children: [
{
path: '',
component: ResumenAdminView
},
{
path: 'usuarios',
component: UsuariosView
},
{
path: 'productos',
component: ProductosAdminView
}
]
}
]
La ruta hija con path: ’’ representa la vista predeterminada cuando se accede a /administracion.
41.2 Mostrar las rutas hijas
El componente padre debe incluir un RouterView adicional donde se mostrarán sus vistas hijas. Por ejemplo, en AdministracionView.vue:
AdministracionView.vue
<template>
<div class="administracion">
<aside>
<nav>
<RouterLink to="/administracion">Resumen</RouterLink>
<RouterLink to="/administracion/usuarios">Usuarios</RouterLink>
<RouterLink to="/administracion/productos">Productos</RouterLink>
</nav>
</aside>
<main>
<RouterView />
</main>
</div>
</template>
El RouterView de App.vue mostrará AdministracionView, y el RouterView de AdministracionView.vue mostrará la vista hija correspondiente. La estructura resultante puede entenderse así:
Estructura de las rutas
App.vue
└── RouterView
└── AdministracionView.vue
├── Menú lateral
└── RouterView
├── ResumenAdminView.vue
├── UsuariosView.vue
└── ProductosAdminView.vue
Las rutas anidadas son especialmente útiles cuando diferentes páginas comparten una estructura visual.
42 Rutas dinámicas y nombres de ruta
Hasta ahora hemos navegado utilizando directamente la URL, pero Vue Router permite asignar un nombre a cada ruta. Por ejemplo:
Rutas con nombre
const routes = [
{
path: '/',
name: 'inicio',
component: InicioView
},
{
path: '/productos',
name: 'productos',
component: ProductosView
},
{
path: '/productos/:id',
name: 'producto-detalle',
component: ProductoDetalleView
}
]
Los nombres de ruta permiten referirse a una ruta sin tener que escribir manualmente su URL.
42.1 Navegar mediante el nombre
Gracias a añadir nombres a las rutas, podemos utilizar RouterLink de esta manera:
RouterLink con nombre
<RouterLink
:to="{ name: 'productos' }">
Productos
</RouterLink>
Nombre y atributos
<RouterLink
:to="{
name: 'producto-detalle',
params: { id: 15 }
}"
>
Ver producto
</RouterLink>
Vue Router construirá la URL correspondiente, en el segundo ejemplo de esta manera /productos/15. También podemos utilizar nombres de ruta desde JavaScript:
Nombre y atributos
router.push({
name: 'producto-detalle',
params: {
id: 15
}
})
42.2 Ventajas de utilizar nombres de ruta
Cuando se usan nombres en las rutas podemos destacar las siguientes ventajas:
- Evitan repetir las URL en muchos componentes.
- Facilitan modificar las direcciones sin tener que cambiar todos los enlaces.
- Permiten generar URL a partir de parámetros.
- Hacen más explícita la intención de la navegación.
Cuando una aplicación tiene muchas rutas, utilizar nombres descriptivos suele facilitar el mantenimiento del código.
43 Redirecciones y rutas no encontradas
En una aplicación real, el usuario puede introducir una dirección incorrecta o acceder a una ruta que ya no existe, o también podemos necesitar redirigir una ruta antigua a otra nueva. Vue Router permite definir ambos comportamientos.
43.1 Redirecciones
Para generar una redirección tenemos el siguiente ejemplo:
Redirección
const routes = [
{
path: '/inicio',
redirect: '/'
}
]
Redirección con nombre
{
path: '/catalogo',
redirect: { name: 'productos' }
}
Cuando el usuario acceda a /inicio, Vue Router lo redirigirá a /. Este tipo de redirecciones son útiles por temas de posicionamiento de buscadorest y cuando cambiamos la organización de las rutas o queremos mantener direcciones antiguas.
43.2 Ruta no encontrada
Podemos definir una ruta que capture las URL que no coinciden con ninguna de las rutas anteriores. De esta manera, si el usuario intenta entrar en una ruta que no existe se visualizará el componente NoEncontradaView.vue en lugar de encontrarse con una pantalla vacía.
Añadir ruta no encontrada
{
path: '/:pathMatch(.*)*',
name: 'no-encontrada',
component: NoEncontradaView
}
Componente
<template>
<section>
<h1>404 - Página no encontrada</h1>
<p>La dirección no existe.</p>
<RouterLink to="/">
Volver al inicio
</RouterLink>
</section>
</template>
Esta ruta debe situarse después de las rutas específicas.
La ruta de Vue Router no sustituye necesariamente a una respuesta HTTP 404 del servidor. En una SPA, el servidor puede entregar el documento principal con un estado HTTP satisfactorio y dejar que Vue Router muestre su vista de error. Si necesitamos códigos HTTP específicos, debemos tenerlos en cuenta en la configuración del servidor o en la arquitectura de renderizado.
La ruta de Vue Router no sustituye a una respuesta HTTP 404 del servidor.
45 Lazy loading de rutas
En aplicaciones grandes, puede resultar innecesario cargar todos los componentes de todas las rutas al iniciar la aplicación. Vue Router permite utilizar importaciones dinámicas para cargar las vistas cuando se necesitan.
Podemos destacar como ventajas las siguientes:
- Puede reducir el tamaño del código JavaScript inicial.
- Permite dividir una aplicación grande en fragmentos.
- Evita descargar inmediatamente el código de vistas que quizá nunca se visiten.
- Facilita organizar aplicaciones con muchas secciones.
No obstante, no siempre es necesario aplicarlo a todos los componentes. En una aplicación pequeña, la mejora puede ser poco relevante y una estructura demasiado fragmentada puede añadir complejidad.
Por ejemplo, en lugar de importar directamente una vista en el fichero de rutas:
Cargar vista
import ProductosView from '../views/ProductosView.vue'
import ProductosView from '../views/ConfiguracionView.vue'
Podemos definir las rutas de esta manera:
Cargar vista con promesa
const routes = [
//...
{
path: '/productos',
name: 'productos',
component: () => import('../views/ProductosView.vue')
},
{
path: '/configuracion',
name: 'configuracion',
component: () => import('../views/ConfiguracionView.vue')
}
]
La función devuelve una promesa que permite cargar el componente cuando sea necesario y cada vista puede cargarse cuando el usuario accede a ella. Con Vite, esta técnica permite generar fragmentos de código separados durante la compilación.
Consumo de APIs y operaciones asíncronas
46 Introducción
Hasta ahora hemos aprendido a crear componentes Vue, gestionar la reactividad, comunicar componentes entre sí y organizar una aplicación mediante Vue Router.
Sin embargo, muchas aplicaciones necesitan trabajar con información que no está almacenada directamente en el código del navegador. Por ejemplo, una tienda necesita consultar sus productos, una aplicación educativa necesita recuperar los datos del alumnado y un sistema de gestión necesita guardar los cambios realizados por sus usuarios.
Para resolver estas situaciones, las aplicaciones Vue suelen comunicarse con un servidor mediante una API.
47 Aplicaciones Vue y APIs REST
Una API (Application Programming Interface) es una interfaz que permite que diferentes programas se comuniquen entre sí. En el desarrollo web, una API puede permitir que una aplicación cliente solicite información o envíe datos a un servidor.
Por ejemplo, una aplicación de gestión de productos podría utilizar las siguientes operaciones:
| Operación | Método HTTP | URL de ejemplo |
|---|---|---|
| Obtener todos los productos | GET | /api/productos |
| Obtener un producto | GET | /api/productos/15 |
| Crear un producto | POST | /api/productos |
| Sustituir un producto completo | PUT | /api/productos/15 |
| Modificar parcialmente un producto | PATCH | /api/productos/15 |
| Eliminar un producto | DELETE | /api/productos/15 |
Estas direcciones son ejemplos. Para que las peticiones funcionen, debe existir un servidor que implemente los correspondientes endpoints.
47.1 ¿Qué es una API REST?
REST es un estilo arquitectónico utilizado para diseñar servicios que trabajan con recursos mediante HTTP. En una API REST, los recursos se identifican habitualmente mediante URL y se utilizan los métodos HTTP para expresar las operaciones que se quieren realizar.
Por ejemplo, /api/productos/15 puede identificar un producto concreto. El método GET solicita información, mientras que DELETE solicita eliminar el recurso identificado por esa URL.
REST no obliga a que todas las API utilicen exactamente las mismas convenciones, pero las operaciones CRUD suelen seguir este esquema:
- Create: crear recursos.
- Read: consultar recursos.
- Update: actualizar recursos.
- Delete: eliminar recursos.
47.2 Arquitectura cliente-servidor
En una aplicación Vue, el navegador actúa como cliente. El servidor recibe las peticiones, realiza las operaciones necesarias y devuelve una respuesta con los datos correspondientes.
Vue se encarga de representar la información y reaccionar a sus cambios. La API es la que proporciona o almacena los datos en el servidor.
Aunque podemos mantener datos temporales en el navegador, eso no significa que estén guardados permanentemente en el servidor.
48 fetch() desde Vue
JavaScript proporciona la función fetch() para realizar peticiones HTTP, que devuelve una promesa que se resuelve cuando se obtiene una respuesta HTTP. Después podemos leer el cuerpo de esa respuesta y transformarlo, por ejemplo, en un objeto JavaScript mediante response.json().
Un detalle importante es que fetch() no rechaza automáticamente la promesa cuando el servidor responde con estados como 404 o 500. Debemos comprobar la propiedad response.ok para detectar las respuestas HTTP que indican un error.
48.1 Primera petición
Podemos realizar una petición desde un componente Vue:
Realizar petición a API
<script setup>
import { ref, onMounted } from 'vue'
const mensaje = ref('Cargando información...')
onMounted(async () => {
try {
const respuesta = await fetch('/api/mensaje')
if (!respuesta.ok) {
throw new Error('No se pudo obtener el mensaje')
}
const datos = await respuesta.json()
mensaje.value = datos.mensaje
} catch (error) {
mensaje.value = error.message
}
})
</script>
<template>
<p>{{ mensaje }}</p>
</template>
En este ejemplo utilizamos varias herramientas que ya conocemos.
- ref() crea una referencia reactiva para almacenar el mensaje.
- onMounted() ejecuta una función cuando el componente se ha montado.
- fetch() realiza la petición HTTP.
- await permite esperar el resultado de una promesa dentro de una función async.
- response.ok permite comprobar si la respuesta HTTP indica éxito.
- response.json() interpreta el cuerpo de la respuesta como JSON.
- La asignación a mensaje.value actualiza la interfaz gracias a la reactividad de Vue.
El ejemplo presupone que /api/mensaje devuelve una respuesta JSON similar a esta:
Respuesta de la API
{
"mensaje": "Bienvenido a la aplicación"
}
Si la API no existe o no devuelve el formato esperado, tendremos que gestionar el error correspondiente.
48.2 ¿Por qué utilizamos onMounted()?
Cuando un componente se muestra, normalmente queremos iniciar la carga de los datos necesarios para representarlo. El hook onMounted() permite ejecutar código cuando el componente ya se ha montado.
Por ejemplo, podemos cargar el listado de productos al acceder a la vista:
Respuesta de la API
onMounted(() => {
cargarProductos()
})
La función cargarProductos() contendrá la lógica necesaria para realizar la petición.
No todas las peticiones deben ejecutarse en onMounted(). Algunas deben producirse cuando el usuario pulsa un botón, envía un formulario o cambia un filtro. La ubicación de la petición depende del comportamiento que necesitemos implementar.
49 Peticiones GET
El método HTTP GET se utiliza habitualmente para consultar información del servidor. Supongamos una API que devuelve un listado de productos mediante una respuesta JSON como la siguiente:
Respuesta de la API
[
{
"id": 1,
"nombre": "Teclado",
"precio": 29.99
},
{
"id": 2,
"nombre": "Ratón",
"precio": 14.5
}
]
Podemos representar esos productos en una lista utilizando Vue, para ello tenemos el siguiente componente:
Parte script
<script setup>
import { ref, onMounted } from 'vue'
const productos = ref([])
const cargando = ref(false)
const error = ref('')
async function cargarProductos() {
cargando.value = true
error.value = ''
try {
const respuesta =
await fetch('/api/productos')
if (!respuesta.ok) {
throw new Error(
`Error: ${respuesta.status}`
)
}
productos.value =
await respuesta.json()
} catch (err) {
error.value = err.message
} finally {
cargando.value = false
}
}
onMounted(cargarProductos)
</script>
Plantilla
<template>
<section>
<h1>Productos</h1>
<p v-if="cargando">
Cargando productos...
</p>
<p v-else-if="error">
Error al cargar: {{ error }}
</p>
<p v-else-if="productos.length === 0">
No hay productos disponibles.
</p>
<ul v-else>
<li
v-for="producto in productos"
:key="producto.id"
>
{{ producto.nombre }}:
{{ producto.precio }} €
</li>
</ul>
<button @click="cargarProductos">
Recargar productos
</button>
</section>
</template>
En este componente tenemos tres estados principales:
- cargando: indica si se está realizando la petición.
- error: contiene un mensaje cuando se produce un error.
- productos: almacena los datos recibidos.
Vue utiliza estos valores para decidir qué parte de la plantilla debe mostrar.
49.1 ¿Por qué utilizamos finally?
El bloque finally se ejecuta tanto si la operación termina correctamente como si se produce un error, y por eso es adecuado para desactivar el indicador de carga:
Uso de “finally”
finally {
cargando.value = false
}
Si no restableciéramos este valor cuando se produce un error, la interfaz podría quedarse mostrando indefinidamente el mensaje de carga.
49.2 Evitar peticiones innecesarias
Si el usuario pulsa repetidamente el botón de recarga, podrían iniciarse varias peticiones simultáneas. En aplicaciones reales podemos deshabilitar temporalmente el botón:
Botón que se deshabilita
<button @click="cargarProductos" :disabled="cargando" >
Recargar productos
</button>
También debemos tener en cuenta que una petición iniciada anteriormente podría terminar después que otra más reciente. En determinados escenarios necesitaremos cancelar peticiones o controlar qué respuesta debe actualizar el estado.
50 Peticiones POST, PUT, PATCH y DELETE
Además de consultar información, una aplicación suele necesitar crear, modificar y eliminar recursos. Para ello utilizamos diferentes métodos HTTP.
50.1 Crear un recurso con POST
El método POST se utiliza habitualmente para crear un recurso. Con fetch() podemos enviar un objeto JavaScript convertido a JSON:
Función para crear producto
async function crearProducto(producto) {
const respuesta = await fetch('/api/productos', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(producto)
})
if (!respuesta.ok) {
throw new Error('No se pudo crear el producto')
}
return respuesta.json()
}
La propiedad method indica el método HTTP utilizado. La cabecera Content-Type informa al servidor de que el cuerpo de la petición contiene JSON, y JSON.stringify() transforma el objeto JavaScript en una cadena JSON.
El servidor deberá interpretar los datos, validar el contenido y realizar la operación correspondiente.
La validación del cliente mejora la experiencia de usuario, pero el servidor debe volver a validar los datos antes de guardarlos.
50.2 Modificar un recurso con PUT
El método PUT se utiliza habitualmente para sustituir la representación de un recurso.
Función para actualizar producto
async function sustituirProducto(id, producto) {
const respuesta = await fetch(`/api/productos/${id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(producto)
})
if (!respuesta.ok) {
throw new Error('No se pudo actualizar el producto')
}
return respuesta.json()
}
En este ejemplo, el identificador del producto forma parte de la URL. El significado exacto de la operación depende del contrato de la API. En un diseño convencional, PUT representa la sustitución de la representación del recurso, mientras que PATCH permite aplicar una modificación parcial.
50.3 Modificar parcialmente un recurso con PATCH
Si únicamente queremos modificar el precio de un producto, podemos utilizar PATCH:
Función para actualizar recurso
async function modificarPrecio(id, precio) {
const respuesta = await fetch(`/api/productos/${id}`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ precio })
})
if (!respuesta.ok) {
throw new Error('No se pudo modificar el precio')
}
return respuesta.json()
}
En este caso, enviamos únicamente el campo que queremos modificar. La forma exacta del cuerpo de un PATCH depende del formato de modificación que acepte la API. El ejemplo utiliza un objeto JSON convencional.
50.4 Eliminar un recurso con DELETE
El método DELETE solicita eliminar el recurso identificado por una URL.
Función para borrar recurso
async function eliminarProducto(id) {
const respuesta = await fetch(`/api/productos/${id}`, {
method: 'DELETE'
})
if (!respuesta.ok) {
throw new Error('No se pudo eliminar el producto')
}
}
No todas las API devuelven un cuerpo JSON después de eliminar un recurso. Algunas responden con 204 No Content, que indica que la operación se ha realizado correctamente y que no se devuelve contenido.
Por eso no debemos llamar automáticamente a respuesta.json() después de cada petición.
51 Estados de carga
Las operaciones de red tardan un tiempo variable. La respuesta puede llegar en mili-segundos o tardar varios segundos. Durante ese tiempo, la aplicación debe informar al usuario de lo que está sucediendo. Un patrón habitual consiste en utilizar una referencia reactiva para indicar si la operación está en curso, a veces en modo texto o con spinners
Una petición puede encontrarse en distintas situaciones:
| Estado | Descripción |
|---|---|
| Inicial | Todavía no se ha solicitado la información |
| Cargando | La petición está en curso |
| Éxito | La información se ha recibido correctamente |
| Vacío | La petición ha terminado, pero no hay resultados |
| Error | La operación no ha podido completarse correctamente |
Es importante distinguir entre un resultado vacío y un error.
Una API puede devolver un array vacío indicando que no hay productos disponibles, no que la petición haya fallado.
52 Gestión de errores
Las peticiones HTTP pueden fallar por diferentes motivos:
- El servidor devuelve un estado HTTP de error.
- El usuario pierde la conexión a Internet.
- El servidor no está disponible.
- La petición es bloqueada por una política de seguridad del navegador.
- La respuesta no contiene JSON válido cuando esperamos JSON.
- Los datos recibidos no tienen el formato esperado.
- La petición se cancela.
Por tanto, debemos evitar asumir que todas las peticiones terminarán correctamente.
52.1 Diferentes tipos de errores
Hay que diferenciar entre los posibles distintos errores que pueden suceder:
- Errores HTTP: El servidor responde, pero indica que la operación no se ha completado correctamente. Por ejemplo, 404 puede indicar que el recurso no existe y 50 que se ha producido un error interno del servidor.
- Errores de red: La petición no llega a completarse debido a un problema de conectividad, resolución del dominio, conexión o restricciones del navegador.
- Errores de interpretación: El servidor devuelve una respuesta, pero no podemos interpretar el cuerpo como esperábamos. Por ejemplo, response.json() puede fallar si el cuerpo no contiene JSON válido.
- Errores de aplicación: La respuesta es válida desde el punto de vista HTTP y sintáctico, pero los datos no cumplen las condiciones que necesita la aplicación.
52.2 Utilizar try…catch
Para gestionar los errores debemos usar la estructura try…catch. En una interfaz real, normalmente no basta con escribir el error en la consola, ya que el usuario no verá el mensaje y por tanto debemos informarle mediante un mensaje adecuado:
Parte script
<script setup>
try {
const resp = await fetch('/api/productos')
if (!resp.ok) {
throw new Error(`Error HTTP: ${resp.status}`)
}
const productos = await resp.json()
console.log(productos)
} catch (error) {
console.error('Error cargando products:', error)
}
</script>
Plantilla
<template>
<p v-if="error" role="alert">
Error al cargar los datos.
</p>
<!-- ... -->
</template>
El atributo role="alert" ayuda a que determinadas tecnologías de asistencia anuncien el mensaje cuando aparece.
52.3 No mostrar siempre el mensaje técnico
El mensaje original de un error puede contener detalles poco útiles para el usuario o información que no conviene mostrar directamente. Podemos registrar el error para facilitar la depuración y mostrar un mensaje más comprensible:
En una aplicación real, también podemos distinguir entre errores de validación, errores de autenticación, recursos inexistentes y fallos temporales del servidor.
53 Cancelación de peticiones
A veces una petición deja de ser necesaria antes de que termine. Por ejemplo:
- El usuario abandona una vista.
- El usuario cambia rápidamente de filtro.
- Se inicia una búsqueda nueva mientras la anterior sigue en curso.
- El componente se desmonta y ya no necesita los resultados.
JavaScript proporciona AbortController, que permite cancelar una petición fetch() mediante una señal, normalmente con un error de tipo AbortError.
Debemos distinguir una cancelación intencionada de un error de red real, por lo que en el siguiente ejemplo la petición se cancela cuando el componente se desmonta.
Parte script
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
const productos = ref([])
const error = ref('')
const controlador = new AbortController()
onMounted(async () => {
try {
const respuesta =
await fetch('/api/productos', {
signal: controlador.signal
})
if (!respuesta.ok) {
throw new Error('No cargan los productos')
}
productos.value = await respuesta.json()
} catch (err) {
if (err.name !== 'AbortError') {
error.value = err.message
}
}
})
onUnmounted(() => {
controlador.abort()
})
</script>
Plantilla
<template>
<p v-if="error">{{ error }}</p>
<ul>
<li
v-for="producto in productos"
:key="producto.id"
>
{{ producto.nombre }}
</li>
</ul>
</template>
La señal se crea fuera de la función que realiza la petición, por lo que puede utilizarse tanto para iniciar la solicitud como para cancelarla posteriormente.
53.1 ¿Qué ocurre si necesitamos repetir la petición?
Si queremos realizar nuevas peticiones con posibilidad de cancelación, normalmente crearemos un AbortController nuevo para cada operación. Un controlador que ya ha sido abortado no puede reutilizarse para iniciar una petición nueva.
Por ejemplo:
Repetir petición
let controladorActual
async function cargarDatos() {
controladorActual?.abort()
controladorActual = new AbortController()
try {
const respuesta = await fetch('/api/datos', {
signal: controladorActual.signal
})
if (!respuesta.ok) {
throw new Error('Error al cargar los datos')
}
return await respuesta.json()
} catch (error) {
if (error.name === 'AbortError') {
return
}
throw error
}
}
Esta técnica resulta útil para búsquedas en las que el usuario puede cambiar rápidamente el texto introducido. No obstante, en una aplicación real debemos coordinar la cancelación con los estados de carga y error, evitando que una operación antigua modifique la interfaz después de que se haya iniciado una nueva.
54 Componentes dependientes de datos externos
En una aplicación Vue, un componente puede necesitar información externa para representar su contenido. Por ejemplo, un componente ListaProductos.vue puede recibir los productos mediante una prop:
Parte script
<script setup>
defineProps({
productos: {
type: Array,
required: true
}
})
</script>
Plantilla
<template>
<ul>
<li
v-for="producto in productos"
:key="producto.id"
>
{{ producto.nombre }}
</li>
</ul>
</template>
Este componente no necesita saber de dónde proceden los productos, pueden venir de una API, de un archivo local o de otro componente. Su responsabilidad consiste en representar los datos recibidos.
54.1 Separar la carga de los datos de su representación
Normalmente deberíamos crear un componente padre encargado de consultar la API, ya que el componente hijo quizá pueda ser reutilizado por ser agnóstico al tipo de listado que debe dibujar.
Componente padre
<script setup>
import { ref, onMounted } from 'vue'
import ListaProductos
from '../components/ListaProductos.vue'
const productos = ref([])
const cargando = ref(false)
const error = ref('')
async function cargarProductos() {
cargando.value = true
error.value = ''
try {
const respuesta = await fetch('/api/productos')
if (!respuesta.ok) {
throw new Error('Error al cargar productos')
}
productos.value = await respuesta.json()
} catch (err) {
error.value = err.message
} finally {
cargando.value = false
}
}
onMounted(cargarProductos)
</script>
Componente padre
<template>
<h1>Catálogo</h1>
<p v-if="cargando">
Cargando...
</p>
<p v-else-if="error">
{{ error }}
</p>
<ListaProductos
v-else
:productos="productos"
/>
</template>
Esta separación tiene varias ventajas:
- ListaProductos puede reutilizarse en diferentes partes de la aplicación.
- La lógica de carga no se mezcla necesariamente con la representación de la lista.
- Podemos probar cada componente de forma independiente.
- El componente visual no necesita conocer los detalles de la API.
Esta organización no es una regla obligatoria para todas las aplicaciones, pero resulta útil cuando la interfaz empieza a crecer.
55 Creación de un servicio para una API
Cuando una aplicación tiene varios componentes que realizan peticiones al mismo servidor, repetir la misma lógica puede generar código difícil de mantener. Por ejemplo, cada componente podría tener su propio fetch(), sus propias comprobaciones de errores y sus propias cabeceras. Una alternativa consiste en centralizar la comunicación con la API en un módulo JavaScript. A este tipo de módulo se le suele llamar servicio o capa de acceso a datos.
Para evitar repetir código de peticiones a una API, lo ideal es crear un servicio para ello.
Entre las ventajas al crear un servicio, podemos destacar:
- Evitamos duplicar peticiones.
- Centralizamos las URL y las cabeceras.
- Podemos modificar la comunicación con el servidor sin cambiar todos los componentes.
- Facilitamos las pruebas y el mantenimiento.
- Podemos compartir funciones entre diferentes vistas.
En proyectos pequeños, puede ser suficiente con unas pocas funciones. En proyectos grandes, esta capa puede crecer para incluir autenticación, transformación de datos, gestión de errores y otros aspectos comunes.
Para guardar los servicios creados, podemos crear un directorio src/services para así tenerlos ordenados.
55.1 Crear el servicio
Para crear el servicio debemos crear un fichero productosService.js que contendrá las funciones encargadas de comunicarse con la API.
Servicio para productos
const API_URL = '/api/productos'
async function comprobarRespuesta(respuesta) {
if (!respuesta.ok) {
throw new Error(`Error HTTP: ${respuesta.status}`)
}
return respuesta
}
export async function obtenerProductos() {
const respuesta = await fetch(API_URL)
await comprobarRespuesta(respuesta)
return respuesta.json()
}
export async function obtenerProducto(id) {
const respuesta = await fetch(`${API_URL}/${id}`)
await comprobarRespuesta(respuesta)
return respuesta.json()
}
export async function crearProducto(producto) {
const respuesta = await fetch(API_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(producto)
})
await comprobarRespuesta(respuesta)
return respuesta.json()
}
export async function eliminarProducto(id) {
const respuesta = await fetch(`${API_URL}/${id}`, {
method: 'DELETE'
})
await comprobarRespuesta(respuesta)
}
En este módulo centralizamos las operaciones relacionadas con los productos, y de esta manera, los componentes pueden importar las funciones que necesitan sin tener que repetir los detalles de cada petición.
El ejemplo de crearProducto() presupone que la API devuelve un cuerpo JSON después de crear el recurso. Si la API responde con 204 No Content u otra respuesta sin cuerpo, debemos adaptar la función para no intentar interpretar un JSON inexistente.
55.2 Utilizar el servicio desde un componente
A la hora de querer usar un servicio creado desde un componente, lo primero que tendremos que hacer es pensar qué necesitamos importar y especificar la función y el fichero.
Parte script
<script setup>
import { ref, onMounted } from 'vue'
import { obtenerProductos }
from '../services/productosService.js'
const productos = ref([])
const cargando = ref(false)
const error = ref('')
async function cargarProductos() {
cargando.value = true
error.value = ''
try {
productos.value =
await obtenerProductos()
} catch (err) {
console.error(err)
error.value = 'Error con productos.'
} finally {
cargando.value = false
}
}
onMounted(cargarProductos)
</script>
Plantilla
<template>
<section>
<h1>Productos</h1>
<p v-if="cargando">
Cargando productos...
</p>
<p v-else-if="error"
role="alert"
>
{{ error }}
</p>
<ul v-else>
<li
v-for="prod in productos"
:key="prod.id"
>
{{ prod.nombre }}
</li>
</ul>
</section>
</template>
El componente ya no conoce la URL exacta ni cómo se construye la petición. Únicamente utiliza la función obtenerProductos().
Estado global con Pinia
56 El problema del estado compartido
En una aplicación Vue, cada componente puede mantener sus propios datos reactivos mediante ref() o reactive(). Esta solución funciona bien cuando los datos solo se utilizan dentro de un componente.
Sin embargo, en una aplicación real es habitual que varios componentes necesiten acceder a la misma información. Por ejemplo, una aplicación de comercio electrónico puede tener:
- Un componente que muestra el catálogo de productos.
- Un componente que muestra los detalles de un producto.
- Un componente que muestra el número de productos del carrito.
- Un componente que muestra el contenido del carrito.
- Un componente que permite consultar el perfil del usuario.
Todos estos componentes pueden necesitar consultar o modificar datos compartidos, como el carrito de la compra o la información del usuario autenticado.
56.1 El problema de la comunicación entre componentes
Vue permite comunicar componentes mediante props y eventos personalizados. No obstante, cuando los componentes están muy separados en la jerarquía, puede ser necesario transmitir datos a través de varios componentes intermedios.
Imaginemos la siguiente estructura de componentes en un proyecto:
Estructura de componentes
App.vue
├── Cabecera.vue
│ └── CarritoResumen.vue
└── Principal.vue
└── Productos.vue
└── Producto.vue
Si el componente Producto.vue permite añadir productos al carrito y CarritoResumen.vue debe mostrar el número de unidades, ambos componentes necesitan compartir información.
Sin una solución de estado compartido, podríamos tener que transmitir datos y eventos a través de varios componentes intermedios, aunque estos no utilicen directamente la información. Esta situación puede hacer que el código resulte difícil de mantener.
Este problema suele denominarse prop drilling: transmitir propiedades a través de varios niveles de componentes hasta llegar al componente que realmente las necesita.
56.2 Posibles soluciones
Existen varias formas de compartir información en Vue:
- Props y eventos: adecuados cuando la comunicación entre componentes es sencilla. <!– FIXME: no se habla de nada hasta ahora de provide() e inject
- provide() e inject(): permiten compartir datos entre un componente y sus descendientes sin transmitirlos manualmente por todos los niveles intermedios. –>
- Un estado reactivo compartido: permite centralizar determinados datos en un módulo JavaScript.
- Pinia: proporciona una solución estructurada para gestionar el estado compartido de una aplicación.
Pinia facilita la organización de los datos, las operaciones que los modifican y la lógica que varios componentes necesitan utilizar.
57 Estado local frente a estado global
El estado de una aplicación es el conjunto de datos que pueden cambiar durante su ejecución y que determinan qué información se muestra o cómo se comporta la interfaz. En Vue podemos distinguir entre estado local y estado global.
57.1 Estado local
El estado local pertenece a un componente y se utiliza principalmente dentro de él. Por ejemplo, el estado que indica si un menú desplegable está abierto o cerrado no suele necesitar compartirse con otros componentes.
Componente estado local
<script setup>
import { ref } from 'vue'
const menuAbierto = ref(false)
function alternarMenu() {
menuAbierto.value = !menuAbierto.value
}
</script>
<template>
<button @click="alternarMenu">
{{ menuAbierto ? 'Cerrar menú' : 'Abrir menú' }}
</button>
<nav v-if="menuAbierto">
<a href="/">Inicio</a>
<a href="/contacto">Contacto</a>
</nav>
</template>
En este caso, menuAbierto solo tiene sentido dentro del componente. No sería necesario almacenarlo en un estado global.
57.2 Estado global
El estado global contiene información que debe estar disponible para varios componentes independientes. Algunos ejemplos son:
- El usuario que ha iniciado sesión.
- El contenido del carrito de la compra.
- Las preferencias de visualización.
- Los filtros que deben compartirse entre distintas vistas.
- Los datos que varios componentes necesitan consultar y modificar.
Cuando diferentes componentes utilizan los mismos datos, conviene centralizar su gestión para evitar duplicaciones y mantener un comportamiento coherente.
57.2.1 ¿Qué información debe almacenarse en un estado global?
No es necesario convertir todos los datos de una aplicación en estado global. Hacerlo puede complicar innecesariamente el proyecto. Como criterio general:
- Si los datos solo afectan a un componente, utiliza estado local.
- Si varios componentes próximos necesitan comunicarse, utiliza props y eventos cuando resulte suficiente.
- Si componentes independientes necesitan compartir y modificar los mismos datos, considera utilizar un estado global.
- Si los datos deben conservarse después de recargar la página, necesitarás además algún mecanismo de persistencia.
Es importante recordar que estado global no significa almacenamiento permanente. El estado global permite compartir datos durante la ejecución de la aplicación, pero no garantiza que se conserven al recargar la página.
58 Pinia
Pinia es una biblioteca de gestión del estado diseñada para aplicaciones Vue. Pinia permite definir almacenes de estado, denominados stores, que centralizan los datos y la lógica relacionada con ellos.
Cada store puede contener:
- State: los datos reactivos que se desean compartir.
- Getters: propiedades calculadas a partir del estado.
- Actions: funciones que permiten modificar el estado y ejecutar operaciones relacionadas con él.
Es la solución recomendada actualmente para organizar el estado compartido en proyectos Vue modernos.
Por ejemplo, una aplicación podría tener un store para el usuario (src/stores/usuario.js), otro para los productos (src/stores/productos.js) y otro para el carrito de la compra (src/stores/carrito.js).
La carpeta stores es una convención habitual para organizar los almacenes de Pinia. No es obligatoria, pero ayuda a mantener una estructura clara.
Podemos destacar las siguientes ventajas:
- Centraliza el estado compartido.
- Permite reutilizar la misma información desde distintos componentes.
- Mantiene la reactividad de los datos.
- Facilita la separación entre la interfaz y la lógica de negocio.
- Permite definir operaciones síncronas y asíncronas.
- Se integra con las herramientas de desarrollo de Vue.
- Facilita las pruebas y el mantenimiento de aplicaciones grandes.
Pinia no sustituye a la reactividad de Vue: se apoya en ella para que los componentes se actualicen cuando cambia el estado compartido.
Anexo: Mise-en-place developer tools
59 Introducción a Mise-en-place
Mise-en-place es una herramienta para gestionar el entorno de desarrollo de un proyecto. Permite instalar y seleccionar diferentes versiones de herramientas como Node.js, Python, Ruby, Go o Bun, de manera que cada proyecto pueda utilizar las versiones que necesita sin depender de la configuración global del sistema.
Una de sus principales características es que la configuración puede almacenarse dentro del propio proyecto, normalmente en un fichero mise.toml. De esta forma, podemos indicar, por ejemplo, que un proyecto Vue.js debe utilizar una determinada versión de Node.js. Al trabajar con el proyecto, Mise se encarga de utilizar las versiones especificadas, evitando tener que cambiar manualmente la versión instalada en el sistema.
Además de gestionar versiones de herramientas, Mise puede encargarse de otras tareas relacionadas con el entorno de desarrollo, como:
- Definir variables de entorno: ideal para diferenciar entornos de desarrollo, test o producción, indicando contra qué base de datos coger los datos, o el número de hilos a ejecutar.
- Crear comandos y/o tareas: permite ejecutar comandos/tareas para dejar el entorno listo para el desarrollo, o para pasar a producción.
- Integrarlo en nuestro CI (continuous integration): permite ejecutar acciones durante las acciones automatizadas de CI como en GitHub Actions o GitLab CI/CD.
- Integración con IDE: permite integrar mise y las herramientas con nuestro IDE favorito.
- Gestión de ficheros de configuración dotfiles: aparte de la integración con herramientas, en Septiembre del 2026 anunciaron la gestión de ficheros de configuración muy habitual en GNU/Linux y MacOS denominada dotfiles, para así poder gestionar esta configuración si contamos con distintas máquinas durante el desarrollo, y que todas tengan la misma configuración.
Es por eso que no debemos entender a Mise únicamente como un sustituto de herramientas como nvm, sino como un gestor del entorno de desarrollo.
mise es un gestor del entorno de desarrollo, que nos facilita las tareas del día a día.
59.1 ¿Por qué utilizar un gestor de herramientas?
Un desarrollador de software es habitual que trabaje en distintos proyectos, y cada uno de ellos hará uso de distintas herramientas y en versiones distintas. Una aplicación antigua podría depender de una versión concreta de Node.js mientras que un proyecto nuevo, por ejemplo desarrollado con Vue.js, puede requerir una versión más reciente. Instalar y cambiar manualmente estas versiones puede provocar conflictos y dificultar la configuración del equipo de desarrollo.
Un gestor como Mise permite asociar las herramientas y versiones necesarias al propio proyecto. Esto facilita que todos los desarrolladores trabajen con un entorno idéntico y reduce los problemas causados por utilizar versiones diferentes de Node.js, npm u otras herramientas. La configuración puede almacenarse además junto al código fuente en el sistema de control de versiones, como Git, facilitando la reproducción del entorno en otro equipo.
Un gestor de herramientas facilita que todos los desarrolladores trabajen con un entorno idéntico.
59.2 Instalación de Mise
Mise-en-place está disponible para los principales sistemas operativos y puede instalarse mediante diferentes métodos, tal como se explica en la documentación. Dependiendo del sistema operativo, tendremos diferentes opciones:
- GNU/Linux: algunas distribuciones permiten la instalación mediante el repositorio propio o añadiendo uno nuevo.
- Windows: permite instalarlo con scoop, winget o chocolatey.
- MacOS: se puede instalar con brew.
Para GNU/Linux y MacOS también se puede usar el instalador proporcionado por el proyecto:
Instalar mise
ruben@archy:~$ curl https://mise.run | sh
mise: installed successfully to /home/ruben/.local/bin/mise
mise: run the following to activate mise in your shell:
echo "eval \"\$(/home/ruben/.local/bin/mise activate bash)\"" >> ~/.bashrc
mise: run `mise doctor` to verify this is set up correctly
El instalador realiza las siguientes tareas:
- Descarga el binario y lo guarda dentro del directorio $HOME/.local/bin/.
- Nos indica que tenemos que ejecutar un comando para activar Mise
- El comando a ejecutar lo que hace guardar el comando para activar Mise, mise activate bash, dentro del fichero de configuración de nuestra SHELL.
- Normalmente el fichero .bashrc o .zshrc.
- En el ejemplo anterior se ha ejecutado en una terminal bash.
- Existen comandos para zsh, fish y powershell de Windows.
- El comando a ejecutar lo que hace guardar el comando para activar Mise, mise activate bash, dentro del fichero de configuración de nuestra SHELL.
Para asegurar que funciona podemos recargar la configuración de la terminal, o cerrarla y abrir una nueva y ejecutar:
Comprobar instalación
ruben@archy:~$ mise doctor
59.3 Desinstalación
Si en algún momento queremos desinstalar Mise y todo lo que hayamos descargado con él, si lo hemos instalado con el instalador explicado en el apartado anterior, la manera más sencilla sería:
Comprobar instalación
ruben@archy:~$ mise implode
Después borrar la línea de activación de Mise que hayamos añadido en nuestro fichero de configuración .bashrc o .zshrc (no el fichero completo).
60 Herramientas
Mise permite instalar cientos de herramientas, no sólo de desarrollo, por lo que a través de este gestor podemos centralizar muchas herramientas que podamos necesitar en nuestro sistema.
Para explicar cómo instalar una herramienta, gestionarla y demás operaciones vamos a usar Node.js como ejemplo.
60.1 Instalar Node.js
Una de las principales utilidades de mise en un proyecto JavaScript es gestionar las versiones de Node.js. En lugar de instalarlo directamente mediante el gestor de paquetes del sistema, podemos dejar que mise se encargue de instalar y seleccionar la versión que necesita cada proyecto.
Para consultar las versiones de Node.js que podemos instalar utilizaremos:
Comprobar versiones de Node
ruben@archy:~$ mise ls-remote node
Este comando muestra las versiones disponibles para su instalación. Nos aparecerá, dependiendo del proyecto, versiones en formato X.Y.Z. Podemos elegir qué versión o sub-versión instalar y dependiendo de lo indicado, mise elegirá la última dentro de la rama indicada.
Instalar distintas versiones de Node
ruben@archy:~$ mise install node@24.18
✓ installed 1 tool in 2.9s: node@24.18.1
ruben@archy:~$ mise install node@24
✓ installed 1 tool in 2.7s: node@24.21.0
ruben@archy:~$ mise install node@latest
✓ installed 1 tool in 3.1s: node@26.8.2
ruben@archy:~$ mise ls
Tool Version Source Requested
node 24.18.1
node 24.21.0
node 26.8.2
En el ejemplo anterior se han instalado tres versiones de Node:
- 24.18: se ha indicado la sub-version, por lo tanto ha cogido la versión más nueva de esa numeración, siendo la 24.18.1
- 24: al no indicar ninguna sub-versión, ha cogido la última versión disponible, en este momento la 24.21.0
- latest: se descarga la última versión disponible, en este momento la 26.8.2
Tal como se puede ver, se pueden instalar distintas versiones de la misma herramienta, lo que es ideal para hacer pruebas a la hora de querer actualizar de versión y testear nuestro proyecto.
Las herramientas descargadas por defecto se instalan en el directorio del usuario, concretamente en GNU/Linux y MacOS en ~/.local/share/mise. Por lo tanto, si un equipo lo usan distintos desarrolladores, cada usuario puede tener sus herramientas instaladas sin molestar al resto.
Instalar distintas versiones de Node
ruben@archy:~$ ls ~/.local/share/mise
downloads installs migrations shims
60.2 Seleccionar versión de Node.js
Hasta ahora lo único que hemos hecho ha sido instalar la herramienta indicada, pero no se ha configurado para poder ser usada. Para ello debemos:
Seleccionar versión
ruben@archy:~$ mise use node@24
mise ~/.config/mise/config.toml tools: node@24.21.0
ruben@archy:~$ mise ls
Tool Version Source Requested
node 24.18.1
node 24.21.0 ~/.config/mise/config.toml 24
node 26.8.2
Con el comando anterior se ha indicado que se use la versión 24 (sin especificar sub-versión), así que coge la última y realiza la configuración en el fichero ~/.config/mise/config.toml porque hemos ejecutado el comando desde nuestra $HOME. Ahora ya se puede usar Node.js dentro de nuestra terminal:
Comprobar versión
ruben@archy:~$ node -v
v24.21.0
Si seleccionamos una herramienta en cualquier otro directorio nos creará un fichero local mise.toml y ahí se indicará la versión a utilizar. De esta manera, el fichero creado lo podemos añadir a nuestro repositorio de proyecto:
Seleccionar versión en un proyecto
ruben@archy:~/repositorios/proy1 $ mise use node@26
mise ~/repositorios/proy1/mise.toml tools: node@26.8.2
ruben@archy:~/repositorios/proy1 $ cat mise.toml
[tools]
node = "26"
Si queremos modificar la versión global debemos hacer lo siguiente:
Usar versión global
ruben@archy:~/repositorios/proy1 $ mise use --global node@24
De esta manera tenemos dos versiones de Node.js disponibles:
- La versión global: será la v24, que será usada en cualquier parte de nuestro sistema.
- La versión por directorio: al entrar a un directorio que contenga un fichero mise.toml será leído y cambiará a la versión indicada en el proyecto.
Podemos usar una versión para la configuración global y otras distintas para cada proyecto.
60.3 Borrar versión de Node.js
Si tenemos alguna versión de una herramienta que ya no vayamos a usar y queremos eliminarla podemos hacer lo siguiente:
Eliminar versión
ruben@archy:~/repositorios/proy1 $ mise uninstall node@24.18
✓ removed 1 tool in 199ms: node@24.18.1
ruben@archy:~/repositorios/proy1 $ mise ls
Tool Version Source Requested
node 24.21.0
node 26.8.2 ~/repositorios/proy1/mise.toml 26
Así libramos espacio de nuestro sistema.
61 Entornos de proyecto
Una de las características más interesantes de mise es la posibilidad de guardar la configuración de las herramientas de un proyecto en un fichero llamado mise.toml tal como hemos visto anteriormente. Este fichero utiliza el formato TOML y permite indicar qué versiones de las herramientas deben utilizarse en ese proyecto.
61.1 Fichero mise.toml
En un proyecto podemos crear el fichero mise.toml en el que podemos especificar las herramientas y las versiones que necesitamos.
Fichero mise.toml
[tools]
node = "24"
bun = "1.4"
De esta forma, cualquier persona que obtenga el proyecto puede saber qué versión principal de Node.js se espera utilizar. mise leerá automáticamente este fichero cuando trabajemos dentro del directorio del proyecto y cambiará a las versiones correspondientes de las herramientas.
61.2 Instalar herramientas definidas
Si nos descargamos un proyecto que contiene un fichero mise.toml debemos asegurar que tenemos instaladas las versiones especificadas. Para ello podemos ejecutar el siguiente comando:
Instalar herramientas necesarias
ruben@archy:~/repositorios/proy1 $ mise install
✓ installed 1 tool · 1 already installed in 1.4s: bun@1.4.2
En este caso se ha descargado una herramienta que no teníamos instalada (bun) mientras que otra ya lo estaba. Podemos comprobar las herramientas utilizadas en el proyecto de la siguiente manera:
Comprobar herramientas usadas
ruben@archy:~/repositorios/proy1 $ mise current
node 26.8.2
bun 1.4.2
62 Resumen de comandos
A continuación una lista de los comandos más utilizados con mise:
| Comando | Función |
|---|---|
mise ls |
Mostrar herramientas instaladas y gestionadas |
mise ls --current |
Mostrar las herramientas seleccionadas para el proyecto |
mise ls-remote node |
Consultar versiones disponibles de Node.js |
mise outdated |
Comprobar si existen actualizaciones |
mise config ls |
Mostrar los ficheros de configuración utilizados |
mise use node@24 |
Seleccionar Node.js 24 para el proyecto |
mise use --global node@24 |
Establecer Node.js 24 como versión global |
mise install |
Instalar las herramientas declaradas en la configuración |
mise install node@24 |
Instalar Node.js 24 sin declararlo en el proyecto |
mise uninstall node@22 |
Eliminar una versión instalada |
mise unuse node |
Eliminar Node.js de la configuración |
mise upgrade node |
Actualizar Node.js dentro de la versión solicitada |
mise exec node@22 -- node --version |
Ejecutar un comando con una versión concreta |
mise tasks ls |
Mostrar las tareas disponibles |
mise run dev |
Ejecutar una tarea del proyecto |
mise doctor |
Comprobar el estado de la instalación |