¡Saludos, desarrolladores y entusiastas de Vue.js! Prepárense para un viaje emocionante al corazón de la modernidad en el desarrollo frontend. Desde su lanzamiento, Vue 3 ha sido un hito, marcando una evolución significativa en cómo construimos interfaces de usuario. La API de Composición (Composition API
) fue el pilar de este cambio, ofreciendo una forma más flexible y robusta de organizar la lógica de los componentes. Pero, ¿qué pasaría si les dijera que la experiencia de usar la Composition API
ha sido mejorada aún más, llevándola a un nivel de elegancia y simplicidad sin precedentes?
Aquí es donde entra en juego script setup
. Esta característica, introducida en Vue 3.2, no es solo una adición más; es una verdadera revolución en la sintaxis que simplifica drásticamente la forma en que escribimos componentes de Vue, especialmente aquellos que aprovechan la Composition API
. Elimina el boilerplate, mejora la legibilidad y hace que la experiencia del desarrollador sea extraordinariamente fluida. Si aún no lo han adoptado, o si están buscando una inmersión profunda para maximizar su potencial, este tutorial es para ustedes. Nos sumergiremos en script setup
, desglosando sus fundamentos y construyendo una aplicación interactiva que demuestre su poder. ¡Prepárense para transformar su flujo de trabajo en Vue!
La Evolución del Componente Vue: De Opciones a Composición, y Ahora `script setup`

