El desarrollo web moderno avanza a un ritmo vertiginoso, y con cada nueva iteración de los frameworks de JavaScript, los desarrolladores buscan herramientas que optimicen su flujo de trabajo, mejoren la legibilidad del código y, en última instancia, permitan construir aplicaciones más robustas y escalables. Vue.js, conocido por su enfoque en la reactividad accesible y su curva de aprendizaje amigable, no es una excepción a esta evolución. Con el lanzamiento de Vue 3, una de las adiciones más significativas y bien recibidas ha sido la etiqueta <script setup>, una mejora sintáctica que transforma radicalmente la forma en que escribimos componentes. Si aún no te has sumergido por completo en esta característica, o si buscas una comprensión más profunda y práctica, este tutorial está diseñado para ti. Exploraremos cómo <script setup> no solo simplifica la estructura de tus componentes, sino que también desbloquea todo el potencial de la Composition API, llevándote a una nueva era de desarrollo Vue más limpio y eficiente. Prepárate para optimizar tu código y elevar tus habilidades con Vue 3.
¿Por qué <script setup>? Una revolución en la sintaxis
Desde la introducción de la Composition API en Vue 3, la forma de estructurar la lógica de los componentes cambió, ofreciendo una alternativa más potente y organizada al Options API. Sin embargo, la sintaxis inicial de la Composition API aún requería envolver toda la lógica dentro de una función setup(), la cual, a su vez, debía retornar un objeto con todas las propiedades y métodos que se querían exponer a la plantilla. Esto, aunque funcional, podía resultar en cierta verbosidad y en un anidamiento que algunos encontraban menos intuitivo.
Aquí es donde <script setup> entra en juego como un dulce alivio. Introducido en la versión 3.2, esta característica es una mejora en el tiempo de compilación que simplifica enormemente la escritura de componentes usando la Composition API. Básicamente, permite escribir el código de la Composition API directamente en el nivel raíz de <script>, eliminando la necesidad de la función setup() explícita y su retorno. Todos los imports, variables reactivas, funciones y propiedades computadas declaradas en <script setup> se exponen automáticamente a la plantilla del componente.
Simplicidad y legibilidad mejoradas
Uno de los mayores beneficios de <script setup> es la drástica reducción de la "boilerplate" o código repetitivo. Ya no necesitamos definir un objeto de retorno para setup(), lo que hace que el código sea más conciso y fácil de leer. La lógica del componente fluye de manera más natural, casi como si estuvieras escribiendo un script regular de JavaScript. Para mí, esta simplicidad es un factor clave en la adopción de nuevas características; si algo hace el código más claro, generalmente es una victoria.
Mayor eficiencia para el desarrollo
Al reducir la cantidad de código que hay que escribir y leer, los desarrolladores pueden centrarse más en la lógica de negocio y menos en la sintaxis del framework. Esto se traduce en una mayor eficiencia y productividad. Además, <script setup> se beneficia de mejores inferencias de tipo cuando se usa con TypeScript, lo que resulta en una experiencia de desarrollo más robusta y con menos errores en tiempo de ejecución. La integración con herramientas de desarrollo como Vite también es excepcional, ofreciendo una recarga en caliente (HMR) ultrarrápida que mejora aún más la experiencia.
Integración nativa con la Composition API
<script setup> no es un reemplazo de la Composition API, sino una mejora sintáctica para usarla. Todos los principios y funciones de la Composition API, como ref, reactive, computed, watch, y los ciclos de vida, se utilizan de la misma manera. Esto significa que los conocimientos que ya tienes sobre la Composition API siguen siendo totalmente válidos y se aplican directamente, pero ahora en un formato mucho más elegante y directo. Considero que esta característica ha sido el empujón final para que muchos adopten plenamente la Composition API, al eliminar una de sus pequeñas fricciones.
Primeros pasos con <script setup>: configurando tu entorno
Para empezar a trabajar con <script setup>, necesitarás un proyecto de Vue 3. La forma más sencilla de hacerlo es utilizando la herramienta de scaffolding oficial de Vue, que ahora por defecto utiliza Vite como bundler, conocido por su velocidad y eficiencia.
Creación de un nuevo proyecto Vue con Vite
Abre tu terminal y ejecuta el siguiente comando:
npm init vue@latest
O si prefieres usar Yarn o pnpm:
yarn create vue@latest
# o
pnpm create vue@latest
El CLI te guiará a través de una serie de preguntas para configurar tu proyecto. Asegúrate de seleccionar las opciones que desees (por ejemplo, TypeScript, Router, Pinia, etc.). Una vez que el proyecto esté creado, navega a la carpeta del proyecto e instala las dependencias:
cd <your-project-name>
npm install
# o
yarn install
# o
pnpm install
Finalmente, inicia el servidor de desarrollo:
npm run dev
# o
yarn dev
# o
pnpm dev
¡Listo! Ahora tienes un proyecto de Vue 3 configurado y listo para usar <script setup>. Por defecto, los nuevos proyectos generados con npm init vue@latest ya utilizan <script setup> en sus componentes de ejemplo, como App.vue y HelloWorld.vue. Puedes consultar la documentación oficial de Vue para más detalles sobre cómo empezar.
Desglosando la sintaxis de <script setup>: el corazón del componente
Ahora que tenemos nuestro entorno listo, veamos cómo se traduce la lógica común de los componentes en la sintaxis de <script setup>. La clave es que todo lo que declares en el bloque <script setup> se auto-expone a la plantilla, eliminando la necesidad de un objeto return explícito.
Declaración de variables reactivas (ref, reactive)
Las variables reactivas son fundamentales en Vue. Con <script setup>, se declaran e importan de la misma manera que antes, pero directamente en el cuerpo del script:
<script setup>
import { ref, reactive } from 'vue';
const count = ref(0);
const user = reactive({
name: 'Juan',
age: 30
});
function increment() {
count.value++;
}
// Puedes acceder a 'count', 'user' e 'increment' directamente en la plantilla
</script>
<template>
<div>
<p>Contador: {{ count }}</p>
<p>Usuario: {{ user.name }} ({{ user.age }} años)</p>
<button @click="increment">Incrementar</button>
</div>
</template>
Como puedes observar, no hay un bloque setup() ni un return. Esto hace que el código sea mucho más limpio y directo. Es una mejora sustancial que, en mi opinión, reduce significativamente la barrera de entrada para quienes vienen de otros frameworks o incluso de JavaScript vanilla, ya que la reactividad se siente más como una extensión natural del lenguaje.
Definición de funciones y manejadores de eventos
Las funciones se definen de manera estándar y están disponibles automáticamente en la plantilla:
<script setup>
import { ref } from 'vue';
const message = ref('');
function updateMessage(event) {
message.value = event.target.value;
}
function greet() {
alert(Hola, ${message.value || 'mundo'}!);
}
</script>
<template>
<div>
<input type="text" v-model="message" @input="updateMessage" placeholder="Escribe tu nombre">
<button @click="greet">Saludar</button>
<p>Tu mensaje: {{ message }}</p>
</div>
</template>
Aquí vemos cómo updateMessage y greet se pueden enlazar directamente a eventos en la plantilla sin ninguna configuración adicional. La simplicidad de este enfoque es uno de los mayores argumentos a favor de <script setup>.
Propiedades computadas (computed) y watchers (watch)
Las propiedades computadas y los watchers son herramientas poderosas para derivar estado o reaccionar a cambios en el estado. Con <script setup>, su uso es igual de directo:
<script setup>
import { ref, computed, watch } from 'vue';
const firstName = ref('Alice');
const lastName = ref('Smith');
const fullName = computed(() => ${firstName.value} ${lastName.value});
watch(count, (newCount, oldCount) => {
console.log(El contador cambió de ${oldCount} a ${newCount});
});
watch([firstName, lastName], ([newFirstName, newLastName], [oldFirstName, oldLastName]) => {
console.log(Nombre cambiado de ${oldFirstName} ${oldLastName} a ${newFirstName} ${newLastName});
});
// También podemos tener efectos secundarios
watch(fullName, (newValue) => {
document.title = Bienvenido, ${newValue};
}, { immediate: true }); // El tercer argumento para opciones también funciona.
</script>
<template>
<div>
<p>Nombre completo: {{ fullName }}</p>
<input type="text" v-model="firstName" placeholder="Nombre">
<input type="text" v-model="lastName" placeholder="Apellido">
</div>
</template>
El código para computed y watch permanece idéntico a cómo se usaría en un bloque setup() tradicional, pero la ausencia de anidamiento y la auto-exposición a la plantilla hacen que la experiencia sea mucho más fluida. Esto elimina gran parte de la "ruido" sintáctico y permite que los desarrolladores se concentren en la lógica reactiva.
Manejo avanzado: props, emits y slots
Los componentes son la base de las aplicaciones Vue, y la comunicación entre ellos es crucial. <script setup> proporciona APIs específicas para manejar props, emits y slots de manera más concisa.
Definición de props con defineProps
Para definir las propiedades (props) que un componente recibe, utilizamos la función defineProps(). Esta es una macro de compilación y no necesita ser importada:
<!-- MiComponente.vue -->
<script setup>
import { defineProps } from 'vue'; // No es estrictamente necesario importar defineProps, pero ayuda a la claridad
const props = defineProps({
title: String,
isActive: {
type: Boolean,
default: false
}
});
// Las props se pueden usar directamente:
console.log(props.title);
</script>
<template>
<div :class="{ 'active': props.isActive }">
<h3>{{ props.title }}</h3>
<p>Estado: {{ props.isActive ? 'Activo' : 'Inactivo' }}</p>
</div>
</template>
Y en el componente padre:
<!-- App.vue -->
<script setup>
import MiComponente from './components/MiComponente.vue';
</script>
<template>
<MiComponente title="Primer componente" :is-active="true" />
<MiComponente title="Segundo componente" />
</template>
La función defineProps se utiliza para declarar las props. El objeto devuelto por defineProps es reactivo y puede ser desestructurado si se desea, pero se recomienda acceder a las props a través del objeto props para mantener la reactividad. Personalmente, me gusta la claridad de `props.someProp`, aunque entiendo la atracción de la desestructuración.
Emisión de eventos con defineEmits
Para que un componente hijo pueda comunicarse con su padre, emite eventos. Esto se logra con la función defineEmits():
<!-- ContadorBoton.vue -->
<script setup>
import { ref, defineEmits } from 'vue';
const count = ref(0);
const emit = defineEmits(['increment', 'reset']);
function incrementCount() {
count.value++;
emit('increment', count.value); // Emitimos el evento 'increment' con el valor actual
}
function resetCount() {
count.value = 0;
emit('reset'); // Emitimos el evento 'reset'
}
</script>
<template>
<div>
<p>Contador interno: {{ count }}</p>
<button @click="incrementCount">Incrementar</button>
<button @click="resetCount">Reiniciar</button>
</div>
</template>
Y en el componente padre:
<!-- App.vue -->
<script setup>
import { ref } from 'vue';
import ContadorBoton from './components/ContadorBoton.vue';
const totalIncrements = ref(0);
function handleIncrement(value) {
totalIncrements.value = value;
console.log('Incremento recibido:', value);
}
function handleReset() {
console.log('Contador reiniciado por el hijo.');
totalIncrements.value = 0;
}
</script>
<template>
<div>
<h3>Total de incrementos en App: {{ totalIncrements }}</h3>
<ContadorBoton @increment="handleIncrement" @reset="handleReset" />
</div>
</template>
Similar a defineProps, defineEmits es una macro de compilación que define los eventos que un componente puede emitir. El valor devuelto, emit, es una función que se utiliza para disparar esos eventos. La claridad de definir los eventos esperados directamente en el script mejora la mantenibilidad y la autocompletado en IDEs.
Uso de slots
Los slots permiten pasar contenido de la plantilla de un componente padre a la plantilla de un componente hijo. En <script setup>, el manejo de slots es transparente, ya que la lógica principal reside en cómo se define el slot en el componente hijo y cómo se utiliza en el padre.
<!-- CardComponent.vue -->
<template>
<div class="card">
<header>
<slot name="header"><h4>Título por defecto</h4></slot>
</header>
<main>
<slot><p>Contenido por defecto de la tarjeta.</p></slot>
</main>
<footer>
<slot name="footer"><small>Pie de página por defecto</small></slot>
</footer>
</div>
</template>
<style scoped>
.card {
border: 1px solid #ccc;
padding: 15px;
margin: 10px;
border-radius: 8px;
background-color: #f9f9f9;
}
</style>
Y cómo se usa en el componente padre (por ejemplo, App.vue):
<!-- App.vue -->
<script setup>
import CardComponent from './components/CardComponent.vue';
</script>
<template>
<div>
<CardComponent>
<template #header>
<h2>Título de mi tarjeta personalizada</h2>
</template>
<p>Este es el contenido principal que se inserta en el slot por defecto.</p>
<template #footer>
<button>Más información</button>
</template>
</CardComponent>
<CardComponent>
<h3>Otra tarjeta, solo con contenido principal</h3>
</CardComponent>
</div>
</template>
El bloque <script setup> en el componente que define los slots no necesita ninguna declaración especial. La magia ocurre en la plantilla del componente que recibe el contenido. Para slots con ámbito (scoped slots), donde el componente hijo expone datos al padre, la sintaxis en la plantilla del padre sigue siendo la misma (usando v-slot="slotProps" o #default="slotProps").
Referencias de plantilla (template refs) y acceso a componentes
A veces, necesitas acceder directamente a un elemento DOM o a una instancia de un componente hijo desde tu script. Las referencias de plantilla permiten esto y funcionan de maravilla con <script setup>.
<script setup>
import { ref, onMounted } from 'vue';
const inputRef = ref(null); // Inicializamos la referencia a null
onMounted(() => {
if (inputRef.value) {
inputRef.value.focus(); // Enfoca el input cuando el componente se monta
console.log('Input DOM element:', inputRef.value);
}
});
// Para referenciar un componente hijo
const childComponentRef = ref(null);
onMounted(() => {
if (childComponentRef.value) {
console.log('Child component instance:', childComponentRef.value);
// Puedes llamar a métodos expuestos o acceder a propiedades
// childComponentRef.value.someMethod();
}
});
</script>
<template>
<div>
<input type="text" ref="inputRef" placeholder="Seré enfocado al cargar">
<!-- Suponiendo que tienes un componente llamado ChildComponent -->
<ChildComponent ref="childComponentRef" />
</div>
</template>
La variable reactiva inputRef (o cualquier nombre que elijas) se enlaza al atributo ref del elemento en la plantilla. Una vez que el componente se monta, inputRef.value contendrá el elemento DOM real o la instancia del componente. Esta es una forma potente y segura de interactuar con elementos directos del DOM o componentes hijos, y <script setup> no altera en absoluto su flujo de trabajo, lo cual es una consistencia bienvenida.
Composición con otros componentes y módulos
La modularidad es clave en cualquier aplicación moderna. <script setup> facilita la importación y el uso de otros componentes y la reutilización de lógica mediante "composables".
Importación de componentes
Importar otros componentes dentro de <script setup> es tan sencillo como importarlos en un módulo JavaScript regular:
<script setup>
import MyButton from './MyButton.vue'; // Ruta relativa a tu componente
import MyHeader from '@/components/MyHeader.vue'; // Con alias de ruta, si configurado
// No necesitas registrarlos en un objeto 'components', se auto-registran
</script>
<template>
<div>
<MyHeader />
<MyButton text="Haz clic" @click="handleClick" />
</div>
</template>
No se requiere un objeto components como en el Options API o en un setup() tradicional. Los componentes importados están disponibles directamente en la plantilla. Esto simplifica a