Respuestas a entrevistas de React y JavaScript que evidencian un conocimiento realmente profundo.
Explora respuestas más sólidas y detalladas a las preguntas habituales en entrevistas de React y JavaScript, desde el Virtual DOM hasta el diseño de sistemas, que demuestran un juicio técnico más profundo.
Introducción: ¿Por qué la mayoría de los candidatos suenan igual?
Si asistes a suficientes entrevistas de React, notarás un patrón: las mismas frases preestablecidas aparecen una y otra vez. “El Virtual DOM es más rápido”. “useEffect se usa para efectos secundarios”. “JavaScript se ejecuta en un único hilo”. Ninguna de estas afirmaciones es falsa, pero son cosas que cualquiera podría recitar después de echar un vistazo a algunos artículos de blog, y no le dicen al entrevistador nada sobre cómo piensas realmente.
Lo que diferencia a un candidato sólido de aquel a quien le envían un correo de rechazo educado no es si conoce la definición estándar, sino si puede ir un nivel más allá. ¿Puedes relacionar un concepto con las trade-offs que implica? ¿Puedes explicar por qué se tomó una decisión de diseño, no solo qué hace? Ese es el tipo de juicio que realmente buscan los entrevistadores.
Esta guía aborda las preguntas que realmente surgen en las entrevistas de nivel intermedio y avanzado para desarrolladores front-end, acompañadas de respuestas lo suficientemente detalladas como para hacer que el entrevistador deje de hojear sus notas y preste atención de verdad.
Sección 1: Conceptos básicos de React
Pregunta 1: “Explique el Virtual DOM. ¿Cómo funciona?”
La respuesta olvidable: “El Virtual DOM es una copia ligera del DOM real. React compara los dos y solo actualiza lo que ha cambiado, lo que lo hace más rápido.”
La respuesta más sólida: el Virtual DOM es una abstracción, pero definirlo únicamente como “más rápido” pasa por alto el punto real. La ventaja real en rendimiento no proviene del Virtual DOM en sí, sino de la lógica de agrupamiento y reconciliación que se construye alrededor de él.
React mantiene un registro interno de dos árboles: el que se muestra actualmente en la pantalla y un árbol en proceso de creación que representa lo que está a punto de ser renderizado. Cuando el estado cambia, React no se apresura a modificar el DOM real de inmediato. En lugar de eso, construye el nuevo árbol Virtual DOM, realiza una comparación para determinar el conjunto más pequeño de cambios necesarios y luego aplica todos esos cambios al DOM real en una sola operación. Ese paso de agrupamiento es precisamente lo que evita cálculos repetidos del diseño, fenómeno a menudo denominado “layout thrashing”.
También hay una nuance que merece mención: el Virtual DOM no es algo gratuito. Su algoritmo de comparación se mantiene deliberadamente con complejidad O(n) al basarse en heurísticas, por ejemplo, asumiendo que los elementos de diferentes tipos generarán subárboles completamente distintos, en lugar de realizar una comparación de árboles totalmente generalizada de complejidad O(n³). Este compromiso permite que las operaciones sean rápidas en los casos comunes, pero implica que ciertas situaciones, como una lista enorme donde solo cambia un elemento, puedan seguir siendo costosas. Por eso existen herramientas como React.memo, useMemo y bibliotecas de virtualización de listas.
También cabe reconocer que el Virtual DOM está perdiendo algo de su atractivo como punto de venta único. Compiladores como el de Svelte omiten por completo el paso del Virtual DOM, y React en sí mismo se dirige hacia funciones de renderizado concurrente que modifican la forma en que tiene lugar la reconciliación en el fondo. El Virtual DOM resolvió un problema específico alrededor de 2013. Saber por qué se introdujo en primer lugar es más importante que poder recitar sus mecanismos.
Responder de esta manera funciona porque demuestra conciencia histórica, un reconocimiento honesto de los compromisos y familiaridad con la evolución del panorama general del frontend: no solo se responde a la pregunta literal, sino que también se demuestra que se entiende dónde encaja esta idea dentro del panorama más amplio.
Pregunta 2: “¿Cuál es la diferencia entre useEffect, useLayoutEffect y cuándo se usaría cada uno?”
La respuesta olvidable: “useEffect se ejecuta después del renderizado. useLayoutEffect se ejecuta antes de que el navegador dibuje la pantalla. Usa useLayoutEffect cuando necesites medir el DOM.”
La respuesta más precisa: la diferencia en los tiempos de ejecución es de conocimiento general, pero explicar por qué ese momento es importante es lo que distingue a una respuesta sólida. useEffect se dispara de forma asíncrona, después de que el navegador ya haya dibujado la pantalla. En contraste, useLayoutEffect se dispara de forma síncrona, justo después de que React termine de calcular las mutaciones en el DOM, pero antes de que el navegador tenga la oportunidad de dibujar.
Esa diferencia significa que useLayoutEffect realmente bloquea la actualización visual. Si colocas cálculos costosos dentro de él, el usuario percibirá una pantalla congelada. Por eso exactamente la documentación de React recomienda usar por defecto useEffect: bloquear el dibujo innecesariamente es una trampa común de rendimiento.
Aun así, existen razones legítimas para utilizar useLayoutEffect más allá de la simple “medición del DOM”. Un buen caso de uso es evitar parpadeos visibles. Imagine renderizar una herramienta de ayuda cuya posición depende de las dimensiones de un elemento objetivo: realizar ese cálculo de posicionamiento dentro de useEffect provoca un destello visible, en el que la herramienta aparece brevemente en el lugar incorrecto antes de colocarse en su posición adecuada. Ejecutar ese mismo cálculo dentro de useLayoutEffect evita por completo dicho destello, ya que tiene lugar antes de que el navegador dibuje cualquier cosa.
Existe un tercer hook en esta familia que muchos desarrolladores pasan por alto: useInsertionEffect. Su propósito es permitir que las herramientas CSS-in-JS inserten reglas de estilo en el documento antes del momento en que los efectos de maquetación leerían información de estilo obsoleta del DOM. La mayoría de los desarrolladores nunca lo necesitarán directamente, pero el simple hecho de saber que forma parte del ciclo de vida de los efectos en React indica una mayor familiaridad con la forma en que React 18 maneja los efectos en general.
Un entrevistador podría insistir y preguntar qué sucede si se utiliza useLayoutEffect durante la renderización del lado servidor. La respuesta es que React emitirá una advertencia, ya que no hay DOM disponible para medir en el servidor. Este hook simplemente no se ejecuta durante la SSR, por lo que cualquier lógica que dependa del DOM necesita contar con una protección exclusiva para el cliente o debe trasladarse a useEffect en su lugar.
Pregunta 3: “Explique el comportamiento de renderizado de React. ¿Cuándo vuelve a renderizarse un componente?”
La respuesta superficial: “Un componente vuelve a renderizarse cada vez que cambia su estado o sus props.”
La respuesta más precisa: eso es solo la superficie. La pregunta más interesante es qué se considera realmente un “cambio” y qué hace React una vez que detecta uno.
Un componente vuelve a renderizarse bajo tres condiciones:
- Cambia su propio estado local, generalmente a través de un setter de estado
- Su componente padre vuelve a renderizarse, independientemente de si los props transmitidos realmente han cambiado
- Cambia un valor del contexto que consume
La idea clave es el segundo punto: React no compara las props antes de decidir si debe volver a renderizar un componente hijo. Por diseño, si el padre se renderiza, sus hijos también se renderizan. Comparar las props implica un costo computacional, y en la mayoría de los casos reales el componente hijo necesita actualizarse de todos modos, por lo que omitir esa comparación por defecto es un compromiso razonable.
Es exactamente aquí donde los desarrolladores recurren a React.memo, a menudo de forma incorrecta. La memorización en sí tiene un costo: React aún debe realizar una comparación de las props durante cada renderizado. Si esas props son objetos complejos, o si el componente que se envuelve es barato de renderizar desde un principio, envolverlo en React.memo puede incluso perjudicar el rendimiento en lugar de ayudarlo.
La verdadera habilidad radica en saber cuándo realmente es necesario optimizar. Una regla práctica es evitar el uso de memorización hasta que se haya medido un problema concreto. Utilice el Profilador de React DevTools para identificar primero los cuellos de botella reales, y solo entonces aplique React.memo, useMemo o useCallback de manera dirigida. Optimizar prematuramente en React suele significar trabajar en contra del diseño del framework en lugar de junto a él.
El renderizado concurrente de React 18 añade otra capa a esto. Ahora los renders pueden ser interrumpidos, priorizados o incluso descartados en medio del proceso. Es esencial reconocer que un render no siempre se traduce directamente en una actualización síncrona del DOM para escribir código que funcione correctamente bajo renderizado concurrente.
Pregunta 4: ¿Cómo debe manejar la gestión de estado en una aplicación grande de React?
Una respuesta superficial trata esto como una cuestión relacionada con las herramientas: recurrir a Redux para cualquier cosa de carácter global y utilizar useState para lo que sea local.
Una respuesta más profunda comienza cuestionando qué tipo de estado está involucrado y quién depende realmente de él antes de elegir alguna biblioteca.
El estado en una aplicación grande generalmente se divide en cuatro categorías:
- Estado de la interfaz local: valores de formularios, controles de activación/desactivación, si un modal está abierto. Aquí
useStatees suficiente. - Estado del servidor: datos obtenidos desde el backend. React Query o SWR están diseñados para esto, ya que ya resuelven problemas como el caché, la deduplicación, la recarga en segundo plano y las actualizaciones optimistas, capacidades para las cuales Redux nunca fue diseñado.
Un error común es recurrir a Redux desde el inicio mismo de un proyecto. Redux es muy adecuado para lógica del lado del cliente realmente compleja con muchas actualizaciones interdependientes, pero la mayoría de las aplicaciones en realidad no tienen ese problema: lo que parece ser estado del cliente suele ser simplemente estado del servidor disfrazado. Almacenar respuestas de API dentro de Redux es comparable a usar un martillo pesado para colgar una imagen: se puede hacer, pero requiere mucho más esfuerzo del necesario para la tarea.
Cuando realmente está justificado usar Redux, combinar Redux Toolkit con RTK Query es un buen enfoque. RTK Query se encarga de las responsabilidades relacionadas con el estado del servidor, dejando que Redux gestione únicamente la lógica del cliente que realmente es complicada. Separar estas responsabilidades hace que la arquitectura general sea más fácil de seguir.
Sección 2: Análisis profundo de JavaScript
Pregunta 5: Explique los cierres en JavaScript. Dé un ejemplo práctico.
Una respuesta superficial describe un cierre como simplemente una función que mantiene las variables del ámbito que la contiene.
Una respuesta más profunda lo relaciona con el ámbito léxico: cuando se crea una función, esta captura referencias a las variables que la rodean en ese momento y conserva el acceso a ellas incluso después de que la ejecución se mueva fuera de ese ámbito original.
Lo que distingue a un buen candidato es su capacidad para explicar por qué este comportamiento es importante específicamente en React. Considere un patrón que con frecuencia causa errores:
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const timer = setInterval(() => {
console.log(count); // Always logs 0
setCount(count + 1); // Resets to 1 every time
}, 1000);
}, []); // Empty deps = closure over initial count
}
El count al que se hace referencia dentro de setInterval queda fijado en el valor que tenía durante la renderización inicial. Dado que el efecto se ejecuta solo una vez, gracias al array de dependencias vacío, esa cierre nunca se actualiza con valores más recientes. Simplemente añadir count a la lista de dependencias tampoco es la solución correcta, ya que eso provocaría desmontar y volver a crear el intervalo en cada actualización. La solución adecuada es utilizar la forma de actualización funcional, setCount(c => c + 1), lo cual evita depender por completo de esa cierre obsoleta.
Las cierres también generan fricción con los listeners de eventos dentro de los ganchos personalizados. Cada vez que se adjunta un listener dentro de useEffect y este lee desde el estado, está involucrada una cierre. Una técnica común para un gancho de estilo useEventListener es mantener la función de manejo dentro de un ref, de modo que el listener pueda leer siempre la versión más reciente sin necesidad de volver a adjuntarse.
Nada de esto hace que las cierres sean algo que deba evitarse; son un mecanismo fundamental que vale la pena dominar. Los patrones de módulos, las variables privadas, las funciones de fábrica y el currying dependen todas de ellas. La habilidad importante es reconocer exactamente cuándo se está formando una cierre y confirmar que esta captura el valor que realmente se pretende.
Pregunta 6: ¿Qué es el bucle de eventos? Explique las microtareas y las macrotareas.
Una respuesta superficial indica que el bucle de eventos gestiona el trabajo asíncrono y que las microtareas se ejecutan antes que las macrotareas.
Una respuesta más detallada explica que JavaScript se ejecuta en un único hilo, y que el navegador simula la concurrencia a través del bucle de eventos. El código síncrono se ejecuta en la pila de llamadas; cuando se encuentra una operación asíncrona, esta se pasa a una API web —setTimeout, fetch, eventos DOM— y una vez que esa operación finaliza, su función de callback se coloca en una cola.
La sutileza que cabe mencionar es que no existe una sola cola. Las macrotareas —setTimeout, setInterval, operaciones de E/S— van a una cola, mientras que las microtareas —Promise.then, queueMicrotask, MutationObserver— van a otra. Una vez que la pila de llamadas está vacía, el bucle de eventos vacía toda la cola de microtareas antes de procesar siquiera una sola macrotask.
Esto crea un riesgo real: las microtareas pueden dejar sin recursos al resto del programa. Si nuevas microtareas siguen siendo encoladas de forma recursiva, las devoluciones pendientes de setTimeout nunca llegan a su turno. Este tipo de situación puede congelar la interfaz de usuario cuando las Promises se encadenan en un bucle sin devolver nunca el control al navegador.
Esto está directamente relacionado con la forma en que React agrupa las actualizaciones de estado. En React 18, las actualizaciones de estado se agrupan automáticamente sin importar de dónde provengan: desde dentro de setTimeout, desde dentro de una Promise o desde dentro de un manejador de eventos nativo. Antes de React 18 no era así, ya que las actualizaciones iniciadas dentro de setTimeout se aplicaban una por una en lugar de agruparse. Comprender el bucle de eventos aclara por qué el agrupamiento automático en React 18 es importante: se conecta a la cola de microtareas para que todas las actualizaciones pendientes se procesen juntas antes de que ocurra el siguiente renderizado.
También es posible que se le pida predecir el resultado de un fragmento corto como este:
console.log('1');
setTimeout(() => console.log('2'), 0);
Promise.resolve().then(() => console.log('3'));
console.log('4');
La respuesta esperada es: 1, 4, 3, 2 — las instrucciones síncronas se ejecutan primero, seguidas por las microtareas como la devolución de llamada de la Promise, y solo después se ejecuta la macrotarea encolada por setTimeout.
Pregunta 7: “Explique this en JavaScript. ¿En qué se diferencia de otros lenguajes?”
La respuesta superficial: “this apunta al objeto que llamó a la función.”
La respuesta completa: “this en JavaScript sigue un ámbito dinámico en lugar de uno léxico. La mayoría de los lenguajes fijan self o this en el momento en que se define la función. JavaScript, en cambio, lo resuelve en el momento de la llamada, según cómo se invoque la función y no según su ubicación en el código fuente.”
“Existe un orden de prioridad de cuatro reglas de enlace:
- Enlace nuevo:
new Foo()establecethisen la instancia recién creada - Enlace explícito:
foo.call(obj),foo.apply(obj)ofoo.bind(obj)obligan a quethissea el objeto que se pasa como parámetro
obj.foo(), this se vuelve igual a objfoo() deja this como undefined en modo estricto, o recurre a globalThis en otros casos""Las funciones de flecha rompen deliberadamente este patrón: heredan this léxicamente del ámbito que las rodea. Precisamente por eso los desarrolladores utilizaron funciones de flecha dentro de componentes React basados en clases antes de que existieran los hooks: así evitaban la necesidad de llamar a .bind(this) dentro del constructor."
"El código de React basado en hooks rara vez accede directamente a this, ya que los componentes ya no son clases. Sin embargo, este concepto vuelve a surgir cuando se mantienen componentes de clase heredados, se integran bibliotecas de terceros o se enfrenta a un entrevistador que quiere comprobar el conocimiento de los fundamentos de JavaScript. Una trampa común en la práctica es pasar un método de objeto como callback, por ejemplo, entregar obj.handleClick a un listener de eventos, lo cual elimina su vinculación implícita y hace que this apunte a un lugar inesperado."
Pregunta 8: "¿Qué son las Promesas de JavaScript? Explique async/await."
La respuesta superficial: "Las Promesas gestionan el trabajo asíncrono, y async/await es simplemente un recurso sintáctico adicional sobre ellas."
La respuesta definitiva: “Una Promise representa un valor que aún no existe pero que eventualmente se resolverá. Reemplaza las complejas cadenas de callbacks por una interfaz encadenable y un método consistente para indicar éxito o fracaso.”
“Lo que hace realmente útiles a las Promises no es su sintaxis, sino las garantías que ofrecen. Una vez que una Promise se resuelve —ya sea cumplida o rechazada— ese resultado queda fijo y no puede cambiar nunca. Esa inmutabilidad es lo que las hace componibles y fáciles de comprender.”
“Calificar a async/await como ‘solo un adorno’ subestima su importancia; transforma fundamentalmente la forma en que se escribe la lógica asíncrona, permitiendo que se lea como código síncrono y facilitando su comprensión. No obstante, introduce algunas trampas a las que hay que prestar atención.”
// This runs sequentially - 6 seconds total
async function sequential() {
const a = await fetch('/a'); // 3s
const b = await fetch('/b'); // 3s
}
// This runs in parallel - 3 seconds total
async function parallel() {
const [a, b] = await Promise.all([fetch('/a'), fetch('/b')]);
}
"Un error frecuente entre los desarrolladores menos experimentados es colocar await dentro de un bucle, lo que fuerza involuntariamente a que las operaciones se ejecuten una tras otra en lugar de en paralelo. La solución es utilizar Promise.all siempre que las operaciones no dependan unas de otras, y reservar un bucle for...of con await para los casos en los que se requiera realmente una secuencia estricta."
"El manejo de errores es otro área donde surgen problemas. Envolver una llamada a await en try/catch permitirá capturar sus rechazos, pero omitir ese envoltorio puede hacer que un rechazo no manejado provoque la caída de un proceso Node.js. En el frontend, al envolver las llamadas asíncronas en límites de errores o al utilizar una biblioteca como React Query que gestiona los estados de error de forma declarativa, se evita este tipo de fallo."
Sección 3: Diseño y arquitectura del sistema
Pregunta 9: “Diseñe un editor de documentos colaborativo en tiempo real como Google Docs.”
Esta pregunta cambia por completo el enfoque de la entrevista. El entrevistador ya no está preguntando sobre conocimientos básicos de React; quiere ver cómo razonas acerca de la arquitectura del sistema.
Un enfoque sólido sería el siguiente:
“Antes de escribir una sola línea de código, definiría primero los requisitos:
- ¿Cuántas personas editarán simultáneamente? El diseño para 10 usuarios concurrentes es muy diferente al diseño para 10,000.
- ¿Qué nivel de latencia es aceptable: verdadero tiempo real o algo más cercano al casi tiempo real?
- ¿La aplicación necesita funcionar sin conexión?
- ¿Qué estrategia de resolución de conflictos adoptaremos?”
“En el lado frontend:
- Gestión de estado: cada cliente mantiene su propia copia local del documento. Las ediciones se aplican de forma optimista primero en el cliente y luego se envían al servidor, que las distribuye a todos los demás clientes conectados.
- Transformación operativa o CRDTs: este es el mecanismo para resolver ediciones conflictivas. La OT fue la técnica original de Google, pero depende de un servidor central para arbitrar el orden. Los CRDTs (Tipos de datos replicados sin conflictos) pueden funcionar en modo peer-to-peer, y herramientas como Yjs los han hecho cada vez más comunes.
requestAnimationFrame y aplicar debounce a las sincronizaciones de red para no enviar una solicitud por cada tecla pulsada."En la capa de sincronización:
- WebSocket se encarga del transporte en tiempo real
- Server-Sent Events o long-polling sirven como solución alternativa si no se puede establecer la conexión WebSocket
"La parte realmente difícil de este problema no tiene nada que ver con la renderización en React: se trata del modelo de consistencia subyacente. Cuando dos personas escriben en la misma posición exacta del cursor al mismo instante, ¿qué debería ocurrir? La respuesta depende por completo de si se ha elegido OT o CRDTs, y esa única decisión influye en casi todas las demás decisiones arquitectónicas posteriores."
Pregunta 10: "¿Cómo se optimiza una aplicación React que carga e interactúa lentamente?"
La respuesta insatisfactoria: enumerar una serie de tácticas —memorización, carga diferida, división de paquetes— sin ningún marco conceptual.
La respuesta que destaca: “Empezaría midiendo en lugar de adivinando. El perfilador de React DevTools y el panel de rendimiento de Chrome DevTools indican si el cuello de botella es el tiempo de carga, el tiempo de renderizado o ambos; no tiene sentido optimizar a ciegas.”
En cuanto a la carga:
- División del código: La división basada en rutas mediante React.lazy y Suspense es la base, pero no debería detenerse allí. Los componentes pesados que no son visibles de inmediato —modales, contenido debajo del despliegue— también merecen tener sus propios puntos de división.
- Carga previa: Utilice
<link rel="preload">para los recursos críticos, y combine React.lazy con indicaciones de prefetching para las rutas que el usuario probablemente visitará a continuación.
import lodash from 'lodash' en lugar de import debounce from 'lodash/debounce' puede marcar la diferencia de 100 KB en tu paquete final.En cuanto a la interacción:
- Virtualización: Cuando una lista supera aproximadamente 50 elementos, utiliza react-window o react-virtualized. Renderizar 10,000 nodos DOM al mismo tiempo nunca será rápido, sin importar cuán eficiente sea el resto de tu código.
- Una estrategia disciplinada de memorización: primero analice y luego actúe. Envuelva las operaciones computacionales costosas en
useMemo, las funciones de callback costosas enuseCallback, y los componentes que se vuelven a renderizar innecesariamente enReact.memo. Memorizar todo por defecto sin medir el impacto real suele añadir carga adicional en lugar de reducirla. - Colocación del estado: mantenga el estado lo más cerca posible del componente que realmente lo utiliza. Elevar el estado a un ancestro compartido solo porque parece más ordenado provoca renders adicionales cada vez que ese estado cambia.
- División de contextos: cuando un único contexto mezcla actualizaciones de alta frecuencia — como la posición del mouse — con actualizaciones de baja frecuencia — como el estado de autenticación — divídalo en dos. De lo contrario, cada movimiento del mouse obliga a volver a renderizar todos los consumidores, incluso aquellos que solo se interesan por la autenticación.
Sobre el rendimiento percibido:
- Las pantallas esqueléticas en lugar de los indicadores de carga hacen que la interfaz parezca más rápida, ya que el contenido parece cargarse progresivamente en lugar de aparecer de golpe.
- La carga progresiva mediante los límites
Suspensede React 18 permite que el contenido crítico se cargue primero, mientras que las secciones secundarias lo hacen después. - Vale la pena monitorear el indicador Interaction to Next Paint, un nuevo métrico de Core Web Vital de Google. Está reemplazando a First Input Delay, ya que mide la capacidad de respuesta durante todo el ciclo de vida de la página y no solo en la primera interacción. El objetivo es que los controladores de eventos se ejecuten en menos de 200 milisegundos.
Lecturas relacionadas
- Errores en la arquitectura backend que complican a las equipos de React con enfoque en el frontend — Explica cinco defectos comunes en el diseño del backend en proyectos liderados por React, desde el uso incorrecto del paradigma API hasta despliegues frágiles, así como las soluciones arquitectónicas para lograr una fiabilidad de nivel profesional.
- Manejo de estados UI en el mundo real con el renderizado condicional de React — Aprenda cómo crear interfaces de autenticación, roles, permisos, carga, errores y estado vacío en React utilizando patrones prácticos de renderizado condicional.