Para apreciar verdaderamente el valor de script setup
, es útil entender el camino que ha recorrido Vue.js.
Originalmente, Vue 2 nos introdujo a la Options API
, un enfoque declarativo donde la lógica de los componentes se organizaba en propiedades predefinidas como data
, methods
, computed
, watch
y lifecycle hooks
. Este método es intuitivo para componentes pequeños y medianos, pero a medida que las aplicaciones crecen y la lógica se vuelve más compleja, la Options API
puede llevar a lo que se conoce como "lógica fragmentada". Es decir, las características relacionadas pueden dispersarse por todo el componente, dificultando su comprensión y mantenimiento.
Vue 3, respondiendo a estas preocupaciones, introdujo la Composition API
. Esta API permite organizar la lógica de los componentes basándose en la funcionalidad, en lugar de las opciones. Permite extraer y reutilizar lógica reactiva a través de funciones componibles (composables
), lo que mejora enormemente la modularidad y la escalabilidad. La Composition API
se utiliza dentro de un método setup()
especial en el componente.
Sin embargo, incluso con la Composition API
, el método setup()
requería que explícitamente se hicieran disponibles las variables, funciones y propiedades computadas al template a través de una sentencia return
. Esto, aunque funcional, aún añadía un pequeño fragmento de boilerplate. Y aquí es donde script setup
brilla con luz propia.
Decodificando `script setup`: Una Declaración Más Clara y Concisa
script setup
es un "azúcar sintáctico" (syntactic sugar
) para la Composition API
dentro de los componentes Single File Components (SFCs) de Vue. En esencia, permite escribir la lógica de la Composition API
directamente en la sección <script setup>
de un componente, sin necesidad de definir un objeto de opciones o un método setup()
explícito. Todo lo que se declara dentro de <script setup>
se expone automáticamente al template.
Beneficios Clave de script setup
:
-
Menos Boilerplate: Adiós al
setup()
y a la declaración explícita dereturn { ... }
. Todas las variables, funciones e importaciones declaradas enscript setup
están automáticamente disponibles en el template. -
Mejor Rendimiento en Tiempo de Ejecución: El compilador de Vue puede optimizar los componentes
<script setup>
con mayor eficacia, lo que puede resultar en un rendimiento ligeramente mejor. -
Mejor Integración con TypeScript:
script setup
mejora la inferencia de tipos y simplifica el uso de TypeScript con Vue. -
Importaciones Automáticas de Componentes: Los componentes importados dentro de
<script setup>
no necesitan ser registrados explícitamente en una opcióncomponents
. Se registran automáticamente. -
Definición de Props y Emits Simplificada: Utiliza las funciones
defineProps
ydefineEmits
para una declaración más limpia y tipada de las propiedades y eventos de un componente.
Personalmente, considero que script setup
es una de las mejoras más significativas en la experiencia de desarrollo de Vue 3. La reducción de la verbosidad y la mejora en la claridad del código son palpables, haciendo que escribir componentes sea un verdadero placer.
Core Reactividad Building Blocks en `script setup`
Antes de sumergirnos en el código, revisemos rápidamente los pilares de la reactividad que usaremos extensivamente con script setup
.
-
ref()
: Se usa para crear una referencia reactiva a un valor. Es ideal para tipos de datos primitivos (números, cadenas, booleanos) y también puede usarse con objetos. Cuando se accede a unref
en elscript setup
, se hace directamente (ej.myRef.value
), pero en el template, Vue lo "desenvuelve" automáticamente (ej.{{ myRef }}
). -
reactive()
: Crea un objeto reactivo. A diferencia deref
,reactive
solo funciona con objetos y arrays, y el acceso a sus propiedades no requiere.value
. Es importante recordar quereactive
no puede reasignarse directamente; si se reasigna, la reactividad se pierde. -
computed()
: Permite crear una propiedad reactiva que se deriva de otra propiedad reactiva. Es una función pura: solo se recalcula cuando sus dependencias cambian, lo que lo hace muy eficiente. -
watch()
: Se utiliza para realizar efectos secundarios en respuesta a cambios en una fuente de datos reactiva. Puede observar uno o variosref
s oreactive
s, y soporta opciones comodeep
eimmediate
. -
onMounted()
,onUnmounted()
y otroslifecycle hooks
: Estas funciones nos permiten enganchar la lógica en diferentes puntos del ciclo de vida de un componente, como cuando el componente se ha montado en el DOM o antes de que se desmonte.
Pueden encontrar más detalles sobre estos conceptos en la documentación oficial de Vue sobre fundamentos de reactividad.
Hands-on Tutorial: Construyendo una Lista de Usuarios Reactiva con Búsqueda
Vamos a construir un componente sencillo pero funcional: una lista de usuarios que se obtiene de una API y permite buscar a los usuarios por nombre. Esto nos dará la oportunidad de usar ref
, computed
, watch
y onMounted
dentro de un <script setup>
.
Paso 1: Configuración del Proyecto
Si aún no tienen un proyecto Vue 3, pueden crearlo fácilmente usando create-vue
, la herramienta oficial de andamiaje:
npm init vue@latest
Sigan las indicaciones. Para este tutorial, pueden seleccionar "No" para TypeScript, JSX, Pinia, Vitest y Cypress, para mantenerlo lo más simple posible. Luego, naveguen a la carpeta del proyecto e instalen las dependencias:
cd <your-project-name>
npm install
npm run dev
Esto iniciará el servidor de desarrollo y podrán ver su aplicación en el navegador.
Paso 2: Creando el Componente `UserList` con `script setup`
Vamos a crear un nuevo componente llamado UserList.vue
dentro de la carpeta src/components
.
<!-- src/components/UserList.vue -->
<script setup>
import { ref, reactive, computed, watch, onMounted } from 'vue';
// 1. Declarar estados reactivos
const users = ref([]);
const searchTerm = ref('');
const isLoading = ref(true);
const error = ref(null);
// 2. Función para obtener usuarios de una API
const fetchUsers = async () => {
isLoading.value = true;
error.value = null; // Resetear error
try {
const response = await fetch('https://jsonplaceholder.typicode.com/users');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
users.value = await response.json();
} catch (err) {
console.error('Failed to fetch users:', err);
error.value = 'No se pudieron cargar los usuarios. Por favor, inténtelo de nuevo más tarde.';
} finally {
isLoading.value = false;
}
};
// 3. Propiedad computada para filtrar usuarios
const filteredUsers = computed(() => {
if (!searchTerm.value) {
return users.value;
}
const lowerCaseSearchTerm = searchTerm.value.toLowerCase();
return users.value.filter(user =>
user.name.toLowerCase().includes(lowerCaseSearchTerm) ||
user.email.toLowerCase().includes(lowerCaseSearchTerm)
);
});
// 4. Observar cambios en el término de búsqueda
watch(searchTerm, (newValue, oldValue) => {
console.log(`Término de búsqueda cambió de "${oldValue}" a "${newValue}"`);
// Aquí podríamos implementar un debounce si la búsqueda fuera costosa
});
// 5. Hook de ciclo de vida: obtener usuarios al montar el componente
onMounted(() => {
fetchUsers();
});
</script>
<template>
<div class="user-list-container">
<h2>Lista de Usuarios</h2>
<div class="search-input-wrapper">
<label for="search">Buscar usuario:</label>
<input
id="search"
type="text"
v-model="searchTerm"
placeholder="Buscar por nombre o email..."
/>
</div>
<p v-if="isLoading" class="loading-message">Cargando usuarios...</p>
<p v-else-if="error" class="error-message">{{ error }}</p>
<p v-else-if="filteredUsers.length === 0" class="no-results-message">
No se encontraron usuarios para su búsqueda.
</p>
<ul v-else class="user-list">
<li v-for="user in filteredUsers" :key="user.id" class="user-item">
<h3>{{ user.name }}</h3>
<p>Email: {{ user.email }}</p>
<p>Teléfono: {{ user.phone }}</p>
<p>Ciudad: {{ user.address.city }}</p>
<a :href="`mailto:${user.email}`" target="_blank" class="contact-link">Contactar</a>
</li>
</ul>
</div>
</template>
<style scoped>
.user-list-container {
max-width: 800px;
margin: 40px auto;
padding: 20px;
background-color: #f9f9f9;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
color: #333;
}
h2 {
text-align: center;
color: #2c3e50;
margin-bottom: 30px;
font-size: 2.2em;
}
.search-input-wrapper {
margin-bottom: 25px;
text-align: center;
}
.search-input-wrapper label {
font-size: 1.1em;
color: #555;
margin-right: 10px;
}
.search-input-wrapper input {
width: 70%;
padding: 12px 18px;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 1.05em;
box-shadow: inset 0 1px 3px rgba(0,0,0,0.06);
transition: border-color 0.3s ease, box-shadow 0.3s ease;
}
.search-input-wrapper input:focus {
border-color: #42b983;
box-shadow: 0 0 0 3px rgba(66, 185, 131, 0.2);
outline: none;
}
.loading-message, .error-message, .no-results-message {
text-align: center;
padding: 15px;
border-radius: 6px;
font-size: 1.1em;
margin-top: 20px;
}
.loading-message {
background-color: #e0f2f7;
color: #2196f3;
}
.error-message {
background-color: #ffe0e0;
color: #d32f2f;
border: 1px solid #ef9a9a;
}
.no-results-message {
background-color: #fff9c4;
color: #fbc02d;
}
.user-list {
list-style: none;
padding: 0;
margin-top: 30px;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 25px;
}
.user-item {
background-color: #ffffff;
border: 1px solid #e0e0e0;
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.user-item:hover {
transform: translateY(-5px);
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.1);
}
.user-item h3 {
color: #42b983;
margin-top: 0;
margin-bottom: 10px;
font-size: 1.6em;
border-bottom: 2px solid #e0e0e0;
padding-bottom: 8px;
}
.user-item p {
margin: 5px 0;
line-height: 1.6;
color: #666;
font-size: 0.95em;
}
.contact-link {
display: inline-block;
margin-top: 15px;
padding: 10px 15px;
background-color: #42b983;
color: white;
text-decoration: none;
border-radius: 5px;
font-weight: bold;
transition: background-color 0.3s ease;
}
.contact-link:hover {
background-color: #36a575;
}
</style>
Paso 3: Integrar `UserList` en `App.vue`
Ahora, para ver nuestro componente en acción, necesitamos importarlo y usarlo en src/App.vue
. Limpien el contenido existente de App.vue
y reemplácenlo con lo siguiente:
<!-- src/App.vue -->
<script setup>
import UserList from './components/UserList.vue';
</script>
<template>
<div id="app">
<UserList />
</div>
</template>
<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
color: #2c3e50;
margin-top: 60px;
}
</style>
Explicación del Código `UserList.vue`
-
import { ref, reactive, computed, watch, onMounted } from 'vue';
: Importamos las funciones necesarias de Vue.js. Gracias ascript setup
, estas funciones se pueden usar directamente sin calificarlas conthis
. -
const users = ref([]);
: Creamos una referencia reactivausers
que inicialmente es un array vacío. Aquí es donde almacenaremos la lista de usuarios obtenida de la API. Usamosref
porque el valor completo del arrayusers
puede ser reasignado. -
const searchTerm = ref('');
: Otra referencia reactiva para almacenar el texto que el usuario introduce en la barra de búsqueda. -
const isLoading = ref(true);
yconst error = ref(null);
: Referencias para manejar los estados de carga y error, cruciales para una buena experiencia de usuario. -
const fetchUsers = async () => { ... };
: Esta función asíncrona realiza la llamada a la API de JSONPlaceholder para obtener los datos de los usuarios. Envuelve la lógica en un bloquetry-catch
para manejar posibles errores de red o del servidor, y actualiza los estadosisLoading
yerror
apropiadamente. -
const filteredUsers = computed(() => { ... });
: Aquí está el corazón de nuestra lógica de búsqueda reactiva.computed
crea una nueva lista de usuarios (filteredUsers
) que se actualiza automáticamente cada vez queusers.value
osearchTerm.value
cambian. Si no hay término de búsqueda, devuelve todos los usuarios. De lo contrario, filtra los usuarios cuyo nombre o email incluyen elsearchTerm
(insensible a mayúsculas/minúsculas). Usarcomputed
es fundamental aquí para la eficiencia, ya que la función de filtrado solo se ejecuta cuando sus dependencias cambian. -
watch(searchTerm, (newValue, oldValue) => { ... });
: Utilizamoswatch
para observar elsearchTerm
. Cada vez que cambia, se ejecuta la función de callback. En este caso, simplemente registramos el cambio en la consola. Esto es un ejemplo sencillo; en una aplicación real,watch
podría usarse para guardar el término de búsqueda en el almacenamiento local, realizar una búsqueda en un servidor cuando el término alcanza una longitud mínima, o cualquier otro efecto secundario. -
onMounted(() => { fetchUsers(); });
: Este es unlifecycle hook
. La funciónfetchUsers()
se llama una vez que el componente ha sido montado en el DOM, asegurando que los usuarios se carguen tan pronto como el componente esté listo para mostrarse. Esto es equivalente almounted()
de la Options API. -
Template (
<template>
):- El input de búsqueda (
<input v-model="searchTerm" />
) está enlazado directamente a nuestrasearchTerm
reactiva. - La lista de usuarios (
<ul v-else class="user-list">
) itera sobrefilteredUsers
, lo que significa que siempre mostrará la lista de usuarios que coincide con el término de búsqueda actual. - También hemos añadido lógica condicional (
v-if
,v-else-if
,v-else
) para mostrar mensajes de carga, error o sin resultados, mejorando la experiencia del usuario.
- El input de búsqueda (
¡Y eso es todo! Con script setup
, todo el código de la Composition API
reside directamente en el <script setup>
, sin necesidad de envolverlo en un objeto setup
o retornar explícitamente las propiedades al template. Vue se encarga de todo el cableado por nosotros.
Para ver más ejemplos y profundizar, les recomiendo visitar la documentación de script setup
.
Mi Opinión: La Experiencia del Desarrollador Elevada
En mi experiencia, script setup
ha sido un verdadero punto de inflexión para la Composition API
. Si bien la Composition API
por sí sola ya era un avance en términos de organización de código y reutilización, el boilerplate de setup()
y la necesidad de retornar cada variable o función al template a veces hacía que los componentes fueran un poco má