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 en el estado
- El próximo estado depende del anterior de forma compleja
- Querés que la lógica de estado sea testeable de forma independiente
16. ¿Cómo funciona Context y cuáles son sus limitaciones?
import { createContext, useContext, useState } from "react";
// 1. Crear el contexto
const TemaContext = createContext("claro");
// 2. Proveer el valor
function App() {
const [tema, setTema] = useState("claro");
return (
<TemaContext.Provider value={tema}>
<Layout />
</TemaContext.Provider>
);
}
// 3. Consumir desde cualquier nivel
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 (ej: posición del mouse), usá Zustand o separá en múltiples contextos.
17. ¿Qué es un custom hook? Escribí uno real.
import { useState, useEffect } from "react";
// Hook reutilizable para fetch con loading/error
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; }; // cleanup
}, [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 en React moderno con Suspense?
jsximport { Suspense } from "react"; // Con React Query (la forma recomendada en 2026) 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>; } // El padre maneja el loading state 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// Diagnóstico: React DevTools Profiler — tab "Profiler" → Record // 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} />; } // 3. Separá el estado para que los cambios afecten solo lo necesario // Estado global que cambia mucho → Zustand o atom granular (Jotai)22. ¿Qué es el Error Boundary y cómo se implementa?
jsximport { Component } from "react"; // Error Boundaries solo se pueden escribir como clase (todavía en 2026) class ErrorBoundary extends Component { state = { hasError: false, error: null }; static getDerivedStateFromError(error) { return { hasError: true, error }; } componentDidCatch(error, info) { // Loggear a Sentry, Datadog, etc. 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, o SSR — solo en el renderizado.
23. ¿Cómo implementás code splitting y lazy loading?
jsximport { lazy, Suspense } from "react"; // El componente se carga solo cuando se necesita (lazy) 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: false para libs que necesitan el browser).24. ¿Qué es el patrón Compound Component?
jsx// Compound Components: subcomponentes que comparten estado implícito import { 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 — la API es declarativa y composable <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 }); } function validate() { if (!state.email.includes("@")) { dispatch({ type: "SET_ERROR", field: "email", msg: "Email inválido" }); return false; } return true; } return ( <form onSubmit={e => { e.preventDefault(); if (validate()) 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"; // Los componentes funcionales no exponen refs por defecto // forwardRef permite "pasar" una ref hacia adentro 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 múltiples estados asíncronos sin race conditions?
jsximport { useEffect, useState, useRef } from "react"; function 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 => { // Solo actualizamos si este resultado es del query más reciente 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?
jsx// Counter.test.jsx import { 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 }); const count = screen.getByText(/Clicks: 0/i); fireEvent.click(button); expect(screen.getByText(/Clicks: 1/i)).toBeInTheDocument(); }); test("decrementa con el botón -1", () => { render(<Counter initialValue={5} />); fireEvent.click(screen.getByRole("button", { name: /-1/i })); expect(screen.getByText(/Clicks: 4/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. ¿Cómo mockéas un hook en tests?
jsx// useFetch.test.jsx import { render, screen } from "@testing-library/react"; import * as hooks from "../hooks/useFetch"; import Usuarios from "./Usuarios"; test("muestra usuarios cuando carga", () => { // Mock del hook jest.spyOn(hooks, "useFetch").mockReturnValue({ data: [{ id: 1, nombre: "Lucía" }], loading: false, error: null, }); render(<Usuarios />); expect(screen.getByText("Lucía")).toBeInTheDocument(); }); test("muestra spinner mientras carga", () => { jest.spyOn(hooks, "useFetch").mockReturnValue({ data: null, loading: true, error: null, }); render(<Usuarios />); expect(screen.getByText(/Cargando/i)).toBeInTheDocument(); });30. ¿Qué es el patrón Render Props y cuándo lo reemplazaron los hooks?
jsx// Render Props (pre-hooks) — pasás una función como prop class MouseTracker extends Component { state = { x: 0, y: 0 }; handleMove = e => this.setState({ x: e.clientX, y: e.clientY }); render() { return ( <div onMouseMove={this.handleMove}> {this.props.render(this.state)} {/* llamás la función */} </div> ); } } // Uso <MouseTracker render={({ x, y }) => <p>Mouse en {x}, {y}</p>} /> // Equivalente moderno con hook function useMouse() { const [pos, setPos] = useState({ x: 0, y: 0 }); useEffect(() => { const move = e => setPos({ x: e.clientX, y: e.clientY }); window.addEventListener("mousemove", move); return () => window.removeEventListener("mousemove", move); }, []); return pos; }Los hooks reemplazaron Render Props y HOCs para compartir lógica stateful. Render Props todavía aparece en algunas APIs de librerías (Formik, React Router).
NIVEL SENIOR — Arquitectura y performance avanzada
31. ¿Qué es React.memo y cuándo NO lo usás?
jsx// React.memo hace shallow comparison de props const Boton = React.memo(function Boton({ onClick, label }) { console.log("Boton renderizó"); return <button onClick={onClick}>{label}</button>; }); // PROBLEMA: onClick es una función nueva en cada render de Padre function Padre() { const [count, setCount] = useState(0); // Sin useCallback: React.memo no ayuda porque onClick cambia siempre const handleClick = useCallback(() => console.log("click"), []); return ( <div> <button onClick={() => setCount(c => c + 1)}>{count}</button> <Boton onClick={handleClick} label="Acción" /> </div> ); }No uses React.memo si:
- El componente es simple (el overhead de comparación supera el render)
- Las props cambian en casi todos los renders de todas formas
- El componente no tiene renders costosos
32. ¿Qué es la concurrencia en React y qué es `startTransition`?
jsximport { useState, startTransition, 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/pausar este render // si llega una actualización más urgente setResultados(buscarEn(data, e.target.value)); }); } return ( <div> <input value={input} onChange={handleChange} /> {isPending ? <p>Actualizando...</p> : <Lista items={resultados} />} </div> ); }Concurrencia significa que React puede trabajar en múltiples tareas de UI al mismo tiempo, priorizando las urgentes (input del usuario) sobre las pesadas (renderizar 1000 items).
33. ¿Cómo manejás el estado global sin Redux?
jsx// Zustand — la opción más popular en 2026 para proyectos medianos import { create } from "zustand"; const useStore = create(set => ({ usuario: null, setUsuario: usuario => set({ usuario }), carrito: [], agregarAlCarrito: item => set(state => ({ carrito: [...state.carrito, item] })), })); // Uso — lee solo lo que necesita, re-render granular function Header() { const usuario = useStore(state => state.usuario); return <p>{usuario?.nombre}</p>; } function Carrito() { const { carrito, agregarAlCarrito } = useStore(); return <p>{carrito.length} items</p>; }34. ¿Qué es el patrón Container/Presentational?
jsx// Container: maneja datos y lógica function UsuariosContainer() { const { data: usuarios, loading } = useFetch("/api/users"); if (loading) return <Spinner />; return <UsuariosList usuarios={usuarios} />; } // Presentational: solo renderiza, sin lógica de datos function UsuariosList({ usuarios }) { return ( <ul> {usuarios.map(u => ( <li key={u.id}>{u.nombre} — {u.email}</li> ))} </ul> ); }Este patrón sigue siendo válido aunque sea anterior a los hooks. Separa claramente la lógica de la presentación y hace los componentes presentacionales trivialmente testeables.
35. ¿Cómo implementás infinite scroll con Intersection Observer?
jsximport { useEffect, useRef, useState } from "react"; function 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> ); }36. ¿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} // altura de cada fila en px width="100%" itemData={items} > {Row} </FixedSizeList> ); }Virtualización = solo renderizás los items visibles en pantalla + un pequeño buffer. Para 10k items es la diferencia entre 16ms y 2000ms de render.
37. ¿Qué son los Server Components de React y cómo difieren de los Client Components?
jsx// Server Component (Next.js app router — default) // Se ejecuta en el servidor, cero JavaScript al cliente async function ProductoPage({ params }) { // fetch directo — sin useEffect, sin estado de loading const producto = await db.productos.findById(params.id); return ( <div> <h1>{producto.nombre}</h1> <BotonesInteractivos id={producto.id} /> {/* Client Component */} </div> ); } // Client Component — tiene estado e interactividad "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, event handlers ni APIs del browser. Client Components son los de siempre. La arquitectura recomendada: empezá todo como Server Component, convertí a Client solo lo que necesita interactividad.
38. ¿Cómo implementás optimistic updates?
jsximport { useMutation, useQueryClient } from "@tanstack/react-query"; function ListaTareas() { const queryClient = useQueryClient(); const { mutate } = useMutation({ mutationFn: (id) => fetch(`/api/tareas/${id}/completar`, { method: "POST" }), onMutate: async (id) => { await queryClient.cancelQueries({ queryKey: ["tareas"] }); // Guardamos el estado anterior (para rollback) const prev = queryClient.getQueryData(["tareas"]); // Actualizamos optimistamente 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); }, }); return <button onClick={() => mutate(tareaId)}>Completar</button>; }39. ¿Cómo medís y mejorás el First Contentful Paint en una app React?
Métricas clave (Lighthouse / Web Vitals):
- LCP (Largest Contentful Paint): < 2.5s
- FID / INP (Interaction): < 200ms
- CLS (Layout Shift): < 0.1
Técnicas en React:
jsx// 1. Lazy loading de rutas (ya visto) const Reportes = lazy(() => import("./Reportes")); // 2. Preload de recursos críticos <link rel="preload" href="/fonts/inter.woff2" as="font" crossOrigin="" /> // 3. Imágenes con dimensiones explícitas para evitar CLS <img src="hero.jpg" width={1280} height={720} alt="..." /> // 4. Skeleton screens en lugar de spinners function PostSkeleton() { return ( <div className="animate-pulse"> <div className="h-4 bg-gray-200 rounded w-3/4 mb-2" /> <div className="h-4 bg-gray-200 rounded w-1/2" /> </div> ); }40. ¿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 compartidosPrincipios: colocá lo que cambia junto. Si
UserProfiletiene su propio hook y sus tests, ponelos en la misma carpeta. Evitácomponents/como papelera global — separalo por feature o dominio.PREGUNTAS DE DEBUGGING Y CASOS REALES
41. ¿Qué bug tiene este código?
jsxfunction Lista({ items }) { const [seleccionado, setSeleccionado] = useState(items[0]); return ( <ul> {items.map(item => ( <li key={item.id} onClick={() => setSeleccionado(item)} style={{ fontWeight: seleccionado === item ? "bold" : "normal" }} > {item.nombre} </li> ))} </ul> ); }Bug: si
itemscambia (el padre pasa una nueva lista),seleccionadosigue siendo el objeto del array anterior. La comparación por referenciaseleccionado === itemsiempre esfalse.Fix:
jsxconst [seleccionadoId, setSeleccionadoId] = useState(items[0]?.id); // compará por id, no por referencia style={{ fontWeight: seleccionadoId === item.id ? "bold" : "normal" }}42. ¿Por qué este useEffect corre en loop infinito?
jsxfunction Componente() { const [datos, setDatos] = useState([]); useEffect(() => { fetch("/api/datos") .then(r => r.json()) .then(d => setDatos(d)); // actualiza estado }, [datos]); // <- BUG: datos está en las deps return <div>{datos.length}</div>; }Diagnóstico:
datoscambia → efecto corre →setDatoscambiadatos→ efecto corre → loop.Fix: si solo necesitás fetchear al montar, usá
[]. Si necesitás re-fetchear cuando algo cambia, usá la dependencia que dispara el fetch (un ID, un filtro), no el resultado.43. ¿Qué problema tiene este componente con async/await?
jsx// BUG: useEffect callback no puede ser async directamente useEffect(async () => { const data = await fetchData(); setData(data); // la función de limpieza (return) 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.44. ¿Cómo manejás eventos en listas sin crear una función por item?
jsx// MAL: nueva función por cada item en cada render {items.map(item => ( <button key={item.id} onClick={() => handleClick(item.id)}> {item.nombre} </button> ))} // MEJOR para listas muy grandes: event delegation function Lista({ items }) { function handleClick(e) { const id = e.target.dataset.id; if (id) procesarItem(id); } return ( <ul onClick={handleClick}> {items.map(item => ( <li key={item.id} data-id={item.id}>{item.nombre}</li> ))} </ul> ); }En la práctica, las funciones inline son aceptables para listas pequeñas. El event delegation importa cuando tenés miles de items.
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> ); };PREGUNTAS SOBRE REACT Y EL ECOSISTEMA
46. ¿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.
47. ¿Qué diferencia hay entre Next.js pages router y app router?
jsx// Pages Router (legado pero estable) // pages/productos/[id].tsx 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) // app/productos/[id]/page.tsx 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.
48. ¿Cómo manejás la internacionalización (i18n) en React?
jsx// next-intl (popular en 2026 con App Router) import { useTranslations } from "next-intl"; function Header() { const t = useTranslations("Header"); return <h1>{t("titulo")}</h1>; } // messages/es.json // { "Header": { "titulo": "Bienvenido" } } // messages/en.json // { "Header": { "titulo": "Welcome" } }En proyectos LATAM, considerá: voseo vs tuteo, fechas en formato DD/MM/YYYY, y monedas locales (
Intl.NumberFormat).49. ¿Cómo implementás dark mode sin flash (FOUC)?
jsx// _document.tsx (Next.js pages router) // 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"); } ` }} /> // Hook para cambiar el tema function useTema() { const [tema, setTema] = useState(() => typeof window !== "undefined" ? localStorage.getItem("tema") || "system" : "system" ); function toggleTema() { const nuevo = tema === "dark" ? "light" : "dark"; setTema(nuevo); localStorage.setItem("tema", nuevo); document.documentElement.classList.toggle("dark", nuevo === "dark"); } return { tema, toggleTema }; }50. ¿Qué es hydration y por qué puede causar bugs?
jsx// Hydration: React "toma control" del HTML generado en el servidor // Si el HTML del servidor y el cliente no coinciden → error // BUG CLÁSICO: código que solo existe en el browser function Componente() { // window no existe en el servidor → mismatch → error de hidratación return <p>Ancho: {window.innerWidth}px</p>; } // FIX: montar el 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>; }51. ¿Qué diferencia hay entre `useEffect` y `useInsertionEffect`?
jsx// useInsertionEffect — solo para librerías de CSS-in-JS // Corre ANTES de useLayoutEffect y useEffect // Sirve para insertar estilos al DOM antes de que el browser pinte import { useInsertionEffect } from "react"; // No lo usés en código de aplicación normal // Es la escape hatch para autores de styled-components, Emotion, etc. useInsertionEffect(() => { const style = document.createElement("style"); style.textContent = `.btn { color: red }`; document.head.appendChild(style); return () => document.head.removeChild(style); }, []);52. Pregunta de diseño: tenés una lista de 500 checkboxes — ¿cómo manejás el estado?
jsx// MAL: array de booleans — cada check re-renderiza 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 const Item = React.memo(function Item({ id, nombre, seleccionado, onToggle }) { return ( <label> <input type="checkbox" checked={seleccionado} onChange={() => onToggle(id)} /> {nombre} </label> ); });53. ¿Cómo implementás un debounce en React?
jsximport { useState, useEffect, useRef } from "react"; // Custom hook de debounce function 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; } // Uso function BusquedaInteligente() { const [query, setQuery] = useState(""); const debouncedQuery = useDebounce(query, 300); // espera 300ms useEffect(() => { if (debouncedQuery) buscar(debouncedQuery); }, [debouncedQuery]); // solo fetcha cuando el usuario dejó de escribir return <input value={query} onChange={e => setQuery(e.target.value)} />; }54. ¿Cuál es la diferencia entre controlled y uncontrolled components? ¿Cuándo usás `useRef` para forms?
jsx// Controlled: React controla el valor function FormControlado() { const [nombre, setNombre] = useState(""); return <input value={nombre} onChange={e => setNombre(e.target.value)} />; } // Uncontrolled: el DOM controla el valor, React solo lo lee cuando lo necesita function FormNoControlado() { const inputRef = useRef(); function handleSubmit() { console.log(inputRef.current.value); // lee al enviar } return ( <form onSubmit={handleSubmit}> <input ref={inputRef} defaultValue="inicial" /> </form> ); }Uncontrolled es útil cuando integrás con librerías que manejan el DOM directamente, o en formularios muy simples donde no necesitás validación en tiempo real.
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.
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.
Artículos relacionados
Cómo responder 'háblame de vos' en una entrevista técnica
Descubre cómo abordar la pregunta ‘háblame de vos’ en entrevistas técnicas para roles remotos en tecnología, con ejemplos y consejos útiles.
Cómo preparar una entrevista técnica en inglés siendo de LATAM
Aprende a preparar una entrevista técnica en inglés desde LATAM. Consejos reales para superar los desafíos y destacar en roles remotos en dólares.
Cómo prepararte para una entrevista de Desarrollador Frontend
Descubre consejos prácticos para preparar tu entrevista de Desarrollador Frontend. Domina técnicas, retos y preguntas en roles remotos y en inglés.
Cómo conseguir trabajo remoto de React en dólares desde LATAM
Guía práctica para conseguir trabajos remotos de React pagando en dólares desde LATAM: estrategias, CV, portfolio, entrevistas y plataformas clave.
Preparate para tu entrevista real
Pegá el link de tu vacante: investigamos quién te entrevista y te ensayamos en vivo.
Empezar gratis →¿Tenés entrevista próxima? Instalá el copiloto en vivo →