Preguntas de entrevista de Vue.js con respuestas (35+)
¿Tenés una entrevista técnica de Vue.js y no sabés por dónde empezar? Esta guía tiene todo lo que necesitás: desde las preguntas que te hacen en el primer filtro hasta las que marcan la diferencia cuando competís por un rol senior.
Cubrimos Vue 2 y Vue 3, Composition API, Options API, Vuex, Pinia, Vue Router y patrones avanzados — con código real que podés leer, entender y reproducir en voz alta.
Índice
- 1[Fundamentos de Vue](#fundamentos)
- 2[Sistema de reactividad](#reactividad)
- 3[Composition API vs Options API](#composition-vs-options)
- 4[Componentes y comunicación](#componentes)
- 5[Vue Router](#router)
- 6[Estado global: Vuex y Pinia](#estado)
- 7[Rendimiento y optimización](#rendimiento)
- 8[Preguntas avanzadas](#avanzadas)
Fundamentos de Vue {#fundamentos}
1. ¿Qué es Vue.js y qué lo diferencia de React o Angular?
Vue es un framework JavaScript progresivo para construir interfaces de usuario. "Progresivo" significa que podés adoptarlo gradualmente: desde sprinkles de interactividad en una página existente hasta una SPA completa.
Diferencias clave:
| | Vue | React | Angular |
|---|---|---|---|
| Curva de aprendizaje | Baja | Media | Alta |
| Opinionado | Medio | Poco | Mucho |
| Templates | HTML-like | JSX | HTML + directivas |
| Estado | Reactivo built-in | useState / libs externas | Services + RxJS |
Vue combina lo mejor de React (component model) y Angular (directivas declarativas en templates) con una sintaxis más accesible. Su sistema de reactividad es automático — no necesitás llamar a setState ni suscribirte manualmente a observables.
2. ¿Qué es el Virtual DOM y cómo lo usa Vue?
El Virtual DOM es una representación en memoria del DOM real. Cuando el estado cambia, Vue construye un nuevo árbol virtual, lo compara con el anterior (diffing) y solo actualiza los nodos del DOM real que cambiaron (patching).
// Vos escribís esto:
const count = ref(0)
// Vue internamente hace algo así:
const vnode = {
type: 'div',
props: {},
children: [`El contador está en ${count.value}`]
}Importante para la entrevista: Vue 3 introdujo un compilador más inteligente que marca los nodos estáticos y los salta completamente durante el diffing. Esto se llama static hoisting y es una de las razones por las que Vue 3 es significativamente más rápido que Vue 2.
3. ¿Cuáles son los lifecycle hooks principales en Vue?
Vue 3 (Composition API):
import { onMounted, onUpdated, onUnmounted, onBeforeMount } from 'vue'
export default {
setup() {
onBeforeMount(() => {
// El componente está por montarse, el DOM no existe todavía
})
onMounted(() => {
// El DOM ya existe — acá podés acceder a refs del template
// Ideal para llamadas a APIs, inicializar librerías de terceros
})
onUpdated(() => {
// El componente se re-renderizó porque el estado cambió
// Ojo: no mutés estado acá o entrás en loop infinito
})
onUnmounted(() => {
// El componente se desmontó — limpiá event listeners, timers, websockets
})
}
}Vue 2 / Options API:
export default {
beforeCreate() {},
created() {}, // Estado inicializado, sin DOM
beforeMount() {},
mounted() {}, // Equivalente a onMounted
beforeUpdate() {},
updated() {},
beforeDestroy() {}, // Equivalente a onUnmounted
destroyed() {}
}En la entrevista te pueden preguntar: "¿En qué lifecycle hook harías una llamada a una API?" La respuesta correcta es onMounted (o mounted), porque es cuando el componente ya está en el DOM y el usuario espera ver datos. En created también funciona, pero el DOM no está disponible.
4. ¿Qué es `v-model` y cómo funciona internamente?
v-model es azúcar sintáctico para binding bidireccional. En un input, es equivalente a:
<!-- Esto: -->
<input v-model="nombre" />
<!-- Es lo mismo que esto: -->
<input :value="nombre" @input="nombre = $event.target.value" />En componentes custom, v-model pasa un prop modelValue y escucha el evento update:modelValue:
// Componente hijo: MiInput.vue
export default {
props: ['modelValue'],
emits: ['update:modelValue'],
template: `
<input
:value="modelValue"
@input="$emit('update:modelValue', $event.target.value)"
/>
`
}<!-- Componente padre -->
<MiInput v-model="nombre" />Vue 3 permite múltiples v-models en el mismo componente:
<MiFormulario v-model:nombre="nombre" v-model:email="email" />5. Explicá la diferencia entre `v-show` y `v-if`
<!-- v-if: agrega/elimina el elemento del DOM -->
<div v-if="visible">Este div aparece y desaparece del DOM</div>
<!-- v-show: siempre está en el DOM, solo cambia display: none -->
<div v-show="visible">Este div siempre existe</div>Cuándo usar cada uno:
v-if: cuando la condición rara vez cambia o el elemento es costoso de inicializar. El renderizado inicial es más barato si la condición esfalse.v-show: cuando toggleás frecuentemente. El elemento siempre está en el DOM, así que el toggle es un cambio de CSS, no un mount/unmount completo.
Regla práctica: modales que se abren una vez → v-if. Tabs que el usuario alterna constantemente → v-show.
6. ¿Qué es la directiva `v-for` y cuál es la importancia del atributo `key`?
<ul>
<li v-for="(item, index) in lista" :key="item.id">
{{ index + 1 }}. {{ item.nombre }}
</li>
</ul>El atributo :key le dice a Vue cómo identificar cada elemento cuando la lista cambia. Sin él, Vue usa el algoritmo de diffing más simple (por posición), lo que puede causar bugs con componentes que tienen estado interno.
Ejemplo del problema sin key:
<!-- ❌ Sin key: si reordenás la lista, el checkbox del elemento A
puede quedar pegado al texto del elemento B -->
<li v-for="item in lista">
<input type="checkbox" /> {{ item.nombre }}
</li>
<!-- ✅ Con key: Vue sabe exactamente qué elemento es cuál -->
<li v-for="item in lista" :key="item.id">
<input type="checkbox" /> {{ item.nombre }}
</li>Regla: nunca uses el index como key si el orden de la lista puede cambiar. Usá siempre un identificador único del dato (como item.id).
Sistema de reactividad {#reactividad}
7. ¿Cómo funciona el sistema de reactividad de Vue 3?
Vue 3 usa Proxies de JavaScript para interceptar lecturas y escrituras de propiedades. Cuando leés una propiedad reactiva dentro de un efecto (como computed, watch, o el render), Vue registra esa dependencia. Cuando la propiedad cambia, Vue re-ejecuta todos los efectos que la usaban.
import { reactive, ref, effect } from 'vue'
const estado = reactive({ count: 0 })
// effect() es la base de computed, watch y el render
effect(() => {
console.log('El count es:', estado.count) // Se ejecuta ahora...
})
estado.count++ // ...y también acá, porque la dependencia fue rastreadaVue 2 usaba Object.defineProperty, que tiene limitaciones:
- No puede detectar adición/eliminación de propiedades nuevas
- No puede detectar cambios en arrays por índice
Por eso en Vue 2 necesitabas Vue.set(obj, 'nuevaClave', valor). En Vue 3 con Proxy, eso ya no es necesario.
8. ¿Cuál es la diferencia entre `ref` y `reactive`?
import { ref, reactive } from 'vue'
// ref: para valores primitivos (y también objetos, aunque reactive es más natural)
const count = ref(0)
console.log(count.value) // 0 — accedés al valor con .value
count.value++
// reactive: para objetos y arrays
const usuario = reactive({
nombre: 'Ana',
edad: 28
})
console.log(usuario.nombre) // 'Ana' — sin .value
usuario.edad = 29Diferencia crucial: reactive no puede ser reasignado completo:
// ❌ Esto rompe la reactividad
let usuario = reactive({ nombre: 'Ana' })
usuario = { nombre: 'Carlos' } // La referencia se perdió, Vue no lo puede rastrear
// ✅ Con ref, podés reasignar
let usuario = ref({ nombre: 'Ana' })
usuario.value = { nombre: 'Carlos' } // Esto sí funcionaRegla general: usá ref para todo (especialmente si venís de React). Usá reactive cuando tenés un objeto de estado bien definido y querés evitar el .value.
9. ¿Qué es `computed` y cuándo lo usás?
Un computed es un valor derivado que se recalcula automáticamente cuando sus dependencias cambian. Se cachea: si las dependencias no cambiaron, devuelve el valor anterior sin recalcular.
import { ref, computed } from 'vue'
const precio = ref(100)
const descuento = ref(0.1)
// computed: se recalcula solo cuando precio o descuento cambien
const precioFinal = computed(() => {
console.log('Recalculando...') // Solo se ejecuta cuando cambia precio o descuento
return precio.value * (1 - descuento.value)
})
console.log(precioFinal.value) // 90, y loguea "Recalculando..."
console.log(precioFinal.value) // 90, sin loguear (usó el caché)
precio.value = 200
console.log(precioFinal.value) // 180, y loguea "Recalculando..."computed writable:
const fullName = computed({
get() {
return `${firstName.value} ${lastName.value}`
},
set(newValue) {
const parts = newValue.split(' ')
firstName.value = parts[0]
lastName.value = parts[1]
}
})10. ¿Cuándo usás `watch` vs `watchEffect`?
import { ref, watch, watchEffect } from 'vue'
const query = ref('')
const resultados = ref([])
// watch: controlás exactamente qué observar y reaccionás al cambio
watch(query, async (nuevoValor, valorAnterior) => {
if (nuevoValor.length > 2) {
resultados.value = await buscar(nuevoValor)
}
}, { immediate: false }) // immediate: true para ejecutar de entrada
// watchEffect: se ejecuta inmediatamente y rastrear dependencias automáticamente
watchEffect(async () => {
// Acá se suscribe a TODAS las refs que lee
if (query.value.length > 2) {
resultados.value = await buscar(query.value)
}
})Cuándo usar cada uno:
| | watch | watchEffect |
|---|---|---|
| Acceso al valor anterior | Sí | No |
| Control explícito de qué observar | Sí | No (automático) |
| Ejecución inmediata por defecto | No | Sí |
| Mejor para | Side effects específicos, lazy | Side effects que dependen de varios valores |
Composition API vs Options API {#composition-vs-options}
11. ¿Cuál es la diferencia entre Options API y Composition API?
Options API organiza el código por tipo de opción (data, methods, computed, watch):
// Options API — Vue 2 y Vue 3
export default {
data() {
return {
count: 0,
nombre: ''
}
},
computed: {
countDoble() {
return this.count * 2
}
},
methods: {
incrementar() {
this.count++
}
},
mounted() {
console.log('Montado')
}
}Composition API organiza el código por lógica de negocio:
// Composition API — Vue 3
import { ref, computed, onMounted } from 'vue'
export default {
setup() {
// Toda la lógica del contador junta
const count = ref(0)
const countDoble = computed(() => count.value * 2)
const incrementar = () => count.value++
// Toda la lógica del usuario junta
const nombre = ref('')
// ... más lógica del usuario
onMounted(() => console.log('Montado'))
return { count, countDoble, incrementar, nombre }
}
}La ventaja real de Composition API: podés extraer lógica en composables (funciones reutilizables) y compartirla entre componentes sin mixins ni herencia:
// useContador.js — composable reutilizable
export function useContador(inicial = 0) {
const count = ref(inicial)
const incrementar = () => count.value++
const resetear = () => count.value = inicial
return { count, incrementar, resetear }
}
// En cualquier componente:
const { count, incrementar } = useContador(10)12. ¿Qué es `<script setup>` y por qué se usa?
es azúcar sintáctico para la Composition API. Todo lo que declarás en él queda automáticamente disponible en el template — no necesitás return {}.
<!-- Sin <script setup> -->
<script>
import { ref } from 'vue'
export default {
setup() {
const nombre = ref('Ana')
return { nombre } // Tenés que retornar todo
}
}
</script>
<!-- Con <script setup> — más limpio, mismo resultado -->
<script setup>
import { ref } from 'vue'
const nombre = ref('Ana') // Automáticamente disponible en el template
</script>
<template>
<div>{{ nombre }}</div>
</template>Bonus: los componentes importados también quedan automáticamente registrados:
<script setup>
import MiBoton from './MiBoton.vue' // No necesitás registrarlo en components: {}
</script>
<template>
<MiBoton />
</template>13. ¿Cómo declarás props y emits en `<script setup>`?
<script setup>
// Props con defineProps
const props = defineProps({
nombre: {
type: String,
required: true
},
edad: {
type: Number,
default: 0
}
})
// O con TypeScript:
const props = defineProps<{
nombre: string
edad?: number
}>()
// Emits con defineEmits
const emit = defineEmits(['actualizar', 'eliminar'])
// O con TypeScript:
const emit = defineEmits<{
actualizar: [nuevoNombre: string]
eliminar: []
}>()
// Usar:
emit('actualizar', 'Carlos')
</script>Componentes y comunicación {#componentes}
14. ¿Cuáles son los patrones de comunicación entre componentes?
1. Props (padre → hijo):
<!-- Padre -->
<Hijo :mensaje="texto" />
<!-- Hijo -->
<script setup>
const props = defineProps(['mensaje'])
</script>2. Emits (hijo → padre):
<!-- Hijo -->
<script setup>
const emit = defineEmits(['cambio'])
const handleClick = () => emit('cambio', 'nuevo valor')
</script>
<!-- Padre -->
<Hijo @cambio="manejarCambio" />3. Provide / Inject (cualquier ancestro → cualquier descendiente):
// Componente ancestro
import { provide, ref } from 'vue'
const tema = ref('oscuro')
provide('tema', tema) // Inyecta 'tema' en todos los descendientes
// Componente descendiente (sin importar qué tan profundo)
import { inject } from 'vue'
const tema = inject('tema') // 'oscuro'4. Estado global con Pinia (ver sección de estado global).
5. v-model custom para formularios y componentes de input (ver pregunta 4).
15. ¿Qué son los slots y cuándo los usás?
Los slots te permiten que el padre pase contenido HTML/template al hijo:
<!-- Componente Card.vue -->
<template>
<div class="card">
<div class="card-header">
<slot name="header">Título por defecto</slot>
</div>
<div class="card-body">
<slot /> <!-- slot default -->
</div>
<div class="card-footer">
<slot name="footer" />
</div>
</div>
</template><!-- Usando Card.vue -->
<Card>
<template #header>
<h2>Mi título personalizado</h2>
</template>
<p>Este es el contenido del body</p>
<template #footer>
<button>Guardar</button>
</template>
</Card>Scoped slots — el hijo pasa datos de vuelta al padre:
<!-- Lista.vue -->
<template>
<ul>
<li v-for="item in items" :key="item.id">
<slot :item="item" :index="index" />
</li>
</ul>
</template><Lista :items="frutas">
<template #default="{ item, index }">
<strong>{{ index + 1 }}.</strong> {{ item.nombre }}
</template>
</Lista>16. ¿Qué es `defineExpose` y cuándo lo usás?
Con , el componente hijo no expone nada al padre por defecto. Si el padre quiere acceder a métodos o datos internos del hijo via template ref, el hijo necesita exponerlos explícitamente:
<!-- Hijo: InputCustom.vue -->
<script setup>
import { ref } from 'vue'
const inputRef = ref(null)
const enfocar = () => inputRef.value?.focus()
const limpiar = () => inputRef.value.value = ''
defineExpose({ enfocar, limpiar }) // Solo esto queda accesible desde afuera
</script><!-- Padre -->
<script setup>
import { ref } from 'vue'
const inputRef = ref(null)
const handleSubmit = () => {
inputRef.value.limpiar() // Funciona
inputRef.value.enfocar() // Funciona
}
</script>
<template>
<InputCustom ref="inputRef" />
<button @submit="handleSubmit">Submit</button>
</template>17. ¿Qué es `Teleport` en Vue 3?
Teleport te permite renderizar un componente fuera de su posición en el árbol DOM — útil para modales, tooltips, y notificaciones que tienen que vivir en el body para no tener problemas con z-index o overflow: hidden.
<template>
<div class="componente-profundo">
<Teleport to="body">
<!-- Este contenido se renderiza como hijo directo de <body>
aunque esté definido acá adentro -->
<div class="modal" v-if="modalAbierto">
<h2>Modal</h2>
<button @click="modalAbierto = false">Cerrar</button>
</div>
</Teleport>
</div>
</template>18. ¿Qué son los composables y cómo los diseñás bien?
Un composable es una función que usa la Composition API para encapsular y reutilizar lógica stateful:
// useFetch.js
import { ref, watchEffect, toValue } from 'vue'
export function useFetch(url) {
const data = ref(null)
const error = ref(null)
const loading = ref(false)
watchEffect(async () => {
// toValue() resuelve tanto refs como valores planos
const urlValue = toValue(url)
if (!urlValue) return
loading.value = true
error.value = null
try {
const res = await fetch(urlValue)
if (!res.ok) throw new Error(`HTTP ${res.status}`)
data.value = await res.json()
} catch (e) {
error.value = e.message
} finally {
loading.value = false
}
})
return { data, error, loading }
}<!-- Uso en un componente -->
<script setup>
import { ref } from 'vue'
import { useFetch } from './useFetch'
const userId = ref(1)
const { data: usuario, loading, error } = useFetch(
computed(() => `/api/users/${userId.value}`)
)
</script>Buenas prácticas para composables:
- Nombre con prefijo
use - Aceptan tanto refs como valores planos (usá
toValue) - Devuelven refs (no valores planos) para mantener la reactividad
- Limpieza automática con
onUnmountedsi crean efectos secundarios
Vue Router {#router}
19. ¿Cómo configurás Vue Router básico en Vue 3?
// router/index.js
import { createRouter, createWebHistory } from 'vue-router'
const routes = [
{
path: '/',
component: () => import('../views/Home.vue') // Lazy loading automático
},
{
path: '/usuarios/:id',
component: () => import('../views/Usuario.vue'),
props: true // El :id llega como prop al componente
},
{
path: '/:pathMatch(.*)*', // 404
component: () => import('../views/NotFound.vue')
}
]
export const router = createRouter({
history: createWebHistory(), // URLs limpias: /usuarios/1
// createWebHashHistory() → /#/usuarios/1
routes
})// main.js
import { createApp } from 'vue'
import App from './App.vue'
import { router } from './router'
createApp(App).use(router).mount('#app')20. ¿Cómo usás navigation guards?
Los navigation guards son hooks que se ejecutan antes, durante y después de cada navegación:
// Guard global — proteger toda la app
router.beforeEach((to, from) => {
const auth = useAuthStore()
if (to.meta.requiresAuth && !auth.isLoggedIn) {
return { name: 'Login', query: { redirect: to.fullPath } }
}
})
// Guard en la ruta — proteger rutas específicas
const routes = [
{
path: '/admin',
component: Admin,
meta: { requiresAuth: true, role: 'admin' },
beforeEnter: (to, from) => {
const auth = useAuthStore()
if (!auth.hasRole('admin')) return '/unauthorized'
}
}
]<!-- Guard en el componente -->
<script setup>
import { onBeforeRouteLeave, onBeforeRouteUpdate } from 'vue-router'
onBeforeRouteLeave((to, from) => {
const respuesta = confirm('¿Seguro que querés salir? Tenés cambios sin guardar.')
if (!respuesta) return false // Cancela la navegación
})
</script>21. ¿Cómo manejás la navegación programática?
import { useRouter, useRoute } from 'vue-router'
const router = useRouter()
const route = useRoute() // Datos de la ruta actual (params, query, etc.)
// Navegar a una ruta
router.push('/home')
router.push({ name: 'Usuario', params: { id: 42 } })
router.push({ path: '/search', query: { q: 'vue' } })
// Reemplazar (sin agregar al historial)
router.replace('/login')
// Historial
router.back()
router.forward()
router.go(-2)
// Acceder a datos de la ruta actual
console.log(route.params.id) // '42'
console.log(route.query.q) // 'vue'
console.log(route.path) // '/search'Estado global: Vuex y Pinia {#estado}
22. ¿Cuándo necesitás estado global y cuándo no?
No todo el estado tiene que ser global. Antes de usar Pinia o Vuex, preguntate:
- ¿Este estado lo necesitan más de 2 componentes que no son padre-hijo?
- ¿Persiste cuando el componente se desmonta?
- ¿Es compartido entre rutas?
Si respondiste sí a alguna: usá estado global. Si no, usá ref local o provide/inject.
23. ¿Cómo funciona Pinia?
Pinia es el store oficial recomendado para Vue 3 (reemplaza a Vuex). Cada store es un módulo independiente:
// stores/contador.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useContadorStore = defineStore('contador', () => {
// State
const count = ref(0)
// Getters (equivalente a computed)
const countDoble = computed(() => count.value * 2)
// Actions
function incrementar(cantidad = 1) {
count.value += cantidad
}
async function incrementarDesdeAPI() {
const { valor } = await fetch('/api/contador').then(r => r.json())
count.value = valor
}
return { count, countDoble, incrementar, incrementarDesdeAPI }
})<!-- En un componente -->
<script setup>
import { storeToRefs } from 'pinia'
import { useContadorStore } from '@/stores/contador'
const store = useContadorStore()
// storeToRefs para acceder a state/getters reactivamente
const { count, countDoble } = storeToRefs(store)
// Las actions se desestructuran directamente (no pierden contexto)
const { incrementar } = store
</script>
<template>
<div>{{ count }} (doble: {{ countDoble }})</div>
<button @click="incrementar">+1</button>
</template>24. ¿Cuál es la diferencia entre Pinia y Vuex?
| | Vuex 4 | Pinia |
|---|---|---|
| Mutations | Requeridas para cambiar state | No existen — actions directamente |
| TypeScript | Difícil, mucho boilerplate | Excelente soporte nativo |
| Módulos | Anidados, namespace: true | Stores individuales por defecto |
| DevTools | Sí | Sí (mejor UX) |
| SSR | Sí | Sí |
| Boilerplate | Mucho | Mínimo |
// Vuex — necesitás mutation para cambiar state
mutations: {
INCREMENT(state) {
state.count++
}
},
actions: {
increment({ commit }) {
commit('INCREMENT') // Dos pasos
}
}
// Pinia — una sola acción
actions: {
increment() {
this.count++ // Directo
}
}Para proyectos nuevos: usá siempre Pinia.
25. ¿Cómo persistís el estado de un store Pinia?
Con el plugin pinia-plugin-persistedstate:
// main.js
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)// stores/auth.js
export const useAuthStore = defineStore('auth', () => {
const token = ref(null)
const usuario = ref(null)
return { token, usuario }
}, {
persist: {
storage: localStorage, // o sessionStorage
pick: ['token'] // Solo persiste el token, no el objeto usuario completo
}
})Rendimiento y optimización {#rendimiento}
26. ¿Cómo usás `defineAsyncComponent` y por qué importa?
Lazy loading de componentes: el bundle de ese componente se carga solo cuando se necesita:
import { defineAsyncComponent } from 'vue'
// Básico
const EditorRich = defineAsyncComponent(() =>
import('./EditorRich.vue')
)
// Con opciones de loading y error
const EditorRich = defineAsyncComponent({
loader: () => import('./EditorRich.vue'),
loadingComponent: Spinner,
errorComponent: ErrorMsg,
delay: 200, // Mostrar spinner después de 200ms
timeout: 10000 // Error si tarda más de 10s
})Combinado con :
<template>
<Suspense>
<EditorRich />
<template #fallback>
<Spinner />
</template>
</Suspense>
</template>27. ¿Qué es `shallowRef` y `shallowReactive`?
Cuando tenés objetos muy grandes o estructuras de datos que cambian completas, la reactividad profunda tiene costo. Las versiones shallow solo rastrean el primer nivel:
import { shallowRef, shallowReactive } from 'vue'
// shallowRef: solo el .value es reactivo, no las propiedades internas
const estado = shallowRef({ contadores: [1, 2, 3], config: { modo: 'oscuro' } })
estado.value.contadores.push(4) // NO reactivo — Vue no se entera
estado.value = { ...estado.value, contadores: [...estado.value.contadores, 4] } // Reactivo
// Útil para: listas grandes que se reemplazan completas, Three.js, canvas, etc.28. ¿Qué hace `v-memo`?
v-memo cachea un subárbol del template y solo lo re-renderiza cuando el array de dependencias cambia. Útil para listas grandes donde algunos elementos son costosos de renderizar:
<!-- Solo re-renderiza este li si item.id, item.nombre o selected cambian -->
<li v-for="item in lista" :key="item.id" v-memo="[item.id, item.nombre, selected === item.id]">
<ItemComplejo :item="item" :selected="selected === item.id" />
</li>29. ¿Cómo evitás re-renders innecesarios?
// 1. computed en lugar de métodos para valores derivados
// Mal: se recalcula en CADA render
const filtered = () => items.value.filter(i => i.activo)
// Bien: se recalcula solo cuando items cambia
const filtered = computed(() => items.value.filter(i => i.activo))
// 2. v-once para contenido verdaderamente estático
// <div v-once>{{ configuracion.textoEstatico }}</div>
// 3. Desnormalizar props: pasar solo lo necesario
// Mal: pasar el objeto completo
<Hijo :usuario="usuario" />
// Bien: pasar solo lo que el hijo necesita
<Hijo :nombre="usuario.nombre" :avatar="usuario.avatarUrl" />Preguntas avanzadas {#avanzadas}
30. ¿Qué es el patrón "Compound Components" en Vue?
Es un patrón donde varios componentes trabajan juntos compartiendo estado interno via provide/inject. Inspirado en el uso de + nativo:
// Accordion.vue — el componente padre
import { provide, ref } from 'vue'
export default {
setup() {
const abierto = ref(null)
const toggle = (id) => {
abierto.value = abierto.value === id ? null : id
}
provide('accordion', { abierto, toggle })
}
}// AccordionItem.vue — el componente hijo
import { inject, computed } from 'vue'
export default {
props: ['id'],
setup(props) {
const { abierto, toggle } = inject('accordion')
const estaAbierto = computed(() => abierto.value === props.id)
return { estaAbierto, toggle: () => toggle(props.id) }
}
}<!-- Uso limpio y semántico -->
<Accordion>
<AccordionItem id="1">
<template #header>Pregunta 1</template>
<template #body>Respuesta 1</template>
</AccordionItem>
<AccordionItem id="2">
<template #header>Pregunta 2</template>
<template #body>Respuesta 2</template>
</AccordionItem>
</Accordion>31. ¿Cómo escribís un plugin de Vue?
Un plugin es un objeto con método install que recibe la app de Vue:
// plugins/i18n.js
export const i18nPlugin = {
install(app, opciones) {
const { idioma, traducciones } = opciones
// Agregar una propiedad global
app.config.globalProperties.$t = (clave) => {
return traducciones[idioma]?.[clave] ?? clave
}
// Agregar una directiva global
app.directive('traducir', {
mounted(el, binding) {
el.textContent = traducciones[idioma]?.[binding.value] ?? binding.value
}
})
// Agregar un componente global
app.component('T', {
props: ['key'],
setup: (props) => () => traducciones[idioma]?.[props.key] ?? props.key
})
}
}
// Uso
app.use(i18nPlugin, {
idioma: 'es',
traducciones: { es: { hola: 'Hola', mundo: 'Mundo' } }
})32. ¿Qué son las directivas custom y cuándo las usás?
Las directivas te dan acceso directo al DOM cuando los componentes son demasiado:
// Directiva v-focus: enfoca el elemento al montarse
const vFocus = {
mounted: (el) => el.focus()
}
// Directiva v-click-outside: detecta clicks fuera del elemento
const vClickOutside = {
mounted(el, binding) {
el._clickOutsideHandler = (event) => {
if (!el.contains(event.target)) {
binding.value(event) // Llama al handler pasado como valor
}
}
document.addEventListener('click', el._clickOutsideHandler)
},
unmounted(el) {
document.removeEventListener('click', el._clickOutsideHandler)
}
}<script setup>
// En <script setup>, las directivas empiezan con v minúscula
const vFocus = { mounted: (el) => el.focus() }
</script>
<template>
<input v-focus />
<div v-click-outside="cerrarMenu">Menú</div>
</template>33. ¿Cómo hacés testing de componentes Vue?
La herramienta estándar es Vue Test Utils + Vitest (o Jest):
// MiBoton.test.js
import { describe, it, expect, vi } from 'vitest'
import { mount } from '@vue/test-utils'
import MiBoton from './MiBoton.vue'
describe('MiBoton', () => {
it('renderiza el texto correcto', () => {
const wrapper = mount(MiBoton, {
props: { texto: 'Guardar' }
})
expect(wrapper.text()).toBe('Guardar')
})
it('emite el evento "click" al hacer click', async () => {
const wrapper = mount(MiBoton)
await wrapper.trigger('click')
expect(wrapper.emitted('click')).toBeTruthy()
expect(wrapper.emitted('click')).toHaveLength(1)
})
it('está deshabilitado cuando se pasa la prop disabled', () => {
const wrapper = mount(MiBoton, {
props: { disabled: true }
})
expect(wrapper.attributes('disabled')).toBeDefined()
})
})Para testear composables:
import { useContador } from './useContador'
it('incrementa correctamente', () => {
const { count, incrementar } = useContador(0)
expect(count.value).toBe(0)
incrementar()
expect(count.value).toBe(1)
})34. ¿Cómo implementás Server-Side Rendering (SSR) con Vue?
Las dos opciones principales:
1. Nuxt.js (framework completo, recomendado):
npx nuxi@latest init mi-app<!-- pages/usuarios/[id].vue — Nuxt auto-rutas -->
<script setup>
const route = useRoute()
const { data: usuario } = await useFetch(`/api/usuarios/${route.params.id}`)
</script>2. @vue/server-renderer (DIY):
import { createSSRApp } from 'vue'
import { renderToString } from '@vue/server-renderer'
const app = createSSRApp({
data: () => ({ count: 1 }),
template: `<button @click="count++">{{ count }}</button>`
})
const html = await renderToString(app)
// <button>1</button> — listo para enviar al browserConceptos clave para la entrevista:
- Hydration: el proceso de conectar el HTML generado en el servidor con la reactividad de Vue en el cliente
useAsyncData/useFetchen Nuxt: fetch que funciona tanto en servidor como en cliente- Estado compartido entre servidor y cliente: usá
useStatede Nuxt, no variables de módulo (se compartirían entre usuarios)
35. ¿Cómo funciona `<Suspense>` en Vue 3?
maneja componentes con setup functions asíncronas o componentes lazy-loaded:
<!-- El componente hijo puede tener un setup async -->
<!-- UsuarioDetalle.vue -->
<script setup>
const props = defineProps(['id'])
// Esto hace que el componente sea "async" — Suspense lo puede manejar
const usuario = await fetch(`/api/usuarios/${props.id}`).then(r => r.json())
</script>
<template>
<div>{{ usuario.nombre }}</div>
</template><!-- Padre -->
<template>
<Suspense>
<!-- Contenido principal — espera a que los async components estén listos -->
<UsuarioDetalle :id="1" />
<!-- Fallback — se muestra mientras carga -->
<template #fallback>
<div>Cargando usuario...</div>
</template>
</Suspense>
</template>Importante: todavía está en fase experimental. Para producción, muchos equipos prefieren manejar los estados de carga manualmente con ref(null) + watchEffect.
36. ¿Qué es `toRefs` y cuándo lo usás?
Cuando desestructurás un objeto reactivo, perdés la reactividad. toRefs convierte cada propiedad en una ref individual:
import { reactive, toRefs } from 'vue'
const estado = reactive({ nombre: 'Ana', edad: 28 })
// ❌ Desestructurar rompe la reactividad
const { nombre, edad } = estado
// nombre y edad son valores planos — no son reactivos
// ✅ toRefs mantiene la reactividad
const { nombre, edad } = toRefs(estado)
// nombre.value === 'Ana', y es reactivo// Muy útil en composables para poder desestructurar en el componente:
function useUsuario() {
const usuario = reactive({ nombre: '', email: '' })
// ...
return toRefs(usuario) // El componente puede desestructurar libremente
}
// En el componente:
const { nombre, email } = useUsuario() // Sigue siendo reactivo37. ¿Cómo usás `markRaw` y cuándo importa?
markRaw marca un objeto para que Vue nunca lo convierta en reactivo. Útil para:
- Objetos de librerías externas (Three.js, Chart.js) que no deben ser "proxificados"
- Objetos muy grandes donde la reactividad es un overhead innecesario
import { ref, markRaw } from 'vue'
import * as THREE from 'three'
const scene = ref(markRaw(new THREE.Scene()))
// Vue no va a envolver la Scene con un Proxy — Three.js maneja su propio estado38. ¿Qué es `effectScope` y para qué sirve?
effectScope agrupa efectos reactivos para poder pararlos todos juntos. Útil en plugins y composables que crean muchos efectos:
import { effectScope, ref, computed, watchEffect } from 'vue'
function useTheme() {
const scope = effectScope()
const tema = ref('claro')
let colorPrimario
scope.run(() => {
// Todos estos efectos quedan dentro del scope
colorPrimario = computed(() => tema.value === 'claro' ? '#000' : '#fff')
watchEffect(() => {
document.body.className = tema.value
})
})
// Cuando querés limpiar todo junto:
const cleanup = () => scope.stop()
return { tema, colorPrimario, cleanup }
}39. ¿Cómo manejás errores globales en Vue?
// En main.js
app.config.errorHandler = (err, instance, info) => {
// err: el error
// instance: el componente donde ocurrió
// info: hook de Vue donde ocurrió (e.g., 'mounted hook')
console.error('Error en Vue:', err, info)
// Enviar a Sentry, Datadog, etc.
sentryCapture(err, { extra: { info } })
}
// En componentes individuales (Vue 3 no tiene onErrorCaptured en <script setup>
// directamente, pero lo podés usar con Options API o el componente padre)
export default {
errorCaptured(err, instance, info) {
// Atrapa errores de componentes hijos
// Retornar false previene que el error suba
return false
}
}40. ¿Cuáles son las diferencias de migración más importantes de Vue 2 a Vue 3?
| Vue 2 | Vue 3 |
|---|---|
| new Vue({ el: '#app' }) | createApp(App).mount('#app') |
| Vue.set(obj, key, val) | obj.key = val (Proxy detecta todo) |
| Vue.observable() | reactive() |
| $on / $off / $emit | Mitt o pequeñas librerías de eventos |
| Múltiples root elements: no | Fragmentos: sí |
| data() → objeto | ref() / reactive() |
| Filtros (\| filter) | Métodos o computed |
| Vue.component() global | app.component() por instancia |
| Mixins para reutilizar lógica | Composables |
| $listeners / $attrs | Unificados en $attrs |
Breaking changes importantes:
v-modelen componentes: prop cambia devalueamodelValue, evento deinputaupdate:modelValuev-iftiene mayor prioridad quev-for(era al revés en Vue 2)- Los lifecycle hooks
beforeDestroyydestroyedse renombraron abeforeUnmountyunmounted
Preguntas bonus: patrones y arquitectura
41. ¿Cómo estructurás un proyecto Vue de escala media?
src/
├── assets/ # Imágenes, fuentes, estilos globales
├── components/ # Componentes reutilizables
│ ├── ui/ # Botones, inputs, cards (sin lógica de negocio)
│ └── shared/ # Componentes con lógica compartida
├── composables/ # useFetch, useAuth, useForm...
├── layouts/ # AppLayout.vue, AuthLayout.vue
├── pages/ (o views/)# Un archivo por ruta
├── router/ # index.js + guards
├── stores/ # Pinia stores por dominio
│ ├── auth.js
│ ├── usuario.js
│ └── productos.js
├── services/ # Llamadas a API (axios instances, etc.)
├── utils/ # Funciones puras sin reactividad
└── types/ # TypeScript interfaces42. ¿Cómo implementás infinite scroll en Vue?
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
const items = ref([])
const loading = ref(false)
const page = ref(1)
const observer = ref(null)
const sentinel = ref(null) // El elemento al final de la lista
async function cargarMas() {
if (loading.value) return
loading.value = true
const nuevos = await fetchItems(page.value++)
items.value.push(...nuevos)
loading.value = false
}
onMounted(() => {
observer.value = new IntersectionObserver(
(entries) => {
if (entries[0].isIntersecting) cargarMas()
},
{ threshold: 0.1 }
)
observer.value.observe(sentinel.value)
})
onUnmounted(() => observer.value?.disconnect())
</script>
<template>
<ul>
<li v-for="item in items" :key="item.id">{{ item.nombre }}</li>
</ul>
<div ref="sentinel">
<Spinner v-if="loading" />
</div>
</template>43. ¿Cómo validás formularios en Vue sin librerías externas?
<script setup>
import { ref, computed } from 'vue'
const form = ref({ email: '', password: '' })
const touched = ref({ email: false, password: false })
const errors = computed(() => ({
email: !form.value.email.includes('@') ? 'Email inválido' : '',
password: form.value.password.length < 8 ? 'Mínimo 8 caracteres' : ''
}))
const isValid = computed(() => Object.values(errors.value).every(e => !e))
function handleSubmit() {
// Marcar todos como tocados para mostrar todos los errores
touched.value = { email: true, password: true }
if (isValid.value) {
// Enviar formulario
}
}
</script>
<template>
<form @submit.prevent="handleSubmit">
<input v-model="form.email" @blur="touched.email = true" />
<p v-if="touched.email && errors.email" class="error">{{ errors.email }}</p>
<input v-model="form.password" type="password" @blur="touched.password = true" />
<p v-if="touched.password && errors.password" class="error">{{ errors.password }}</p>
<button type="submit" :disabled="!isValid">Enviar</button>
</form>
</template>Para formularios complejos, considerá VeeValidate o Zod + composable propio.
44. ¿Qué modifiers de eventos usás más en la práctica?
<!-- .prevent: preventDefault() -->
<form @submit.prevent="handleSubmit">
<!-- .stop: stopPropagation() -->
<button @click.stop="handleClick">
<!-- .once: se dispara una sola vez -->
<button @click.once="handleWelcome">
<!-- .self: solo si el click fue directo en este elemento (no en hijos) -->
<div @click.self="cerrarModal">
<!-- .enter: solo cuando se presiona Enter -->
<input @keyup.enter="buscar" />
<!-- .ctrl.shift: combinaciones de teclas -->
<input @keydown.ctrl.s="guardar" />
<!-- Encadenables: -->
<a @click.prevent.stop="handleLink">45. ¿Qué diferencia hay entre `$attrs`, `$props` y `$slots`?
<!-- BotonBase.vue — componente "transparente" -->
<script setup>
defineProps(['variante'])
// $attrs contiene todo lo que NO está en props/emits
// (class, style, event listeners, data-* attrs, etc.)
</script>
<template>
<!-- inheritAttrs: false en el script options si querés controlarlo manualmente -->
<!-- Por defecto, Vue pasa $attrs al root element automáticamente -->
<button
v-bind="$attrs"
:class="['btn', `btn-${variante}`]"
>
<slot />
</button>
</template>// En Vue 3 con <script setup>:
import { useAttrs, useSlots } from 'vue'
const attrs = useAttrs() // { class: 'extra', 'data-id': '42', onClick: fn }
const slots = useSlots() // { default: fn, icon: fn }
// Verificar si un slot existe:
if (slots.icon) {
// Renderizar icono
}Preguntas cortas para el final de la entrevista
¿Qué es nextTick?
Una promesa que se resuelve después de que Vue procese todas las actualizaciones pendientes del DOM. Usás await nextTick() cuando necesitás acceder al DOM después de cambiar el estado.
import { nextTick, ref } from 'vue'
const visible = ref(false)
const input = ref(null)
async function mostrarYEnfocar() {
visible.value = true
await nextTick() // Espera a que Vue renderice el input
input.value?.focus() // Ahora el input existe en el DOM
}¿Qué es keepAlive?
Un componente built-in que cachea instancias de componentes en lugar de destruirlas. Útil para tabs donde no querés perder el estado del formulario al cambiar de tab.
<KeepAlive :include="['TabA', 'TabB']" :max="5">
<component :is="tabActual" />
</KeepAlive>¿Qué hace el modificador .lazy en v-model?
En lugar de sincronizar en cada keystroke, sincroniza cuando el input pierde el foco (evento change). Útil para validaciones que no querés disparar mientras el usuario tipea.
Cómo usá esta guía para preparar tu entrevista
El conocimiento técnico es la mitad del trabajo. La otra mitad es poder reproducir estas respuestas bajo presión, con alguien mirándote y esperando que no te traben.
Lo que separa a los candidatos que pasan de los que no no es que saben más — es que sus respuestas salen solas, sin buscarlas. El código lo podés leer acá. Para que te salga en el momento de la entrevista, necesitás practicarlo en voz alta.
Tomá las 10 preguntas que más te costaron de esta lista y explicá cada una como si le hablaras a alguien que no sabe nada de Vue. Sin mirar. Si te trabás, volvé a leer y hacelo de nuevo.
Eso es lo que funciona.