Preguntas de entrevista de React con código (50+ con respuestas)
Esta guía cubre más de 50 preguntas reales que aparecen en entrevistas técnicas de React — de junior a senior. Cada respuesta incluye código que podés ejecutar, la explicación del *por qué*, y lo que el entrevistador realmente quiere escuchar. Sin relleno.
NIVEL JUNIOR — Fundamentos y JSX
1. ¿Qué es JSX y a qué se transforma en tiempo de compilación?
JSX no es HTML. Es syntactic sugar sobre React.createElement. Babel lo transforma así:
// Lo que escribís
const el = <h1 className="titulo">Hola</h1>;
// Lo que ve el motor JavaScript
const el = React.createElement("h1", { className: "titulo" }, "Hola");Lo que busca el entrevistador: que entiendas que JSX es una abstracción, no HTML real. Si lo sabés, podés razonar sobre por qué className en lugar de class, por qué las expresiones van entre {}, y por qué un componente tiene que empezar con mayúscula.
2. ¿Cuál es la diferencia entre un componente funcional y uno de clase?
// Componente de clase (legacy)
class Saludo extends React.Component {
render() {
return <p>Hola, {this.props.nombre}</p>;
}
}
// Componente funcional (moderno — preferido)
function Saludo({ nombre }) {
return <p>Hola, {nombre}</p>;
}Los componentes funcionales son más simples, no necesitan this, y desde React 16.8 soportan estado y ciclo de vida con hooks. Los de clase siguen funcionando pero son legacy — en entrevistas es raro que te pidan escribir uno nuevo.
3. ¿Qué son las props y por qué son inmutables?
function Tarjeta({ titulo, descripcion }) {
// titulo y descripcion NO se pueden modificar acá
return (
<div>
<h2>{titulo}</h2>
<p>{descripcion}</p>
</div>
);
}
// Uso
<Tarjeta titulo="React hooks" descripcion="Cambiaron todo en 2019" />Las props son el contrato entre padre e hijo. Si el hijo pudiera mutarlas, el flujo de datos se volvería impredecible. El padre es el único responsable de cambiarlas (pasando nuevas props).
4. ¿Para qué sirve la prop `key` en listas?
// MAL — React no puede reconciliar eficientemente
users.map(u => <li>{u.nombre}</li>)
// BIEN
users.map(u => <li key={u.id}>{u.nombre}</li>)
// EVITAR — usar el índice si la lista puede reordenarse
users.map((u, i) => <li key={i}>{u.nombre}</li>)key le dice al algoritmo de reconciliación de React qué elemento es qué cuando la lista cambia. Sin key estable, React puede reutilizar el DOM incorrecto y generar bugs visuales raros — especialmente con inputs o animaciones.
5. ¿Qué hace `useState`? Mostrá un ejemplo con counter.
import { useState } from "react";
function Counter() {
const [count, setCount] = useState(0); // valor inicial: 0
return (
<div>
<p>Clicks: {count}</p>
<button onClick={() => setCount(count + 1)}>+1</button>
<button onClick={() => setCount(prev => prev - 1)}>-1</button>
</div>
);
}Nota importante: cuando el próximo estado depende del anterior, usá la forma funcional setCount(prev => prev - 1). Si usás setCount(count - 1) dentro de un closure viejo, podés leer un valor obsoleto.
6. ¿Qué es el virtual DOM y por qué importa?
React mantiene una representación ligera del DOM en memoria (el virtual DOM). Cuando el estado cambia, React:
- 1Genera un nuevo virtual DOM
- 2Lo compara con el anterior (diffing)
- 3Aplica solo los cambios reales al DOM del browser (reconciliación)
Esto es más eficiente que reescribir todo el DOM real en cada cambio. No significa que sea siempre la solución más rápida — herramientas como Svelte no usan virtual DOM — pero en la práctica React es lo suficientemente rápido para la mayoría de los casos.
7. ¿Cómo se maneja un formulario controlado?
import { useState } from "react";
function Formulario() {
const [email, setEmail] = useState("");
function handleSubmit(e) {
e.preventDefault();
console.log("Email enviado:", email);
}
return (
<form onSubmit={handleSubmit}>
<input
type="email"
value={email} // React controla el valor
onChange={e => setEmail(e.target.value)}
/>
<button type="submit">Enviar</button>
</form>
);
}En un formulario controlado, el estado de React es la "fuente de verdad". La alternativa (no controlado) usa useRef para leer el valor solo cuando lo necesitás — útil en formularios simples o cuando integrás librerías externas.
8. ¿Qué es el prop drilling y cuándo se vuelve un problema?
// Prop drilling: pasás `user` por 3 niveles solo para llegar a Avatar
function App() {
const user = { nombre: "Lucía" };
return <Layout user={user} />;
}
function Layout({ user }) {
return <Header user={user} />;
}
function Header({ user }) {
return <Avatar user={user} />;
}
function Avatar({ user }) {
return <img alt={user.nombre} />;
}Se vuelve problema cuando tenés más de 2-3 niveles, porque los componentes intermedios reciben props que no usan. Soluciones: Context, Zustand, o composición de componentes.
NIVEL MEDIO — Hooks y efectos
9. Explicá `useEffect` con dependency array.
import { useState, useEffect } from "react";
function Perfil({ userId }) {
const [perfil, setPerfil] = useState(null);
useEffect(() => {
// Corre cuando userId cambia
fetch(`/api/users/${userId}`)
.then(r => r.json())
.then(setPerfil);
// Función de limpieza — importante en fetches
return () => { /* cancelar request si el componente se desmonta */ };
}, [userId]); // <- dependency array
if (!perfil) return <p>Cargando...</p>;
return <p>{perfil.nombre}</p>;
}[]= corre solo al montar[userId]= corre al montar y cuando userId cambia- Sin array = corre en cada render (casi siempre un bug)
10. ¿Qué es el "stale closure" y cómo lo evitás?
// BUG: closure viejo captura count = 0
function Timer() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setCount(count + 1); // count SIEMPRE es 0 acá
}, 1000);
return () => clearInterval(id);
}, []); // dependency array vacío = closure no se actualiza
return <p>{count}</p>;
}
// FIX: forma funcional — no necesita leer count del closure
useEffect(() => {
const id = setInterval(() => {
setCount(prev => prev + 1); // lee el valor actual
}, 1000);
return () => clearInterval(id);
}, []);El stale closure es uno de los bugs más comunes en React. Aparece cuando un efecto o callback "recuerda" un valor viejo porque no está en el array de dependencias.
11. ¿Para qué sirve `useCallback`? ¿Cuándo lo usás?
import { useState, useCallback } from "react";
function Padre() {
const [count, setCount] = useState(0);
// Sin useCallback: nueva referencia en cada render → Hijo se re-renderiza siempre
// Con useCallback: misma referencia si las deps no cambian
const handleClick = useCallback(() => {
console.log("click");
}, []); // deps vacías = la función nunca cambia
return (
<div>
<button onClick={() => setCount(c => c + 1)}>+{count}</button>
<Hijo onAction={handleClick} />
</div>
);
}
const Hijo = React.memo(function Hijo({ onAction }) {
console.log("Hijo renderizó"); // Solo si onAction cambió
return <button onClick={onAction}>Acción</button>;
});useCallback solo tiene sentido combinado con React.memo. Solo en componentes que renderizan mucho o reciben funciones como props — no lo agregues por defecto.
12. ¿Qué hace `useMemo`? Ejemplo real.
import { useMemo } from "react";
function ListaFiltrada({ items, filtro }) {
// Sin useMemo: filtra en cada render aunque items y filtro no cambien
const filtrados = useMemo(
() => items.filter(item => item.nombre.includes(filtro)),
[items, filtro] // solo recalcula si cambia alguno de estos
);
return (
<ul>
{filtrados.map(i => <li key={i.id}>{i.nombre}</li>)}
</ul>
);
}Usalo cuando la computación es costosa (filtrar/ordenar listas grandes, transformaciones complejas). Para cálculos simples, el overhead de useMemo es mayor que el beneficio.
13. ¿Cómo funciona `useRef`? ¿Para qué sirve además del DOM?
import { useRef, useEffect } from "react";
function AutoFocus() {
const inputRef = useRef(null);
useEffect(() => {
inputRef.current.focus(); // acceso directo al DOM
}, []);
return <input ref={inputRef} />;
}
// También útil para guardar valores que NO deben causar re-render
function Cronometro() {
const [tiempo, setTiempo] = useState(0);
const intervalRef = useRef(null); // no re-renderiza cuando cambia
function start() {
intervalRef.current = setInterval(() => setTiempo(t => t + 1), 1000);
}
function stop() {
clearInterval(intervalRef.current);
}
return <button onClick={start}>Start</button>;
}useRef devuelve un objeto { current } que persiste entre renders sin causar re-render. Perfecto para IDs de timers, instancias de librerías externas, o valores previos.
14. ¿Cómo guardás el valor previo de un estado?
import { useState, useRef, useEffect } from "react";
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value; // actualiza DESPUÉS del render
});
return ref.current; // devuelve el valor ANTES del render actual
}
function Precio({ valor }) {
const valorAnterior = usePrevious(valor);
const subio = valor > valorAnterior;
return (
<p style={{ color: subio ? "green" : "red" }}>
${valor} (antes: ${valorAnterior})
</p>
);
}15. ¿Qué es `useReducer` y cuándo preferís usarlo sobre `useState`?
import { useReducer } from "react";
const initialState = { count: 0, loading: false, error: null };
function reducer(state, action) {
switch (action.type) {
case "INCREMENT":
return { ...state, count: state.count + 1 };
case "SET_LOADING":
return { ...state, loading: action.payload };
case "SET_ERROR":
return { ...state, error: action.payload, loading: false };
default:
return state;
}
}
function Componente() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<button onClick={() => dispatch({ type: "INCREMENT" })}>
Count: {state.count}
</button>
);
}Preferí useReducer cuando tenés varios sub-valores relacionados, cuando el próximo estado depende del anterior de forma compleja, o cuando querés que la lógica sea testeable independientemente.
16. ¿Cómo funciona Context y cuáles son sus limitaciones?
import { createContext, useContext, useState } from "react";
const TemaContext = createContext("claro");
function App() {
const [tema, setTema] = useState("claro");
return (
<TemaContext.Provider value={tema}>
<Layout />
</TemaContext.Provider>
);
}
function Boton() {
const tema = useContext(TemaContext);
return <button className={tema}>Click</button>;
}Limitación clave: cualquier cambio en el Provider re-renderiza TODOS los consumidores, aunque solo cambió una parte del contexto. Para estado frecuente, usá Zustand o separá en múltiples contextos.
17. ¿Qué es un custom hook? Escribí uno real.
import { useState, useEffect } from "react";
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
let cancelado = false;
setLoading(true);
fetch(url)
.then(r => r.json())
.then(d => { if (!cancelado) setData(d); })
.catch(e => { if (!cancelado) setError(e); })
.finally(() => { if (!cancelado) setLoading(false); });
return () => { cancelado = true; };
}, [url]);
return { data, loading, error };
}
// Uso
function Usuarios() {
const { data, loading, error } = useFetch("/api/users");
if (loading) return <p>Cargando...</p>;
if (error) return <p>Error: {error.message}</p>;
return <ul>{data.map(u => <li key={u.id}>{u.nombre}</li>)}</ul>;
}Los custom hooks extraen lógica stateful y la hacen reutilizable. La regla: si dos componentes comparten lógica de estado, probablemente debería ser un hook.
18. ¿Qué es el reconciliador de React y cómo decide qué re-renderizar?
React usa el algoritmo Fiber para decidir qué actualizar. Las reglas principales:
- 1Si el tipo del elemento cambia (→
), destruye el árbol y crea uno nuevo- 2Si el tipo es el mismo, actualiza las props
- 3Los hijos se comparan por
keyjsx// React destruye y recrea porque el tipo cambió {mostrar ? <Input /> : <Textarea />} // React PRESERVA el estado porque el tipo es igual (solo cambian props) {mostrar ? <Input placeholder="A" /> : <Input placeholder="B" />}19. ¿Qué diferencia hay entre `useEffect` y `useLayoutEffect`?
jsxuseEffect(() => { // Corre DESPUÉS de que el browser pintó la pantalla // No bloquea la pintura — preferido por defecto }); useLayoutEffect(() => { // Corre ANTES de que el browser pinte // Bloquea la pintura — usar solo si necesitás medir DOM const rect = elementRef.current.getBoundingClientRect(); setTooltipPosition(rect); });Uso real de
useLayoutEffect: calcular dimensiones del DOM para posicionar tooltips o menús. En el 99% de los casos,useEffectes lo correcto.20. ¿Cómo hacés data fetching moderno con Suspense?
jsximport { Suspense } from "react"; import { useSuspenseQuery } from "@tanstack/react-query"; function DatosUsuario({ id }) { const { data } = useSuspenseQuery({ queryKey: ["user", id], queryFn: () => fetch(`/api/users/${id}`).then(r => r.json()), }); return <p>{data.nombre}</p>; } function App() { return ( <Suspense fallback={<p>Cargando...</p>}> <DatosUsuario id={1} /> </Suspense> ); }NIVEL SEMI-SENIOR — Patrones y performance
21. ¿Cómo optimizás un componente que renderiza demasiado?
jsx// 1. React.memo para componentes puros const Item = React.memo(function Item({ nombre, precio }) { return <div>{nombre}: ${precio}</div>; }); // 2. Estabilizá los objetos/arrays que pasás como props function Lista() { // MAL: nuevo objeto en cada render → Item siempre re-renderiza return <Item config={{ bold: true }} />; // BIEN: misma referencia const config = useMemo(() => ({ bold: true }), []); return <Item config={config} />; }Primero medí con React DevTools Profiler. El 80% de las veces el cuello de botella no está donde creés.
22. ¿Qué es el Error Boundary y cómo se implementa?
jsximport { Component } from "react"; class ErrorBoundary extends Component { state = { hasError: false, error: null }; static getDerivedStateFromError(error) { return { hasError: true, error }; } componentDidCatch(error, info) { console.error("Error capturado:", error, info.componentStack); } render() { if (this.state.hasError) { return ( <p> Algo salió mal.{" "} <button onClick={() => this.setState({ hasError: false })}> Reintentar </button> </p> ); } return this.props.children; } } // Uso <ErrorBoundary> <ComponenteQuePuedeFallar /> </ErrorBoundary>Los Error Boundaries NO capturan errores en event handlers, código asíncrono, ni SSR — solo en el renderizado.
23. ¿Cómo implementás code splitting y lazy loading?
jsximport { lazy, Suspense } from "react"; const Dashboard = lazy(() => import("./pages/Dashboard")); const Reportes = lazy(() => import("./pages/Reportes")); function App() { return ( <Suspense fallback={<div>Cargando página...</div>}> <Routes> <Route path="/dashboard" element={<Dashboard />} /> <Route path="/reportes" element={<Reportes />} /> </Routes> </Suspense> ); }Con Next.js:
next/dynamichace lo mismo con más control (SSR, loading state,ssr: falsepara libs que necesitan el browser).24. ¿Qué es el patrón Compound Component?
jsximport { createContext, useContext, useState } from "react"; const AccordionContext = createContext(); function Accordion({ children }) { const [open, setOpen] = useState(null); return ( <AccordionContext.Provider value={{ open, setOpen }}> <div>{children}</div> </AccordionContext.Provider> ); } Accordion.Item = function Item({ id, title, children }) { const { open, setOpen } = useContext(AccordionContext); const isOpen = open === id; return ( <div> <button onClick={() => setOpen(isOpen ? null : id)}>{title}</button> {isOpen && <div>{children}</div>} </div> ); }; // Uso declarativo <Accordion> <Accordion.Item id="1" title="Sección 1">Contenido A</Accordion.Item> <Accordion.Item id="2" title="Sección 2">Contenido B</Accordion.Item> </Accordion>25. ¿Cómo manejás formularios complejos sin librerías?
jsximport { useReducer } from "react"; function formReducer(state, action) { switch (action.type) { case "SET_FIELD": return { ...state, [action.field]: action.value }; case "SET_ERROR": return { ...state, errors: { ...state.errors, [action.field]: action.msg } }; case "RESET": return initialState; default: return state; } } const initialState = { nombre: "", email: "", errors: {} }; function Formulario() { const [state, dispatch] = useReducer(formReducer, initialState); function handleChange(e) { dispatch({ type: "SET_FIELD", field: e.target.name, value: e.target.value }); } return ( <form onSubmit={e => { e.preventDefault(); submitForm(state); }}> <input name="nombre" value={state.nombre} onChange={handleChange} /> <input name="email" value={state.email} onChange={handleChange} /> {state.errors.email && <span>{state.errors.email}</span>} </form> ); }26. ¿Qué es `forwardRef` y cuándo lo necesitás?
jsximport { forwardRef } from "react"; const Input = forwardRef(function Input({ label, ...props }, ref) { return ( <label> {label} <input ref={ref} {...props} /> </label> ); }); // Uso function Formulario() { const inputRef = useRef(); useEffect(() => { inputRef.current.focus(); }, []); return <Input ref={inputRef} label="Email" type="email" />; }Necesitás
forwardRefcuando creás una librería de componentes UI y los consumidores necesitan acceso al DOM interno.27. ¿Cómo manejás race conditions en fetches concurrentes?
jsxfunction Busqueda({ query }) { const [resultado, setResultado] = useState(null); const latestQuery = useRef(query); useEffect(() => { latestQuery.current = query; fetch(`/api/search?q=${query}`) .then(r => r.json()) .then(data => { if (query === latestQuery.current) { setResultado(data); } }); }, [query]); return <div>{resultado?.nombre}</div>; }La alternativa más limpia:
AbortControllerpara cancelar el fetch anterior cuando llega uno nuevo.28. ¿Cómo probás un componente con React Testing Library?
jsximport { render, screen, fireEvent } from "@testing-library/react"; import Counter from "./Counter"; test("incrementa el contador al hacer click", () => { render(<Counter />); const button = screen.getByRole("button", { name: /\+1/i }); fireEvent.click(button); expect(screen.getByText(/Clicks: 1/i)).toBeInTheDocument(); });Filosofía de RTL: testeá lo que el usuario ve y hace, no los detalles de implementación. Evitá
getByTestId— preferí roles y texto.29. ¿Qué bug tiene este código de useEffect?
jsx// BUG: loop infinito function Componente() { const [datos, setDatos] = useState([]); useEffect(() => { fetch("/api/datos") .then(r => r.json()) .then(d => setDatos(d)); }, [datos]); // <- BUG: datos está en las deps return <div>{datos.length}</div>; }datoscambia → efecto corre →setDatoscambiadatos→ efecto corre → loop infinito. Fix: usar[]si solo necesitás fetchear al montar, o la dependencia que realmente dispara el refetch (un ID, un filtro), nunca el resultado.30. ¿Qué problema tiene este código con async/await?
jsx// BUG: useEffect callback no puede ser async useEffect(async () => { const data = await fetchData(); setData(data); // la función de limpieza nunca se llama correctamente }, []); // FIX: función async adentro useEffect(() => { let cancelado = false; async function cargar() { const data = await fetchData(); if (!cancelado) setData(data); } cargar(); return () => { cancelado = true; }; }, []);useEffectespera que el callback devuelva una función de limpieza oundefined. Una función async devuelve una Promise, lo que React ignora y genera warnings en consola.NIVEL SENIOR — Arquitectura y performance avanzada
31. ¿Qué es la concurrencia en React y qué es `startTransition`?
jsximport { useState, useTransition } from "react"; function Busqueda() { const [input, setInput] = useState(""); const [resultados, setResultados] = useState([]); const [isPending, startTransition] = useTransition(); function handleChange(e) { setInput(e.target.value); // urgente: se actualiza de inmediato startTransition(() => { // no urgente: React puede interrumpir este render setResultados(buscarEn(data, e.target.value)); }); } return ( <div> <input value={input} onChange={handleChange} /> {isPending ? <p>Actualizando...</p> : <Lista items={resultados} />} </div> ); }32. ¿Cómo manejás el estado global sin Redux?
jsximport { create } from "zustand"; const useStore = create(set => ({ usuario: null, setUsuario: usuario => set({ usuario }), carrito: [], agregarAlCarrito: item => set(state => ({ carrito: [...state.carrito, item] })), })); // Re-render granular — cada componente lee solo lo que necesita function Header() { const usuario = useStore(state => state.usuario); return <p>{usuario?.nombre}</p>; }Zustand es la opción dominante en 2026 para proyectos medianos. Jotai para estado atómico granular. Redux Toolkit para equipos grandes con estado muy complejo.
33. ¿Cómo implementás infinite scroll con Intersection Observer?
jsxfunction ListaInfinita() { const [items, setItems] = useState([]); const [page, setPage] = useState(1); const bottomRef = useRef(); useEffect(() => { fetch(`/api/items?page=${page}`) .then(r => r.json()) .then(data => setItems(prev => [...prev, ...data])); }, [page]); useEffect(() => { const observer = new IntersectionObserver(entries => { if (entries[0].isIntersecting) { setPage(p => p + 1); } }); if (bottomRef.current) observer.observe(bottomRef.current); return () => observer.disconnect(); }, []); return ( <div> {items.map(i => <Item key={i.id} data={i} />)} <div ref={bottomRef} style={{ height: 1 }} /> </div> ); }34. ¿Cómo virtualizás una lista de 10.000 items?
jsximport { FixedSizeList } from "react-window"; const Row = ({ index, style, data }) => ( <div style={style}> {data[index].nombre} </div> ); function ListaGrande({ items }) { return ( <FixedSizeList height={600} itemCount={items.length} itemSize={50} width="100%" itemData={items} > {Row} </FixedSizeList> ); }Virtualización = solo renderizás los items visibles + un pequeño buffer. Para 10k items es la diferencia entre 16ms y 2000ms de render.
35. ¿Qué son los Server Components y cómo difieren de los Client Components?
jsx// Server Component (Next.js app router — default) // Se ejecuta en el servidor, cero JS al cliente async function ProductoPage({ params }) { const producto = await db.productos.findById(params.id); return ( <div> <h1>{producto.nombre}</h1> <BotonesInteractivos id={producto.id} /> </div> ); } // Client Component "use client"; import { useState } from "react"; function BotonesInteractivos({ id }) { const [agregado, setAgregado] = useState(false); return ( <button onClick={() => setAgregado(true)}> {agregado ? "Agregado ✓" : "Agregar al carrito"} </button> ); }Server Components no pueden usar hooks ni event handlers. La arquitectura recomendada: todo Server Component por defecto, convertí a Client solo lo que necesita interactividad.
36. ¿Cómo implementás optimistic updates?
jsximport { useMutation, useQueryClient } from "@tanstack/react-query"; const { mutate } = useMutation({ mutationFn: (id) => fetch(`/api/tareas/${id}/completar`, { method: "POST" }), onMutate: async (id) => { await queryClient.cancelQueries({ queryKey: ["tareas"] }); const prev = queryClient.getQueryData(["tareas"]); // Actualizamos ANTES de que responda el server queryClient.setQueryData(["tareas"], old => old.map(t => t.id === id ? { ...t, completada: true } : t) ); return { prev }; }, onError: (err, id, context) => { // Si falló, revertimos queryClient.setQueryData(["tareas"], context.prev); }, });37. Pregunta de diseño: 500 checkboxes — ¿cómo manejás el estado?
jsx// MAL: array de booleans — cada check puede re-renderizar toda la lista const [checks, setChecks] = useState(Array(500).fill(false)); // BIEN: Set con los ids seleccionados const [seleccionados, setSeleccionados] = useState(new Set()); function toggleItem(id) { setSeleccionados(prev => { const next = new Set(prev); next.has(id) ? next.delete(id) : next.add(id); return next; }); } // Cada item solo lee su propio estado — React.memo evita cascadas const Item = React.memo(function Item({ id, nombre, seleccionado, onToggle }) { return ( <label> <input type="checkbox" checked={seleccionado} onChange={() => onToggle(id)} /> {nombre} </label> ); });38. ¿Cómo implementás un debounce en React?
jsxfunction useDebounce(valor, delay) { const [debouncedValue, setDebouncedValue] = useState(valor); useEffect(() => { const timer = setTimeout(() => setDebouncedValue(valor), delay); return () => clearTimeout(timer); // limpia el timer anterior }, [valor, delay]); return debouncedValue; } function BusquedaInteligente() { const [query, setQuery] = useState(""); const debouncedQuery = useDebounce(query, 300); useEffect(() => { if (debouncedQuery) buscar(debouncedQuery); }, [debouncedQuery]); return <input value={query} onChange={e => setQuery(e.target.value)} />; }39. ¿Cómo implementás dark mode sin flash (FOUC)?
jsx// Script inline ANTES de que React hidrate — evita el flash <script dangerouslySetInnerHTML={{ __html: ` const stored = localStorage.getItem("tema"); const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches; if (stored === "dark" || (!stored && prefersDark)) { document.documentElement.classList.add("dark"); } ` }} />El script corre sincrónicamente antes del primer paint, por eso no hay flash. Si esperás a React, el usuario ve el modo claro por un instante.
40. ¿Qué es hydration y por qué puede causar bugs?
jsx// BUG: window no existe en el servidor → mismatch → error de hidratación function Componente() { return <p>Ancho: {window.innerWidth}px</p>; } // FIX: valor del cliente después de la hidratación function Componente() { const [ancho, setAncho] = useState(null); useEffect(() => { setAncho(window.innerWidth); // solo corre en el cliente }, []); return <p>Ancho: {ancho ?? "---"}px</p>; }Hydration es cuando React "toma control" del HTML generado en el servidor. Si el HTML del server y el cliente no coinciden, React genera una advertencia y puede tener comportamiento impredecible.
41. ¿Cuándo usás React Query vs SWR vs fetch directo?
| Necesidad | Solución |
|---|---|
| Fetching simple en 1-2 componentes |
useEffect+fetch|| Cache, refetch, optimistic updates | React Query (TanStack Query) |
| Apps Next.js con foco en simplicidad | SWR |
| Server Components (Next.js 13+) |
fetchdirecto en el servidor |React Query gana en proyectos grandes: cache automático, dedupe de requests, background refetch, invalidación manual.
42. ¿Qué diferencia hay entre Next.js pages router y app router?
jsx// Pages Router (legado pero estable) export async function getServerSideProps({ params }) { const producto = await getProducto(params.id); return { props: { producto } }; } export default function ProductoPage({ producto }) { ... } // App Router (moderno — React Server Components) async function ProductoPage({ params }) { const producto = await getProducto(params.id); // async directo return <div>{producto.nombre}</div>; }App Router habilita Server Components, streaming, layouts anidados y mejor code splitting. Pages Router es más simple y tiene mejor soporte de librerías legacy.
43. ¿Cómo estructurás un proyecto React grande?
src/ app/ # rutas (Next.js app router) components/ ui/ # componentes genéricos (Button, Input, Modal) features/ # componentes de negocio (ProductCard, UserProfile) hooks/ # custom hooks reutilizables lib/ # utils, clientes API, helpers stores/ # estado global (Zustand) types/ # tipos TypeScript compartidosPrincipio clave: colocá lo que cambia junto. Si
UserProfiletiene su propio hook y sus tests, ponelos en la misma carpeta. Evitácomponents/como papelera global.44. ¿Cómo implementás un tooltip con posicionamiento dinámico?
jsximport { useState, useRef, useLayoutEffect } from "react"; function Tooltip({ children, texto }) { const [pos, setPos] = useState({ top: 0, left: 0 }); const [visible, setVisible] = useState(false); const triggerRef = useRef(); const tooltipRef = useRef(); useLayoutEffect(() => { if (!visible) return; const trigger = triggerRef.current.getBoundingClientRect(); const tooltip = tooltipRef.current.getBoundingClientRect(); setPos({ top: trigger.top - tooltip.height - 8, left: trigger.left + trigger.width / 2 - tooltip.width / 2, }); }, [visible]); return ( <> <span ref={triggerRef} onMouseEnter={() => setVisible(true)} onMouseLeave={() => setVisible(false)} > {children} </span> {visible && ( <div ref={tooltipRef} style={{ position: "fixed", top: pos.top, left: pos.left }} > {texto} </div> )} </> ); }useLayoutEffectacá es correcto porque necesitás medir el DOM antes de que el browser pinte, para evitar el flash de tooltip mal posicionado.45. ¿Cómo compartís tipos entre componente padre e hijo en TypeScript?
tsx// types.ts — fuente de verdad export type Usuario = { id: number; nombre: string; email: string; rol: "admin" | "user"; }; // UsuarioCard.tsx type Props = { usuario: Usuario; onEdit: (id: number) => void; compact?: boolean; }; const UsuarioCard: React.FC<Props> = ({ usuario, onEdit, compact = false }) => { return ( <div className={compact ? "p-2" : "p-4"}> <p>{usuario.nombre}</p> <button onClick={() => onEdit(usuario.id)}>Editar</button> </div> ); };46. ¿Qué es `useInsertionEffect` y para quién es?
jsximport { useInsertionEffect } from "react"; // Solo para autores de librerías CSS-in-JS (styled-components, Emotion) // Corre ANTES de useLayoutEffect y useEffect // Sirve para insertar estilos al DOM antes de que el browser pinte useInsertionEffect(() => { const style = document.createElement("style"); style.textContent = `.btn { color: red }`; document.head.appendChild(style); return () => document.head.removeChild(style); }, []);No lo usés en código de aplicación normal. Es la escape hatch para autores de librerías de estilos en tiempo de ejecución.
47. ¿Cómo manejás el patrón Provider con TypeScript estricto?
tsximport { createContext, useContext, useState, ReactNode } from "react"; type AuthContextType = { user: Usuario | null; login: (email: string, password: string) => Promise<void>; logout: () => void; }; // undefined como valor inicial — fuerza a verificar que el hook se use dentro del Provider const AuthContext = createContext<AuthContextType | undefined>(undefined); export function AuthProvider({ children }: { children: ReactNode }) { const [user, setUser] = useState<Usuario | null>(null); async function login(email: string, password: string) { const u = await authApi.login(email, password); setUser(u); } return ( <AuthContext.Provider value={{ user, login, logout: () => setUser(null) }}> {children} </AuthContext.Provider> ); } // Hook con guardrail — lanza si se usa fuera del Provider export function useAuth(): AuthContextType { const ctx = useContext(AuthContext); if (!ctx) throw new Error("useAuth debe usarse dentro de AuthProvider"); return ctx; }48. ¿Cómo evitás re-renders en una lista con contexto?
jsx// MAL: el contexto tiene todo el estado → todos los consumidores re-renderizan const AppContext = createContext(); // value={{ user, carrito, tema, notificaciones }} — cualquier cambio = re-render global // BIEN: contextos separados por frecuencia de cambio const UserContext = createContext(); // cambia poco (login/logout) const CarritoContext = createContext(); // cambia frecuente const TemaContext = createContext(); // cambia raramente // O usá Zustand con selectores granulares const useUser = () => useStore(state => state.user); // solo re-renderiza si user cambia const useCarrito = () => useStore(state => state.carrito); // independiente49. ¿Cómo implementás un portal para modales?
jsximport { createPortal } from "react-dom"; import { useEffect, useRef } from "react"; function Modal({ isOpen, onClose, children }) { const elRef = useRef(document.createElement("div")); useEffect(() => { const el = elRef.current; document.body.appendChild(el); return () => document.body.removeChild(el); }, []); useEffect(() => { if (isOpen) { document.body.style.overflow = "hidden"; // bloquea scroll } return () => { document.body.style.overflow = ""; }; }, [isOpen]); if (!isOpen) return null; return createPortal( <div className="modal-overlay" onClick={onClose}> <div className="modal-content" onClick={e => e.stopPropagation()}> {children} </div> </div>, elRef.current ); }El portal renderiza el modal fuera del árbol del componente padre — en
document.body— lo que evita problemas deoverflow: hiddenoz-indexdel padre.50. ¿Qué es `useImperativeHandle` y cuándo lo usás?
jsximport { forwardRef, useImperativeHandle, useRef } from "react"; const VideoPlayer = forwardRef(function VideoPlayer(props, ref) { const videoRef = useRef(); // Exponés solo la API que querés — no todo el elemento DOM useImperativeHandle(ref, () => ({ play: () => videoRef.current.play(), pause: () => videoRef.current.pause(), seek: (time) => { videoRef.current.currentTime = time; }, })); return <video ref={videoRef} src={props.src} />; }); // Uso — el padre solo puede play/pause/seek, no accede al DOM directamente function Pantalla() { const playerRef = useRef(); return ( <> <VideoPlayer ref={playerRef} src="/video.mp4" /> <button onClick={() => playerRef.current.play()}>Reproducir</button> </> ); }Cómo practicar estas preguntas antes de tu entrevista
Leer el código no es lo mismo que escribirlo. Las entrevistas técnicas de React te piden:
- 1Explicar en voz alta mientras escribís — practicá decir "lo que estoy haciendo es..." mientras codificás
- 2Identificar el problema antes de escribir — tomá 30 segundos para pensar, el entrevistador aprecia el proceso
- 3Conocer los trade-offs — no existe "la respuesta correcta", existe "depende de" seguido de razonamiento real
- 4Preguntar antes de asumir — "¿qué tamaño tiene la lista?" "¿qué navegadores soportamos?" demuestra madurez técnica
Los temas que más caen por nivel:
- Junior: JSX, props vs state, useState, useEffect básico, keys en listas
- Semi-senior: useCallback/useMemo, custom hooks, Context, testing con RTL
- Senior: performance, arquitectura, Server Components, optimistic updates, concurrencia
Practicá escribir código sin autocompletado. Muchas entrevistas usan editores sin IntelliSense — y eso es exactamente donde falla el que solo copió sin entender.
FAQ
¿Cuáles son las preguntas más comunes en entrevistas de React para junior?+
Las más frecuentes: qué es JSX, diferencia entre props y estado, cómo funciona useState, para qué sirve la prop key en listas, y el ciclo de vida con useEffect. Si las respondés con código real y explicás el porqué, ya superás al 70% de los candidatos junior.
¿Qué diferencia hay entre useCallback y useMemo?+
useMemo memoriza el resultado de una función costosa. useCallback memoriza la referencia de la función en sí. Ambos solo tienen sentido combinados con React.memo para evitar re-renders de componentes hijos — sin eso, el overhead supera el beneficio.
¿Qué es un stale closure en React y cómo lo evitás?+
Un stale closure ocurre cuando un efecto o callback captura un valor viejo de una variable de estado porque ese valor no estaba en el dependency array. Se evita usando la forma funcional del setter — setCount(prev => prev + 1) — o agregando la variable a las dependencias del efecto.
¿Qué son los React Server Components?+
Componentes que se ejecutan en el servidor, generan HTML, y envían cero JavaScript al cliente. No pueden usar useState, useEffect ni event handlers. En Next.js App Router son el default. Se combinan con Client Components — marcados con use client — para las partes interactivas.
¿Cuándo usás useReducer en lugar de useState?+
useReducer es preferible cuando tenés múltiples valores de estado relacionados que cambian juntos, cuando el próximo estado depende del anterior de forma compleja, o cuando querés extraer la lógica de estado a una función pura que sea fácil de testear por separado.
¿Cómo se evita el re-render innecesario de componentes hijos en React?+
Con React.memo para el componente hijo, useCallback para estabilizar las funciones que se pasan como props, y useMemo para estabilizar objetos o arrays. Sin embargo, primero medí con React DevTools Profiler — el 80% de las veces el problema no está donde creés.
¿Qué es hydration en React y por qué puede causar bugs?+
Hydration es cuando React toma control del HTML generado en el servidor y lo hace interactivo en el cliente. Si el HTML del servidor y el cliente no coinciden — por ejemplo, porque usaste window o Date.now() directamente — React genera una advertencia y puede tener comportamiento impredecible.
¿Para qué sirve forwardRef en React?+
forwardRef permite que un componente funcional exponga una ref al componente padre. Es necesario cuando construís una librería de componentes UI y los consumidores necesitan acceso al elemento DOM interno — por ejemplo, para hacer focus() o medir dimensiones.
Related articles
Cómo usar el método STAR en entrevistas (con ejemplos reales)
Aprende a responder preguntas difíciles usando el método STAR en entrevistas. Consejos y ejemplos concretos para roles remotos tech de LATAM.
Cómo conseguir trabajo remoto en dólares desde LATAM: guía real
Descubre consejos concretos para conseguir trabajo remoto en dólares desde LATAM: estrategias de búsqueda, preparación y entrevista para roles tecnológicos.
Las mejores preguntas para hacerle al entrevistador al final
Descubre las mejores preguntas para hacerle al entrevistador al final, útiles para entrevistas tech remotas, diferenciándote y logrando roles en dólares.
Cómo preparar entrevistas de desarrollo sin experiencia previa
Consejos prácticos para enfrentar entrevistas de tu primer trabajo como desarrollador, incluso sin experiencia. Técnicas para destacar y convencer en cada etapa.
Prepare for your real interview
Paste your job link: we research who's interviewing you and rehearse you live.
Start free →Have an interview coming up? Install the live copilot →