Razonamiento sobre los Hooks de React a través de capturas de renderizado y compromisos.
Un modelo mental de nivel avanzado para React y React Native Hooks: capturas de renderizado, efectos, refs, memorización, Hooks personalizados y cómo explicar los compromisos en las entrevistas.
Fuentes: React & React Native Hooks: The Complete Senior Interview Guide
Las entrevistas avanzadas para React y React Native rara vez se detienen en “¿qué hace este Hook?”. Una pregunta sobre useState se convierte en un tema de procesamiento por lotes y actualizaciones funcionales en cuestión de minutos; una pregunta sobre useEffect da lugar a discusiones sobre bucles infinitos, cierres obsoletos, cancelación y cuándo un efecto en realidad no debería existir; useMemo y useCallback llevan directamente a la pregunta “¿realmente hace que sea más rápido?”. Las definiciones memorizadas no resisten ese tipo de preguntas adicionales. Lo que sí sobrevive es un modelo interconectado, y esta guía lo construye: renderizado, luego estado, cierres, efectos, referencias, memorización, Hooks personalizados, rendimiento y, finalmente, arquitectura, con ejemplos de React Native donde la plataforma cambia el panorama.
El modelo de renderizado del cual depende todo lo demás
Comience con una idea y áncírese a ella: cuando React renderiza un componente, llama nuevamente a la función del componente desde el principio. Utilice un contador lo más pequeño posible.
function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
{count}
</button>
);
}
Es natural pensar que al hacer clic en el botón se accede al componente y se modifica la variable count. Pero eso no es lo que sucede. El setter solicita a React un nuevo renderizado, y la siguiente llamada a Counter recibe el nuevo valor como una constante fresca.
Render 1
count = 0
↓
User clicks
↓
setCount(1)
↓
React schedules another render
↓
Render 2
count = 1
Cada renderizado posee, por lo tanto, una instantánea de sus props y estado. Nada dentro de esa instantánea cambia posteriormente. Esta observación explica los cierres sobre valores antiguos, el estado obsoleto en los temporizadores, por qué existen las dependencias de efecto, cómo se comportan las callbacks y los manejadores de eventos, y por qué la memorización se relaciona con la identidad. Los entrevistadores que van más allá de la superficie de la API suelen verificar si comprendes este modelo.
useState: estado que pertenece al renderizado
useState proporciona a un componente funcional un fragmento de estado que alimenta su salida.
const [count, setCount] = useState(0);
La pareja que se obtiene tiene dos roles distintos: un valor para el renderizado actual y una función que solicita una actualización.
count
↓
current state
setCount
↓
request a state update
Llamar al setter no modifica nada en el lugar; simplemente programa otro renderizado en el cual el componente verá el nuevo valor.
Cómo evolucionan las preguntas
Una progresión típica se ve así:
- Básico: ¿qué problema resuelve
useState? - Intermedio: ¿en qué se diferencia el estado de una variable local común?
- Avanzado: ¿cuándo es la función actualizadora la elección adecuada?
- Más profundo: ¿qué produce realmente
setCount(count + 1), repetido tres veces en un mismo manejador?
“Almacena estado” responde solo a la primera pregunta. Una respuesta más completa abarca todo el modelo:
useState almacena un valor que influye en lo que renderiza el componente. Llamar al setter coloca en cola una nueva renderización con el valor actualizado, por lo que lo mejor es tratar el estado como parte del estado inmediato de esa renderización, y no como una variable que se pueda modificar. Cuando el valor siguiente depende del anterior, la función actualizadora es la opción segura, ya que React agrupa las actualizaciones y el valor capturado podría estar ya desactualizado.
Por el contrario, una variable común se recrea con su valor inicial en cada renderizado y cambiarla nunca le pide a React que se vuelva a renderizar, por lo que no puede controlar la interfaz de usuario.
Por qué son importantes las actualizaciones funcionales
Aquí hay tres incrementos escritos en función del valor capturado.
setCount(count + 1);
setCount(count + 1);
setCount(count + 1);
Todos los tres leen el mismo count del snapshot actual, por lo que después de agruparlos el resultado es un solo incremento, no tres. La forma de actualización correcta soluciona esto:
setCount(prev => prev + 1);
setCount(prev => prev + 1);
setCount(prev => prev + 1);
React pasa a cada actualizador el valor pendiente más reciente, de modo que las tres llamadas se ejecutan en secuencia correctamente. Utilice esta forma siempre que ocurran varias actualizaciones al mismo tiempo o estén en cola, siempre que una función de callback pueda ejecutarse con un snapshot antiguo, y en general cada vez que el nuevo valor se calcule a partir del anterior.
Un carrito de compras en una aplicación React Native es un caso realista. La cantidad se muestra, por lo que debe estar en el estado:
const [quantity, setQuantity] = useState(1);
Los controladores obtienen el valor siguiente a partir del anterior, y la función de decremento lo limita a uno para que el carrito nunca muestre cero o una cantidad negativa.
const increaseQuantity = () => {
setQuantity(prev => prev + 1);
};const decreaseQuantity = () => {
setQuantity(prev => Math.max(1, prev - 1));
};
Nada cambia entre la web y la versión nativa
El Hook es idéntico en ambas plataformas. Un menú desplegable en la web podría registrar si está abierto:
const [isOpen, setIsOpen] = useState(false);
Una pantalla de React Native podría registrar la visibilidad de un modal de la misma manera:
const [isModalVisible, setModalVisible] = useState(false);
El mecanismo de estado se comparte; solo difieren las primitivas de interfaz que controla.
useEffect: sincronización con el mundo exterior
No Hook genera más discusiones durante las entrevistas. La descripción común, “ejecuta código después de renderizar”, es cierta pero omite su propósito real. Una forma mejor de explicarlo es que un efecto mantiene a tu componente sincronizado con un sistema que React no controla.
En la web, ese sistema puede ser eventos DOM, APIs del navegador, WebSockets, suscripciones, temporizadores o solicitudes de red. En React Native suele tratarse del AppState, el módulo Keyboard, módulos nativos, receptores de notificaciones push, estado de red, APIs del dispositivo, WebSockets y temporizadores.
Cómo evolucionan las preguntas
- Básico: ¿para qué sirve
useEffect? - Intermedio: explique cómo funciona el array de dependencias.
- Nivel avanzado: ¿qué hace que un efecto se ejecute indefinidamente?
- Nivel avanzado+: ¿en qué situaciones un efecto es la herramienta inadecuada?
- En producción: ¿cómo se cancela una suscripción a WebSocket?
Compare “useEffect es para llamadas a API” con esto:
useEffect existe principalmente para mantener un componente sincronizado con un sistema externo. El array de dependencias enumera los valores reactivos que el efecto lee, y debe ser preciso; la regla de linting de Hooks detecta omisiones. Cuando el efecto abre una suscripción, inicia un temporizador o adjunta un oyente, devuelve una función de limpieza para que el recurso antiguo sea liberado antes de que el efecto se ejecute nuevamente o cuando el componente se desmonte.
Leer el array de dependencias
Con una dependencia, el efecto se ejecuta después de la primera renderización y nuevamente cada vez que ese valor cambie entre renderizaciones.
useEffect(() => {
fetchUser(userId);
}, [userId]);
Si no hay ningún array, se ejecuta después de cada renderización:
useEffect(() => {
// ...
});
Con un array vacío, se ejecuta después de que el componente se monte:
useEffect(() => {
// ...
}, []);
Tenga cuidado con la frase “se ejecuta una vez”. En el modo estricto de desarrollo, se monta intencionadamente, se limpian los recursos y luego se vuelve a montar para detectar efectos que no se limpian correctamente. La afirmación precisa es que un efecto con array vacío se ejecuta después del montaje, y que el modo estricto de desarrollo puede añadir un ciclo adicional de configuración y limpieza. El código que falla debido a este doble ejecutarse suele tener un error real.
La limpieza evita fugas de recursos
Una suscripción que depende de userId abre una conexión dentro del cuerpo del efecto:
useEffect(() => {
const subscription = subscribeToUpdates(userId);
y devuelve una función que la cierra:
return () => {
subscription.unsubscribe();
};
}, [userId]);
Cuando userId cambia, React ejecuta la limpieza anterior antes de iniciar el nuevo efecto, por lo que nunca hay más de una suscripción activa:
Old effect
↓
cleanup old subscription
↓
new effect
↓
subscribe using new userId
Al desmontar, se ejecuta la última operación de limpieza y no queda nada atrás:
component unmounts
↓
cleanup
↓
subscription removed
El mismo mecanismo se utiliza para cancelar tareas asíncronas. En el caso de una solicitud fetch, puedes abortarla con un AbortController durante la limpieza, o establecer una bandera ignore para que una respuesta tardía proveniente de un userId antiguo no sobrescriba los datos más recientes.
Ejemplo en React Native: reaccionar a AppState
Un efecto más convincente que una solicitud de datos es aquel que vincula al componente con el ciclo de vida de la aplicación nativa. Este listener actualiza los datos cuando la app vuelve al primer plano y se elimina al desmontarse.
useEffect(() => {
const subscription = AppState.addEventListener(
"change",
nextState => {
if (nextState === "active") {
refreshData();
}
}
);
return () => {
subscription.remove();
};
}, []);
Esa es la forma más clara de sincronización con un sistema externo, y demuestra a un entrevistador que comprendes para qué sirven los efectos.
Errore: sincronizar valores derivados a través de un efecto
Supongamos que un formulario almacena el nombre y el apellido:
const [firstName, setFirstName] = useState("");
const [lastName, setLastName] = useState("");
Un patrón anti-padrón frecuente consiste en agregar un tercer elemento de estado y un efecto para mantenerlo actualizado:
const [fullName, setFullName] = useState("");
useEffect(() => {
setFullName(`${firstName} ${lastName}`);
}, [firstName, lastName]);
Esto introduce un estado redundante y una renderización adicional cada vez que cambia un nombre, por lo que en una de las renderizaciones el nombre completo está desactualizado. Dado que el valor está completamente determinado por el estado existente, calcúlelo durante la renderización:
const fullName = `${firstName} ${lastName}`;
El principio que vale la pena mencionar en voz alta: si un valor puede calcularse de forma síncrona a partir de props o estado, derívelo durante la renderización en lugar de almacenarlo y modificarlo mediante un efecto. Este patrón se analiza con más profundidad en dejar de sincronizar el estado con useEffect.
Errore: dependencias de objetos inestables
Aquí se crea un literal de objeto dentro del componente y se enumera como dependencia.
const filters = {
status: "active",
};
useEffect(() => {
fetchUsers(filters);
}, [filters]);
Cada renderización crea un nuevo objeto filters, y las dependencias se comparan por referencia, por lo que el efecto percibe un “cambio” cada vez. Si el efecto actualiza el estado, el ciclo se autoalimenta:
Render
↓
new filters object
↓
dependency changed
↓
effect
↓
setState
↓
render
↓
new filters object
↓
effect again
La solución reflexiva es envolver el objeto en una función de memorización:
useMemo(...)
Antes de hacerlo, pregúntese si realmente es necesario crear el objeto dentro del componente. Una constante puede colocarse fuera del componente; un valor generado a partir de props a menudo puede reemplazarse al depender de sus campos primitivos (por ejemplo status) o al construirlo dentro del efecto. Corregir el flujo de datos es la solución adecuada; añadir memorización para ocultarlo no lo es.
useRef: memoria que no se renderiza
useRef devuelve un contenedor mutable que persiste entre renderizaciones y cuyos cambios nunca provocan una nueva renderización.
const valueRef = useRef(0);
Se leen y escriben sus contenidos a través de la propiedad current:
valueRef.current
Al asignar un nuevo valor, como se muestra a continuación, se actualiza el contenedor pero la pantalla permanece igual hasta que algo más provoca una renderización.
valueRef.current = 10;
Los entrevistadores suelen pasar de “¿en qué se diferencia un ref de estado?”, a “dé un ejemplo concreto”, y finalmente a “¿por qué elegir un ref en lugar de estado aquí?”. Una respuesta sólida:
El estado es los datos que alimentan la renderización, por lo que cambiarlo programa una nueva renderización. Un ref mantiene un valor mutable activo entre renderizaciones sin realizar una nueva renderización cuando cambia
.current. Los refs son adecuados para identificadores imperativos, IDs de temporizadores, valores anteriores y otros datos mutables que la interfaz de usuario no muestra.
En la web, un ref proporciona acceso imperativo a un nodo DOM, por ejemplo para hacer foco en un campo de entrada:
const inputRef = useRef<HTMLInputElement>(null);
inputRef.current?.focus();
En React Native, los refs son igualmente comunes, ya que las interfaces de usuario móviles suelen necesitar llamadas imperativas como desplazar una lista hacia la parte superior:
const scrollRef = useRef<ScrollView>(null);
scrollRef.current?.scrollTo({
y: 0,
animated: true,
});
Elegir entre estado y un ref
La pregunta decisiva es sencilla: ¿cambiar este valor necesita modificar lo que ve el usuario? Si es así, elige
useState
y si no, elige
useRef
La pestaña activa afecta obviamente a la interfaz de usuario, por lo que se trata de estado:
const [selectedTab, setSelectedTab] = useState("home");
Un identificador de temporizador existe únicamente para poder borrarlo posteriormente; nada lo renderiza, por lo que un ref es el lugar adecuado para él:
const timerRef = useRef<number | null>(null);
useMemo: almacenamiento en caché de un valor calculado
useMemo almacena el resultado de un cálculo y solo lo vuelve a calcular cuando cambia una dependencia. Una lista de usuarios filtrada y ordenada es el ejemplo clásico:
const filteredUsers = useMemo(() => {
return users
.filter(user => user.active)
.sort(sortUsers);
}, [users]);
Mientras users mantenga la misma referencia, el filtrado y el ordenamiento se omitirán en las renderizaciones posteriores. Hay que prestar atención a un requisito oculto: sortUsers también debe ser estable, o debería declararse como dependencia, ya que sort modifica el array en su lugar, lo cual solo es seguro aquí porque filter ya generó un nuevo array.
La pregunta que surge casi siempre es “¿deberíamos usarlo en todas partes?”. La respuesta que demuestra criterio es:
No. useMemo no es gratuito: añade seguimiento de dependencias, consumo de memoria y código adicional para su lectura. Sí tiene sentido cuando se repite un cálculo costoso sin motivo, o cuando una referencia estable es importante para algo posterior, y siempre se debe realizar un análisis de rendimiento antes de usarlo.
El uso excesivo típico es memorizar la concatenación de cadenas:
const fullName = useMemo(
() => `${firstName} ${lastName}`,
[firstName, lastName]
);
El trabajo es trivial, por lo que la expresión simple es tanto más rápida de leer como al menos tan rápida de ejecutar:
const fullName = `${firstName} ${lastName}`;
Ahora considere una aplicación de trading en React Native que muestra miles de transacciones, filtradas por símbolo y ventana de tiempo y ordenadas de las más recientes a las antiguas.
const visibleTrades = useMemo(() => {
return trades
.filter(trade => trade.symbol === selectedSymbol)
.filter(trade => trade.timestamp >= startTime)
.sort((a, b) => b.timestamp - a.timestamp);
}, [trades, selectedSymbol, startTime]);
Cuando esa transformación resulta significativamente costosa, la memorización cobra sentido. Incluso entonces, confírmelo con un perfilador en lugar de asumirlo.
useCallback: mantener la identidad de una función
useCallback devuelve el mismo objeto de función entre renders hasta que cambien sus dependencias.
const handlePress = useCallback(() => {
submitOrder();
}, [submitOrder]);
La diferencia con useMemo radica únicamente en lo que se almacena en caché. Uno almacena un valor:
useMemo
→ memoises a value
El otro almacena una referencia a una función:
useCallback
→ memoises a function reference
De hecho, useCallback(fn, deps) se comporta como useMemo(() => fn, deps). “Para mejorar el rendimiento” es una respuesta débil a la pregunta “¿por qué usarlo?”. Una mejor respuesta sería:
useCallback mantiene estable la identidad de una función mientras sus dependencias permanecen iguales. Vale la pena usarlo cuando realmente importe la igualdad de referencias, como en un callback pasado a un componente memoizado o en una función que otro Hook considere como dependencia. No debería añadirse por defecto, ya que la memorización tiene su propio costo y hace que el código sea más difícil de seguir.
Cómo se relaciona con React.memo
React.memo omite la vuelta a renderizar de un componente cuando todas sus props son igualmente simples que las anteriores.
const UserRow = React.memo(function UserRow({
user,
onPress,
}) {
return (
// ...
);
});
Si el padre pasa una función flecha nueva en cada renderizado, siempre habrá una prop que será diferente:
<UserRow
user={user}
onPress={() => openUser(user.id)}
/>
Dado que la referencia cambia, la comparación superficial falla y la fila memorizada se renderiza de todos modos, lo que puede anular silenciosamente los beneficios de React.memo. Un manejador estable se ve así:
const handlePress = useCallback(() => {
openUser(user.id);
}, [user.id]);
y se pasa en lugar de la función inline:
<UserRow
user={user}
onPress={handlePress}
/>
Tenga presente esta advertencia: haga esto cuando tenga evidencias de que el hijo se vuelve a renderizar de forma costosa, no como un acto reflejo.
Cierres y el problema de los valores obsoletos
Los cierres son una característica de JavaScript y no un Hook, pero es difícil razonar sobre los Hooks sin ellos. Un cierre es una función que mantiene el acceso a las variables del ámbito en el que fue creada.
function createLogger(name: string) {
return () => {
console.log(name);
};
}
La función interna sigue teniendo acceso a name mucho tiempo después de que createLogger haya retornado.
Cómo aparece un cierre obsoleto en React
Considere un componente con un contador:
function Counter() {
const [count, setCount] = useState(0);
y dos botones, uno que incrementa y otro que registra la cuenta después de un tiempo de espera de un segundo:
const handleClick = () => {
setTimeout(() => {
console.log(count);
}, 1000);
}; return (
<>
<button onClick={handleClick}>
Log Count
</button> <button onClick={() => setCount(c => c + 1)}>
Increment
</button>
</>
);
}
Supongamos que el manejador de registro se creó durante un renderizado donde
count = 0
Si hace clic en “Registrar cuenta” y luego incrementa varias veces dentro de ese segundo, el tiempo de espera sigue imprimiendo
0
aunque los renderizados posteriores ya hayan avanzado
count = 1
count = 2
count = 3
La función de callback del tiempo de espera hace referencia a count del renderizado que la creó, y esa copia nunca cambia.
Explicándolo en una entrevista
Los componentes funcionales funcionan en capturas de renderizado: cada renderizado tiene sus propios valores y sus propias cierres. Una función de callback creada en un renderizado captura los valores de ese renderizado, por lo que algo asíncrono como un temporizador puede observar el estado anterior. La solución depende de la necesidad: una actualización funcional, corregir las dependencias de efectos o utilizar un ref cuando realmente se necesita el valor mutable más reciente sin provocar un renderizado.
Esa respuesta nombra la causa y ofrece tres soluciones junto con las condiciones para cada una, que es exactamente lo que busca un entrevistador senior.
Ganchos personalizados: empaquetado de comportamientos
Los ganchos personalizados son donde se combinan las ideas anteriores. Un gancho personalizado es una función cuyo nombre comienza con use y que llama a otros ganchos, lo que permite extraer comportamientos con estado y reutilizarlos. Un cargador de perfiles comienza declarando su estado:
function useUserProfile(userId: string) {
const [data, setData] = useState<User | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
Luego ejecuta la solicitud en un efecto basado en userId y devuelve los tres valores:
useEffect(() => {
// fetch user
}, [userId]); return {
data,
loading,
error,
};
}
Una pantalla lo consume sin saber cómo funciona la carga:
function ProfileScreen({ userId }) {
const {
data,
loading,
error,
} = useUserProfile(userId);
Y renderiza lo que necesite a partir del resultado:
// UI
}
Tenga en cuenta que cada componente que llama a un Hook personalizado tiene su propio estado independiente; el Hook comparte la lógica, no los datos.
Lo que realmente está verificando el entrevistador
La pregunta “¿puede escribir un Hook personalizado?” se refiere en realidad a si es capaz de identificar una abstracción de comportamiento reutilizable. Los buenos candidatos se comportan de esta manera:
useAuth()
useUserProfile()
useNetworkStatus()
usePagination()
useDebounce()
useKeyboard()
useAppState()
usePermissions()
useInfiniteScroll()
En React Native, un Hook de estado de red es un ejemplo claro. Se suscribe a los cambios en la conectividad, almacena la bandera en el estado y devuelve la función de cancelación como parte de su limpieza.
function useNetworkStatus() {
const [isConnected, setIsConnected] = useState(true);
useEffect(() => {
const unsubscribe = subscribeToNetworkChanges(
state => {
setIsConnected(state.isConnected);
}
);
return unsubscribe;
}, []);
return {
isConnected,
};
}
Una pantalla de pago luego toma decisiones según el resultado:
function CheckoutScreen() {
const { isConnected } = useNetworkStatus();
if (!isConnected) {
return <OfflineMessage />;
}
return <Checkout />;
}
La pantalla no tiene idea de cómo están conectadas las suscripciones a la red, y esa ignorancia es toda la utilidad de la abstracción.
El resto de las herramientas de Hook
Las entrevistas para puestos senior suelen ir más allá de los cinco Hooks habituales, así que es necesario conocer el conjunto completo.
useContext
Lee un valor proporcionado en niveles superiores de la estructura.
const theme = useContext(ThemeContext);
Es adecuado para problemas compartidos y transversales: temas, autenticación, configuración regional y parámetros de configuración. No es automáticamente una biblioteca de gestión de estado; cada consumidor vuelve a renderizarse cuando cambia el valor proporcionado, lo cual es importante para datos que se actualizan con frecuencia.
useReducer
Cuando varios elementos de estado cambian al mismo tiempo, los setters separados dispersan la lógica de transición:
setLoading(...)
setData(...)
setError(...)
Un reducer la centraliza:
const [state, dispatch] = useReducer(reducer, initialState);
Los componentes describen lo que ha ocurrido y el reducer decide el estado siguiente:
dispatch({
type: "FETCH_SUCCESS",
payload: data,
});
Modelar las transiciones como acciones nombradas hace que los estados complejos sean más fáciles de predecir y probar.
useLayoutEffect
Tiene la misma estructura que useEffect, pero se ejecuta de forma síncrona después de que React actualice el árbol y antes de que el navegador dibuje, lo cual es necesario cuando se debe medir el layout y realizar ajustes antes de que el usuario observe cualquier parpadeo. React Native también presenta casos similares en los que el momento del layout es importante. No se trata de un useEffect más rápido; dado que bloquea el dibujo, su uso excesivo afecta la capacidad de respuesta del sistema.
useId
Genera identificadores únicos y estables, principalmente para relaciones de accesibilidad como vincular una etiqueta a un campo de entrada. Destaca en el entorno web; en React Native, que no cuenta con un DOM del navegador, sus casos de uso son más limitados.
useImperativeHandle
Permite que un componente elija exactamente qué expone una referencia asociada, en lugar de revelar el nodo subyacente. Un componente reutilizable podría exponer la funcionalidad de enfocar:
ref.current?.focus();
o desplazarse al principio:
ref.current?.scrollToTop();
Esa interfaz imperativa controlada es especialmente útil para componentes React Native compartidos.
useSyncExternalStore
Es la forma soportada para leer desde un almacén que se encuentra fuera de React, lo que proporciona instantáneas consistentes incluso con renderizado concurrente. Es avanzado, pero aparece en discusiones sobre arquitectura y es la base sobre la cual se construyen las bibliotecas de estado.
useTransition
Marca algunas actualizaciones como no urgentes. Conceptualmente, el trabajo urgente mantiene la interfaz receptiva:
Urgent update
↓
Keep UI responsive
mientras que el trabajo no urgente puede programarse con una prioridad menor:
Non-urgent update
↓
Can be scheduled with lower priority
Esto es importante en aplicaciones grandes donde un proceso de actualización costoso compite con las acciones de escritura o toque.
useDeferredValue
Pospone la evaluación de un valor para que las interacciones urgentes se realicen rápidamente. Escenario típico:
User typing
↓
Keep input responsive
↓
Defer expensive filtering
La entrada se actualiza de inmediato, mientras que el filtrado intensivo procesa una copia diferida de la consulta.
Las reglas de los Hooks
Existen dos reglas que garantizan el correcto funcionamiento de los Hooks, ya que React identifica cada Hook por el orden en que se llama.
Llamar a los Hooks en el nivel superior
Nunca envuelva un Hook dentro de una condición, bucle o retorno anticipado:
if (isLoggedIn) {
useEffect(() => {});
}
En su lugar, coloque la condición dentro del Hook:
useEffect(() => {
if (!isLoggedIn) {
return;
}
y enumere el valor del cual depende:
// logic
}, [isLoggedIn]);
Llamar a los Hooks solo desde funciones de React
Los Hooks deben utilizarse en componentes funcionales y en Hooks personalizados, no en funciones auxiliares arbitrarias ni métodos de clase.
Hablando de rendimiento
“Esta pantalla es lenta, ¿qué harías?” es una pregunta habitual. Responder “agregar useMemo y useCallback” indica un instinto equivocado. Una respuesta mejor:
El primer paso es averiguar dónde se gasta realmente el tiempo: con qué frecuencia se renderizan los componentes, qué cálculos son costosos, dónde están los límites de los componentes, cómo se renderizan las listas, cómo se propagan las actualizaciones de estado y qué trabajo de plataforma está involucrado. Luego se debe analizar el rendimiento y solucionar el verdadero cuello de botella.
El rendimiento de React Native va más allá del renderizado
El costo de una pantalla móvil se distribuye en varios niveles:
React rendering
↓
JavaScript execution
↓
Native platform work
↓
Layout
↓
Images
↓
Animations
↓
Lists
↓
Network
Así que cuando una pantalla de React Native tiene problemas, la causa podría no ser
useState
o
useEffect
En absoluto. También podría tratarse de un trabajo intensivo con JavaScript, demasiadas actualizaciones de la interfaz, listas extensas, procesamiento de imágenes, carga de animaciones, operaciones nativas, latencia de red o suscripciones a estados que activan demasiados componentes. Saber dónde buscar entre todas esas capas es lo que distingue a los expertos en experiencia móvil.
Una lista extensa de transacciones
Imagínese una aplicación financiera que muestra miles de transacciones. La versión ingenua vuelve a calcular todo el proceso en cada actualización:
const visibleTransactions = transactions
.filter(...)
.sort(...)
.map(...);
Memorizar los filtros y ordenamientos es un paso razonable:
const visibleTransactions = useMemo(() => {
return transactions
.filter(...)
.sort(...);
}, [transactions]);
Rara vez es la respuesta completa. También se necesita virtualización de listas, renderizado eficiente de elementos, claves estables, componentes de fila memorizados, suscripciones a estado limitado, carga sensata de imágenes e interacciones responsivas. En React Native, elegir FlatList o FlashList y configurarlo adecuadamente suele ser mucho más importante que usar un useMemo. La pregunta a hacer es dónde está el cuello de botella, no qué Hook agregar.
Propiedad del estado y arquitectura
Contexto versus biblioteca de almacenes
"¿Cuándo usarías Context en lugar de Redux o Zustand?" no tiene una respuesta basada en marcas específicas.
Depende del estado. El contexto funciona bien para valores bastante estables y transversales como el tema o la configuración. Para estados de aplicaciones complejas que se actualizan con frecuencia y que necesitan selectores, transiciones predecibles o suscripciones detalladas, una biblioteca dedicada al estado suele ser la opción más adecuada.
Los conceptos importantes son la propiedad y la frecuencia de actualización.
No todo pertenece a un almacén global
Un error arquitectónico común es dirigir todos los valores a un único almacén:
Everything
↓
Global store
En su lugar, pregúntese quién es el responsable de cada parte del estado. El estado de los componentes locales abarca detalles de la interfaz de usuario de corta duración:
isModalOpen
inputValue
selectedTab
El estado del servidor es datos obtenidos de una API; a menudo, es mejor gestionarlo con una biblioteca dedicada al estado del servidor que se encargue del caché y de la recarga:
user profile
products
orders
transactions
El estado global del cliente es el pequeño conjunto de valores que realmente comparten muchas pantallas:
theme
authentication state
application preferences
En la web, algunos estados deben estar en la URL para que sobrevivan a las recargas y puedan compartirse como un enlace:
filters
search
pagination
La regla a recordar: el estado debe encontrarse en el ámbito más restringido que tenga sentido.
Aplicándolo en una pantalla de inversiones
Imaginemos una pantalla de inversiones de React Native que necesita datos del mercado, un símbolo seleccionado, datos de gráficos, datos del portafolio, manejo de AppState, desplazamiento y una transformación de gráficos costosa. Una estructura simplificada:
InvestmentScreen
│
├── useMarketData()
│
├── usePortfolio()
│
├── useAppState()
│
├── useRef()
│
├── useMemo()
│
└── useCallback()
│
▼
UI
A continuación se muestra un boceto del componente. Hay dos cosas a tener en cuenta: el fragmento termina con unas líneas de prosa y separadores que no son código válido, por lo que debe leerse como pseudocódigo; además, setSymbol de useState ya es estable, por lo que el envoltorio useCallback aquí es opcional y se podría pasar directamente el setter.
function InvestmentScreen() {
const [symbol, setSymbol] = useState("AAPL");
const scrollRef = useRef<ScrollView>(null);
const { data: marketData } = useMarketData(symbol);
const { portfolio } = usePortfolio();
const chartData = useMemo(() => {
return transformMarketData(marketData);
}, [marketData]);
const handleSymbolChange = useCallback(
(nextSymbol: string) => {
setSymbol(nextSymbol);
},
[]
);
return (
<ScrollView ref={scrollRef}>
<MarketHeader
symbol={symbol}
onSymbolChange={handleSymbolChange}
/>
<PriceChart data={chartData} />
<PortfolioSummary
portfolio={portfolio}
/>
</ScrollView>
);
}
---
Now each concept has a clear responsibility.
---
Cada Hook ahora tiene una única responsabilidad explicable:
useState
→ selected symboluseRef
→ imperative ScrollView accessuseMemo
→ expensive chart transformationuseCallback
→ stable handler where usefulCustom Hooks
→ market and portfolio behaviouruseEffect
→ internal synchronization within those Hooks
Los efectos permanecen dentro de los Hooks personalizados, donde se sincronizan con el feed del mercado y el ciclo de vida de la aplicación, y la propia pantalla parece casi una descripción de sus datos.
Respondiendo como ingeniero senior
Ir un nivel más allá de la pregunta
Un error común es responder solo a lo que se preguntó. Para “¿qué es useMemo?”, decir “memoriza un valor” es correcto pero fácil de olvidar. Una respuesta más completa:
useMemo almacena en caché un valor calculado según sus dependencias. No es algo predeterminado, ya que implica un costo adicional y añade complejidad. Tiene sentido cuando un cálculo costoso se repite innecesariamente o cuando es importante contar con una referencia estable, idealmente después de que el análisis de rendimiento haya identificado el cuello de botella.
En pocas frases ha demostrado conocimiento de las API, conciencia sobre el rendimiento, capacidad para evaluar compromisos y juicio crítico.
Estructura de respuesta en cinco pasos
Un modelo fiable para cualquier pregunta relacionada con los Hooks:
1. What is it?
2. Why does it exist?
3. Real example
4. Trade-off / pitfall
5. React Native example when relevant
Aplicado a useRef:
- Qué: un valor mutable persistente que no provoca una actualización de la interfaz.
- Por qué: para datos que deben mantenerse tras las actualizaciones pero que no forman parte de la UI.
- Ejemplo: un ID de temporizador o un identificador de componente imperativo.
- Compromiso: como su actualización no genera una renderización, no debe contener estado de la UI.
Preguntas complementarias a preparar
Para useState, esté listo para hablar sobre las funciones de actualización y el agrupamiento de varias llamadas, por qué mutar una variable simple no modifica la pantalla, qué provoca un nuevo renderizado, dónde debería almacenarse un estado específico y cómo evitar estados que en realidad son derivados.
Para useEffect, espere preguntas sobre el papel del array de dependencias, los cierres obsoletos, las ejecuciones repetidas, para qué sirve la función de limpieza, cómo cancelar tareas asíncronas en curso, las situaciones en las que un efecto no es la herramienta adecuada y la doble invocación de Modo Estricto durante el desarrollo.
Para useRef, prepárese para explicar por qué escribir en .current no provoca un renderizado, qué situaciones favorecen el uso de un ref en lugar del estado, cómo los refs se vinculan al DOM o a vistas nativas y qué significa una API imperativa.
Con respecto a useMemo, debe tener claro si siempre acelera el rendimiento, en qué se diferencia de useCallback, qué significa la igualdad referencial, cuándo es adecuado eliminarlo y cómo demostrar que ayudó.
En cuanto a useCallback, debe saber por qué la identidad de una función puede ser importante, cómo se combina con React.memo, cuándo no aporta nada, si puede ralentizar el proceso y qué debe incluirse en su lista de dependencias.
Un ejercicio de depuración
A los entrevistadores les gusta entregar componentes con errores. Aquí hay uno con varios problemas.
function UserList({ users }) {
const [search, setSearch] = useState("");
const filteredUsers = useMemo(() => {
return users.filter(user =>
user.name.includes(search)
);
}, [users]);
useEffect(() => {
console.log("Search:", search);
}, []);
return (
<List
data={filteredUsers}
renderItem={({ item }) => (
<UserRow
user={item}
onPress={() => selectUser(item.id)}
/>
)}
/>
);
}
Dependencia faltante en useMemo
El memo se basa únicamente en los usuarios:
}, [users]);
pero el filtro también lee
search
por lo que la lista nunca se actualiza mientras el usuario escribe. Las dependencias deben incluir ambos elementos:
}, [users, search]);
La regla de lint marcaría esto de inmediato.
Un efecto congelado en el primer valor
Este efecto tiene un array vacío:
useEffect(() => {
console.log(search);
}, []);
por lo que solo se ejecuta después del montaje y registra la búsqueda inicial search. Si el objetivo es registrar cada cambio, depende de ello:
useEffect(() => {
console.log(search);
}, [search]);
La función de callback inline, manejada con criterio
El manejador de fila
onPress={() => selectUser(item.id)}
es una función nueva en cada renderizado. Eso por sí solo no es un error. Primero hay que determinar si
UserRow
es costoso y si realmente se está volviendo a renderizar innecesariamente. Solo con esa evidencia se podría recurrir a
useCallback
o reestructurar los componentes, por ejemplo pasando el id y un manejador estable a la fila. Mostrar esa moderación forma parte de la respuesta esperada.
Respuestas buenas, sólidas y de nivel avanzado
La progresión es fácil de recordar. Un buen desarrollador conoce useCallback. Un desarrollador competente sabe cuándo usarlo. Un desarrollador senior sabe cuándo no usarlo y puede explicar las ventajas e inconvenientes. La misma lógica se aplica a cada Hook. El objetivo nunca es
Maximum Hooks
sino más bien
Correct abstraction
+
Correct data flow
+
Correct performance trade-offs
Un mismo conjunto de conceptos para web y nativo
No necesitas conocimientos separados de los Hooks para React y React Native. En la web, los ejemplos suelen involucrar
DOM
browser events
Web APIs
WebSockets
browser storage
mientras que las discusiones sobre móviles involucran
AppState
Keyboard
native modules
device APIs
push notifications
network state
native event subscriptions
Pero la herramienta básica es la misma en ambos casos: React.
useState
useEffect
useRef
useMemo
useCallback
useContext
useReducer
Custom Hooks
closures
dependency arrays
Así que explica primero el concepto de React y luego muestra cómo aplicarlo en la plataforma en cuestión. Esto te mantendrá creíble tanto en roles relacionados con web, móviles como híbridos.
Ficha de referencia rápida
useState
Indica qué elemento desencadena una nueva renderización al actualizarse. Usos típicos:
form values
selected tabs
modal visibility
loading state
local UI state
useEffect
Sincronización con sistemas externos. Usos típicos:
subscriptions
timers
WebSockets
AppState
native events
network operations
Evítarlo para valores que puedan obtenerse durante la renderización.
useRef
Un valor mutable persistente que no se renderiza. Usos típicos:
DOM/native refs
timers
previous values
imperative APIs
mutable non-UI data
useMemo
Un valor calculado memorizado. Úsalo cuando
calculation is expensive
or
referential identity matters
y mides primero.
useCallback
Una referencia a una función memorizada. Vale la pena usarla cuando
function identity matters
React.memo is involved
another Hook depends on function identity
No lo agregues automáticamente.
useContext
Lee datos contextuales compartidos, como por ejemplo:
theme
locale
authentication
configuration
useReducer
Modela transiciones complejas. Útil cuando tienes
many related state variables
complex transitions
state machine-like behaviour
Ganchos personalizados
Comportamiento con estado reutilizable, por ejemplo:
useAuth()
useNetworkStatus()
useAppState()
usePagination()
useKeyboard()
useUserProfile()
Un modelo mental en lugar de un glosario
En lugar de memorizar una definición de una sola palabra para cada Hook,
useState = state
useEffect = side effect
useRef = reference
useMemo = memoisation
useCallback = callback
imagine cómo se relacionan con la renderización: el estado de la interfaz de usuario fluye a través de useState para generar nuevas renderizaciones, los valores derivados provienen de useMemo, los datos mutables que no son de la interfaz de usuario se almacenan en useRef, y useEffect conecta la interfaz de usuario renderizada con APIs, eventos y sistemas nativos, siendo los Hooks personalizados quienes lo integran todo.
React Render
│
┌──────────┼──────────┐
│ │ │
UI State Derived Mutable
│ Values Data
│ │ │
useState useMemo useRef
│
▼
Re-render
│
▼
UI
│
▼
External Systems
│
▼
useEffect
│
▼
APIs / Events / Native
│
▼
Custom Hooks
Debajo de todo esto hay unos pocos principios fundamentales:
Rendering
+
Closures
+
Referential Equality
+
Data Flow
+
Performance
Una vez que estos están bien establecidos, los Hooks individuales se vuelven sencillos de entender.
Preguntas que hacer en cada revisión de código
Los Hooks son herramientas para gestionar diferentes tipos de comportamiento en una aplicación reactiva, y la habilidad importante es saber por qué se utiliza cada uno. Estas preguntas son útiles tanto en revisiones de código como en entrevistas:
- ¿Es necesario que este estado exista, o puede derivarse?
useState?Conclusión
Los Hooks se vuelven más fáciles una vez que dejas de tratarlos como APIs separadas y los ves como etapas de un mismo flujo:
Rendering
↓
State
↓
Closures
↓
Effects
↓
References
↓
Memoisation
↓
Custom Hooks
↓
Performance
↓
Architecture
Teniendo en cuenta ese flujo, pasar de React web a React Native resulta mucho menos intimidante. Cuando alguien pregunte por qué se utilizó useMemo en un lugar específico, evite la definición típica y cuente la historia: el problema que se observó, qué lo causó, por qué encajaba este Hook, qué compromisos se tuvieron en cuenta y cómo se midió el resultado.
Antes de una entrevista, asegúrese de poder hablar con naturalidad sobre:
- Conceptos básicos: las causas de un render, qué es una captura de estado, cómo el estado difiere de una variable común, cierres, igualdad referencial y reconciliación.
useContext y useReducer, los especializados (useLayoutEffect, useId, useImperativeHandle, useSyncExternalStore, useTransition, useDeferredValue) y cómo diseñar ganchos personalizados.React.memo compara los props, el costo de la memorización, análisis del rendimiento de las renderizaciones, listas largas, cálculos intensivos y suscripciones al almacenamiento demasiado amplias.El objetivo no es tener respuestas memorizadas para todo, sino contar con suficiente comprensión como para razonar sobre un problema desconocido frente al entrevistador.
Lecturas relacionadas
- Compromisos de React Native a Escala: Estado, Listas, Dispositivos, Tokens y Mejoras — Cinco problemas de React Native que ponen a prueba el juicio técnico: estado en servidor frente al del cliente, FlatLists con retrasos, dispositivos Android de gama baja, almacenamiento de tokens y actualizaciones incrementales.
- Cómo React-Redux Decide Volver a Renderizar: Selectores, Igualdad y Gancho Tipados — Una guía de estudio sobre los mecanismos internos de React-Redux: Provider, igualdad en useSelector, selectores memorizados, connect(), gancho tipados con withTypes() y casos límite poco comunes.