Inicio / Artículos

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.

Editor: REACTAPP.TOP26 min de lectura6055 palabras

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.
  • React Native: un ref proporciona un control imperativo sobre un ScrollView.
  • 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?
  • ¿Este efecto realmente está sincronizándose con algo externo, y son correctas sus dependencias?
  • ¿Podría este callback estar leyendo un cierre obsoleto?
  • ¿Esta referencia contiene datos imperativos o el estado de la interfaz que debería estar en useState?
  • ¿Esta memorización está resolviendo un problema real?
  • ¿Este Hook personalizado oculta una complejidad útil o simplemente añade indirección?
  • ¿Debería este estado ser local o global?
  • ¿Dónde está el verdadero cuello de botella en el rendimiento?
  • 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.
  • Gancho: los cinco más comunes como useContext y useReducer, los especializados (useLayoutEffect, useId, useImperativeHandle, useSyncExternalStore, useTransition, useDeferredValue) y cómo diseñar ganchos personalizados.
  • Efectos: dependencias precisas, valores obsoletos en las funciones de callback, limpieza y cancelación, manejo de suscripciones, comportamiento del Modo Estricto y cómo reconocer cuándo no se necesita ningún efecto.
  • Rendimiento: cómo 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.
  • React Native: el ciclo de vida de la aplicación a través de AppState, manejo del teclado, módulos nativos y APIs del dispositivo, listeners registrados en el lado nativo, ajuste de listas, la separación entre tareas en JavaScript y en código nativo, y cómo renderiza la plataforma.
  • Arquitectura: estado local versus estado compartido, datos del cliente versus datos del servidor, dónde se colocan los límites de los hooks personalizados, qué es responsable cada componente, quién posee cada valor, y diseño de APIs para componentes reutilizables.
  • 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