La lección anterior terminó con un memo sobre TarjetaBicicleta que no ahorra un solo render, porque ListaBicicletas crea funciones flecha nuevas en cada ejecución y la comparación por identidad falla siempre. Ese es el problema que estos dos hooks resuelven, y no es el único: el módulo 7 dejó abierta la misma deuda en tres sitios más —el value de los proveedores de contexto (07-02), las instancias de selectores de Redux (07-05) y las dependencias de los efectos (05-02)—, y todos son la misma cuestión: cómo conseguir que un valor construido dentro de un componente siga siendo el mismo objeto entre renders. A eso se suma un problema distinto que también resuelve useMemo: evitar que un cálculo realmente caro —ordenar y filtrar 2.000 bicicletas— se repita cuando sus entradas no han cambiado. Distinguir esos dos usos es lo más importante de esta lección, porque casi todo el mal uso de la memorización viene de confundirlos. Al final verás además los hooks de concurrencia de React 18/19, useTransition y useDeferredValue, que atacan el mismo síntoma desde un ángulo completamente distinto: en lugar de hacer menos trabajo, reorganizar cuándo se hace.
Contenido
useMemo: firma y qué guarda exactamenteuseCallback: el caso particular deuseMemo- Los dos hooks, comparados
- Uso legítimo 1: evitar un cálculo realmente costoso
- Cómo comprobar de verdad si un cálculo es caro
- Uso legítimo 2: mantener la identidad de un valor
- Cerrando 08-02:
ListaBicicletasdefinitiva - Cerrando 07-02: el
valuede un proveedor de contexto - Dependencias de
useEffecty selectores de Redux - Cómo elegir las dependencias y por qué el linter tiene razón
- Cuando una dependencia cambia demasiado
- Cuándo NO memorizar
- Alternativas mejores que memorizar
- Hooks de concurrencia:
useTransitionyuseDeferredValue - El React Compiler aquí
useMemo: firma y qué guarda exactamente
useMemo: firma y qué guarda exactamenteTres partes:
- La fábrica: una función sin argumentos que devuelve el valor. React la llama; tú no.
- El array de dependencias: los valores de los que depende el resultado.
- El valor devuelto: lo que produjo la fábrica.
Y el comportamiento, con precisión:
- En el primer render, React ejecuta la fábrica y guarda el resultado junto con las dependencias.
- En los siguientes, compara cada dependencia con la guardada usando
Object.is. - Si todas coinciden, devuelve el valor guardado sin ejecutar la fábrica.
- Si alguna difiere, ejecuta la fábrica otra vez, guarda el nuevo resultado y las nuevas dependencias.
Dos advertencias que cambian cómo se escribe el código:
La fábrica se ejecuta durante el render, así que debe ser pura: nada de peticiones, temporizadores, escrituras en el DOM ni setEstado. Para eso están los efectos (05-02).
useMemo es una optimización, no una garantía. La documentación de React lo dice explícitamente: React puede descartar valores memorizados cuando lo necesite (por ejemplo, para liberar memoria de componentes fuera de pantalla). Nunca escribas código cuya corrección dependa de que la fábrica no se vuelva a ejecutar. Si necesitas que algo ocurra una sola vez de verdad, eso es un useRef o un efecto, no un useMemo.
useCallback: el caso particular de useMemo
useCallback: el caso particular de useMemouseCallback guarda la función misma, no el resultado de llamarla. Y esta equivalencia lo explica todo:
// Estas dos líneas hacen exactamente lo mismo
const f = useCallback((id) => reservar(id), [reservar]);
const f = useMemo(() => (id) => reservar(id), [reservar]);useCallback(fn, deps) es useMemo(() => fn, deps). Existe como hook propio solo porque memorizar funciones es tan frecuente que la doble flecha resultaba incómoda y propensa a errores.
El error de principiante que hay que evitar desde el primer día:
// ❌ Llama a la función y memoriza su RESULTADO
const total = useCallback(calcularTotal(horas), [horas]);
// ✅ Memoriza la FUNCIÓN
const calcular = useCallback(() => calcularTotal(horas), [horas]);
// ✅ Memoriza el RESULTADO — que es lo que probablemente querías
const total = useMemo(() => calcularTotal(horas), [horas]);
- Los dos hooks, comparados
useMemo |
useCallback |
|
|---|---|---|
| Qué guarda | El valor devuelto por la fábrica | La función que le pasas |
| Firma | useMemo(() => valor, deps) |
useCallback(fn, deps) |
| Cuándo recalcula | Cuando cambia alguna dependencia | Cuando cambia alguna dependencia |
| Equivalencia | — | useMemo(() => fn, deps) |
| Motivo 1: coste | ✅ Su razón principal | ❌ Crear una función es baratísimo |
| Motivo 2: identidad | ✅ Objetos, arrays, instancias | ✅ Su razón única |
| Uso típico | Filtrar/ordenar listas, value de contexto, objetos de opciones |
Manejadores pasados a componentes memorizados o a dependencias de efectos |
| La fábrica se ejecuta | Durante el render | Nunca la ejecuta React: solo la guarda |
Un matiz que ordena la tabla: useCallback nunca es una optimización de coste. Crear una función en JavaScript cuesta prácticamente nada. useCallback existe solo para preservar la identidad. Si nadie compara esa función —no va a un componente memorizado, no es dependencia de un efecto ni de otro hook—, el useCallback es coste puro.
- Uso legítimo 1: evitar un cálculo realmente costoso
Este es el catálogo de CicloUrbano con las 2.000 bicicletas, filtrando y ordenando en el render:
// src/paginas/PaginaCatalogo.jsx (sin memorizar)
import { useSelector } from 'react-redux';
import { useSearchParams } from 'react-router';
import { useBicicletas, useEstaciones } from '../consultas/consultasBicicletas.js';
function PaginaCatalogo() {
const { data: bicicletas = [] } = useBicicletas();
const { data: estaciones = [] } = useEstaciones();
const termino = useSelector((estado) => estado.catalogo.termino);
const orden = useSelector((estado) => estado.catalogo.orden);
const [parametros] = useSearchParams();
const tipo = parametros.get('tipo') ?? 'todos';
// Este bloque se ejecuta en CADA render, cambie lo que cambie
const visibles = bicicletas
.filter((b) => tipo === 'todos' || b.tipo === tipo)
.filter((b) => b.modelo.toLowerCase().includes(termino.toLowerCase()))
.map((b) => ({
...b,
nombreEstacion: estaciones.find((e) => e.id === b.estacionId)?.nombre ?? '—'
}))
.sort((a, b) =>
orden === 'precio' ? a.precioHora - b.precioHora : a.modelo.localeCompare(b.modelo)
);
return (
<>
<BuscadorBicicletas />
<SelectorTipo />
<ListaBicicletas bicicletas={visibles} />
</>
);
}Dos problemas independientes, y conviene no mezclarlos:
- Coste: con 2.000 bicicletas hay dos filtrados, un
mapque hace unfindsobre las estaciones por cada elemento —eso es O(n × m)— y una ordenación conlocaleCompare, que es de las comparaciones más caras de JavaScript. Entre 10 y 40 ms por render en un equipo de desarrollo, y tres o cuatro veces más en un móvil. - Identidad:
visibleses un array nuevo en cada render, así que cualquiermemosobreListaBicicletasestaría condenado a fallar.
useMemo resuelve los dos a la vez:
const visibles = useMemo(() => {
// 1) Índice de estaciones: convierte el find O(m) en un acceso O(1)
const nombrePorEstacion = new Map(estaciones.map((e) => [e.id, e.nombre]));
// 2) Colador reutilizable: crear un Intl.Collator por comparación es carísimo
const colador = new Intl.Collator('es');
const terminoNormalizado = termino.trim().toLowerCase();
return bicicletas
.filter((b) => tipo === 'todos' || b.tipo === tipo)
.filter((b) => b.modelo.toLowerCase().includes(terminoNormalizado))
.map((b) => ({ ...b, nombreEstacion: nombrePorEstacion.get(b.estacionId) ?? '—' }))
.sort((a, b) =>
orden === 'precio' ? a.precioHora - b.precioHora : colador.compare(a.modelo, b.modelo)
);
}, [bicicletas, estaciones, termino, tipo, orden]);Presta atención a lo que ha pasado aquí, porque es la lección de fondo: antes de memorizar, se ha reducido el trabajo. El Map de estaciones convierte una búsqueda lineal por elemento en un acceso directo, y el Intl.Collator reutilizado evita construir un comparador en cada llamada de sort. Esas dos líneas bajan el coste más que el useMemo, y siguen sirviendo aunque las dependencias cambien en cada render. La memorización llega después, no en lugar de.
Con el useMemo puesto, esta es la tabla de comportamiento:
| Qué cambia | ¿Recalcula? | ¿Correcto? |
|---|---|---|
| El usuario escribe en el buscador | Sí (termino) |
Sí: el resultado depende de ello |
Cambia ?tipo= en la URL |
Sí (tipo) |
Sí |
| Cambia el orden | Sí (orden) |
Sí |
| Query revalida y devuelve los mismos datos | Sí (bicicletas es un array nuevo) |
Inevitable: Query sustituye la referencia |
Se abre el Modal (estado local de la página) |
No | Esa es la ganancia |
| Cambia el tema | No | Ídem |
| Llega un aviso nuevo | No | Ídem |
- Cómo comprobar de verdad si un cálculo es caro
«Caro» no es una opinión. Se mide, y hay dos formas rápidas antes de abrir el Profiler.
Con console.time, envolviendo el cálculo sin memorizar:
const visibles = useMemo(() => {
console.time('filtrar y ordenar catálogo');
const resultado = /* … el cálculo … */;
console.timeEnd('filtrar y ordenar catálogo');
return resultado;
}, [bicicletas, estaciones, termino, tipo, orden]);Cómo interpretar el número que salga por consola:
| Duración medida | Veredicto |
|---|---|
| < 1 ms | No memorices. El useMemo cuesta más que el cálculo |
| 1 – 5 ms | Zona gris: depende de con qué frecuencia ocurra y de si hay más trabajo en el mismo render |
| 5 – 16 ms | Memorizar probablemente compensa: te acercas al presupuesto de un fotograma |
| > 16 ms | Memoriza, y además revisa el algoritmo: probablemente hay un find dentro de un map |
La referencia de los 16 ms viene de que a 60 fotogramas por segundo el navegador dispone de 16,7 ms por fotograma. Todo lo que se pase de ahí produce un salto visible.
Con la ralentización de CPU de las herramientas del navegador. En la pestaña Rendimiento (Performance) hay un selector de CPU throttling: ponlo en 4× o 6×. Es lo más parecido a un móvil de gama media que tienes sin comprar uno. Un cálculo de 4 ms en tu portátil pasa a 16–24 ms ahí, y lo que era irrelevante deja de serlo.
Y el paso que casi nadie da: quita el useMemo y mide otra vez. Si el número no cambia de forma perceptible, quítalo definitivamente. Es el paso «volver a medir» del flujo de trabajo de 08-01, y es lo que distingue optimizar de decorar.
- Uso legítimo 2: mantener la identidad de un valor
El segundo uso no tiene nada que ver con el coste del cálculo. Aquí la fábrica puede ser trivial —() => ({ usuario, esOperario })— y aun así el useMemo es imprescindible, porque lo que importa no es lo que cuesta construir el valor, sino que alguien más abajo lo va a comparar por identidad.
¿Quién compara? Exactamente estos cuatro:
| Quién compara | Qué pasa si la identidad cambia |
|---|---|
memo en un componente hijo |
La comparación falla y el componente se reejecuta siempre (08-02) |
useEffect con esa dependencia |
El efecto se vuelve a ejecutar: petición repetida, temporizador reiniciado, suscripción rehecha |
Un proveedor de contexto (value) |
Todos los consumidores se reejecutan, usen o no la parte que cambió (07-02) |
useSelector de Redux o un selector memorizado |
Repintado en cada acción despachada, o memorización que nunca acierta (07-05) |
Y hay un quinto, silencioso: otro useMemo o useCallback que tenga ese valor entre sus dependencias. Una identidad inestable se propaga en cascada e invalida toda la memorización que hay por debajo. Por eso los problemas de identidad se arreglan desde arriba: estabiliza primero el valor de más arriba y muchos de los de abajo se arreglan solos.
- Cerrando 08-02:
ListaBicicletas definitiva
ListaBicicletas definitivaEste es el código que 08-02 dejó pendiente.
// src/componentes/ListaBicicletas.jsx (versión definitiva)
import { useCallback } from 'react';
import { useNavigate } from 'react-router';
import TarjetaBicicleta from './TarjetaBicicleta.jsx';
import estilos from './ListaBicicletas.module.css';
function ListaBicicletas({ bicicletas }) {
const navegar = useNavigate();
// navegar es estable: React Router garantiza su identidad entre renders.
// Con [navegar] como dependencia, estas funciones se crean UNA vez.
const manejarSeleccion = useCallback(
(id) => navegar(`/bicicletas/${id}`),
[navegar]
);
const manejarReserva = useCallback(
(id) => navegar(`/reservas/nueva?bicicleta=${id}`),
[navegar]
);
return (
<ul className={estilos.rejilla}>
{bicicletas.map((bicicleta) => (
<li key={bicicleta.id}>
<TarjetaBicicleta
bicicleta={bicicleta}
nombreEstacion={bicicleta.nombreEstacion}
alSeleccionar={manejarSeleccion}
alReservar={manejarReserva}
/>
</li>
))}
</ul>
);
}
export default ListaBicicletas;Tres decisiones y sus motivos:
useCallbackcon[navegar]. La funciónnavegarde React Router es estable por diseño, así que ambas funciones se crean una sola vez en toda la vida del componente. AhoraObject.is(propsPrevias.alSeleccionar, propsNuevas.alSeleccionar)datruey elmemode 08-02 por fin funciona.nombreEstacionviene ya calculado desde eluseMemodePaginaCatalogo, en lugar de resolverse aquí con unfindpor cada tarjeta. Menos trabajo y una prop primitiva más.- Los manejadores reciben el
idcomo argumento, no se crea una función por tarjeta. SiTarjetaBicicletanecesitara() => alReservar(bicicleta.id), esa flecha se crearía dentro de la tarjeta, que es donde debe estar: es una función que la tarjeta pasa a un elemento del DOM, y a unonClickde un<button>le da igual la identidad.
Este es el escenario donde memo y useCallback funcionan: juntos, en una lista larga, con una medición previa que lo justifica. Ninguno de los dos sirve de nada por separado.
- Cerrando 07-02: el
value de un proveedor de contexto
value de un proveedor de contextoLa deuda que el módulo 7 dejó explícitamente pendiente. El problema, recordado en una línea: el value de un proveedor es un objeto literal creado en cada render, así que cada render del proveedor reejecuta a todos sus consumidores, aunque el contenido sea idéntico.
// ❌ El problema
export function ProveedorTema({ children }) {
const [tema, setTema] = useAlmacenLocal('tema', 'claro');
// Objeto NUEVO en cada render → todos los consumidores se reejecutan
const valor = { tema, alternarTema: () => setTema((t) => (t === 'claro' ? 'oscuro' : 'claro')) };
return <ContextoTema value={valor}>{children}</ContextoTema>;
}// ✅ La versión definitiva de src/contextos/ContextoTema.jsx
import { createContext, useContext, useMemo, useCallback, useEffect } from 'react';
import { useAlmacenLocal } from '../hooks/useAlmacenLocal.js';
const ContextoTema = createContext(null);
export function ProveedorTema({ children }) {
const [tema, setTema] = useAlmacenLocal('tema', 'claro');
useEffect(() => {
document.documentElement.dataset.tema = tema;
}, [tema]);
// 1) La función, estable: setTema de useState es estable por contrato de React
const alternarTema = useCallback(() => {
setTema((actual) => (actual === 'claro' ? 'oscuro' : 'claro'));
}, [setTema]);
// 2) El objeto, estable mientras no cambie el tema
const valor = useMemo(() => ({ tema, alternarTema }), [tema, alternarTema]);
return <ContextoTema value={valor}>{children}</ContextoTema>;
}
export function useTema() {
const contexto = useContext(ContextoTema);
if (contexto === null) {
throw new Error('useTema debe usarse dentro de <ProveedorTema>');
}
return contexto;
}Qué consigue y qué no, con precisión:
- Consigue: si
ProveedorTemase reejecuta por un motivo ajeno al tema —por ejemplo, porque su padre se repintó—,valores el mismo objeto,Object.isdatruey React no notifica a ningún consumidor. - No consigue: cuando el tema sí cambia, todos los consumidores se reejecutan, incluidos los que solo usan
alternarTema. Para eso está la división de contextos en estado y acciones de 07-02, que es una solución estructural y complementaria.
Aplicado a ContextoAvisos, donde la división ya existe, el resultado es el patrón completo:
// src/contextos/ContextoAvisos.jsx (fragmento con la memorización completa)
export function ProveedorAvisos({ children }) {
const [avisos, setAvisos] = useState([]);
const descartarAviso = useCallback((id) => {
setAvisos((previos) => previos.filter((aviso) => aviso.id !== id));
}, []);
const mostrarAviso = useCallback((tono, texto, duracion = 5000) => {
const id = `aviso-${crypto.randomUUID().slice(0, 8)}`;
setAvisos((previos) => [...previos, { id, tono, texto }]);
if (duracion > 0) setTimeout(() => descartarAviso(id), duracion);
return id;
}, [descartarAviso]);
// Las ACCIONES no cambian NUNCA: sus consumidores no se reejecutan jamás
const acciones = useMemo(
() => ({ mostrarAviso, descartarAviso }),
[mostrarAviso, descartarAviso]
);
return (
<ContextoAccionesAvisos value={acciones}>
<ContextoEstadoAvisos value={avisos}>
{children}
</ContextoEstadoAvisos>
</ContextoAccionesAvisos>
);
}El resultado combinado es exactamente lo que se buscaba en 07-02: un componente como FormularioReserva, que solo llama a mostrarAviso y nunca lee la lista, no se reejecuta nunca por culpa de los avisos. Antes se reejecutaba con cada aviso que apareciera o desapareciera en cualquier punto de la aplicación.
Regla operativa, ahora justificada: todo proveedor que publique un objeto literal como
valuedebe estabilizarlo conuseMemo, y las funciones que contenga conuseCallback. Es de los poquísimos sitios donde memorizar es la opción por defecto y no una optimización prematura, porque el coste de no hacerlo se propaga a toda la aplicación.
- Dependencias de
useEffect y selectores de Redux
useEffect y selectores de ReduxEn efectos (retomando 05-02): una función o un objeto en el array de dependencias de un useEffect provoca que el efecto se vuelva a ejecutar en cada render.
// ❌ Bucle de peticiones: opciones es un objeto nuevo cada render
function PanelIncidencias({ estacionId }) {
const opciones = { estacionId, incluirCerradas: false };
useEffect(() => {
cargarIncidencias(opciones).then(setIncidencias);
}, [opciones]); // cambia SIEMPRE → petición en cada render
}
// ✅ Opción A: estabilizar el objeto
const opciones = useMemo(() => ({ estacionId, incluirCerradas: false }), [estacionId]);
// ✅ Opción B, mejor: no crear el objeto fuera del efecto
useEffect(() => {
cargarIncidencias({ estacionId, incluirCerradas: false }).then(setIncidencias);
}, [estacionId]); // solo primitivosLa opción B es casi siempre preferible, y esa es la enseñanza: cuando una dependencia inestable causa problemas en un efecto, la primera pregunta no es «¿cómo la memorizo?» sino «¿por qué está fuera del efecto?».
En selectores de Redux (retomando 07-05): un selector creado en línea que construye un valor devuelve una referencia nueva en cada llamada, y useSelector compara por identidad, así que el componente se repinta con cada acción despachada en la aplicación.
// ❌ Objeto nuevo en cada ejecución del selector
const { activas, total } = useSelector((estado) => ({
activas: estado.reservas.ids.filter((id) => estado.reservas.entidades[id].estado === 'activa'),
total: estado.reservas.ids.length
}));
// ✅ Selector memorizado en el slice (createSelector), leído sin construir nada
const resumen = useSelector(seleccionarResumenReservas);
// ✅ Fábrica de selectores con argumento, con su instancia estabilizada
const selectorReservas = useMemo(crearSelectorReservasDeUsuario, []);
const reservas = useSelector((estado) => selectorReservas(estado, usuarioId));La distinción entre las tres capas de memorización que ahora conviven en CicloUrbano:
| Capa | Herramienta | Ámbito | Qué evita |
|---|---|---|---|
| Almacén | createSelector |
Global, compartido por todos los componentes | Recalcular derivaciones del estado de Redux |
| Componente | useMemo |
Una instancia de un componente | Recalcular en cada render de ese componente |
| Servidor | staleTime de Query |
Global, por clave de consulta | Volver a pedir datos al servidor |
- Cómo elegir las dependencias y por qué el linter tiene razón
La regla es exacta y no admite excepciones: el array de dependencias debe contener todos los valores reactivos que la fábrica lee. Valor reactivo es cualquier cosa declarada dentro del componente que pueda cambiar entre renders: props, estado, valores de contexto, y cualquier variable derivada de ellos.
function PanelReserva({ bicicleta, descuento }) {
const [horas, setHoras] = useState(1);
const { usuario } = useSesion();
const importe = useMemo(
() => bicicleta.precioHora * horas * (1 - descuento),
[bicicleta.precioHora, horas, descuento] // los tres valores que lee, ninguno más
);
// `usuario` NO es dependencia: la fábrica no lo lee
}Qué pasa si te equivocas, en cada dirección:
| Error | Consecuencia | Gravedad |
|---|---|---|
| Falta una dependencia | El valor se queda congelado con datos viejos. La pantalla miente | Grave: es un fallo de corrección |
| Sobra una dependencia | Recalcula de más. Solo se pierde la optimización | Leve |
Array vacío [] en un valor que depende de props |
Congelado para siempre | Muy grave |
| Sin array (omitirlo) | Recalcula siempre: el useMemo no hace nada |
Leve, pero es código muerto |
Por eso eslint-plugin-react-hooks con la regla exhaustive-deps no es opcional en un proyecto serio, y por eso silenciarla con un comentario casi siempre esconde un problema de diseño:
// ❌ La señal de alarma más fiable del ecosistema React
// eslint-disable-next-line react-hooks/exhaustive-depsCuando sientas la tentación de escribir esa línea, el problema real suele ser uno de estos tres: la fábrica hace algo que debería estar en un efecto, la dependencia debería estabilizarse en el componente padre, o el valor no debería estar en el estado.
- Cuando una dependencia cambia demasiado
Este es el caso difícil: has puesto las dependencias correctas y una de ellas cambia en cada render, con lo que la memorización nunca acierta. Cuatro salidas, ordenadas de mejor a peor:
1. Sacar lo que no depende de nada fuera del componente.
// ✅ Constantes y funciones puras: al ámbito del módulo
const TIPOS_PERMITIDOS = ['urbana', 'electrica', 'carga'];
const FORMATO_EURO = new Intl.NumberFormat('es-ES', { style: 'currency', currency: 'EUR' });
function calcularImporte(precioHora, horas) {
return precioHora * horas;
}
function PanelReserva({ bicicleta }) {
// Ni useMemo ni useCallback: estos valores no se crean nunca más de una vez
}Es la mejor solución con diferencia: cero memorización, cero dependencias, cero riesgo. Antes de memorizar cualquier cosa, pregúntate si depende realmente de algo del componente.
2. Mover la función dentro de quien la usa. Si manejarEnvio solo la usa un efecto, defínela dentro del efecto y desaparece del array de dependencias (05-02).
3. Usar useReducer en lugar de varios useState. La función despachar que devuelve useReducer es estable por contrato: React garantiza que no cambia entre renders. Eso permite pasarla directamente a componentes memorizados sin ningún useCallback.
// El estado del formulario de reserva, con dispatch estable
const [borrador, despachar] = useReducer(reductorBorrador, BORRADOR_INICIAL);
// `despachar` es estable: memo funciona sin useCallback
<FormularioReserva borrador={borrador} alDespachar={despachar} />Lo mismo vale para las funciones setX de useState y para el dispatch de react-redux: todas son estables y no necesitan useCallback nunca.
4. Usar una referencia para valores que no deben provocar recálculo. Es el último recurso, con cuidado, y solo para valores que se leen en manejadores o efectos, nunca durante el render:
const alReservarRef = useRef(alReservar);
useEffect(() => { alReservarRef.current = alReservar; }); // siempre al día
const manejarClic = useCallback((id) => {
alReservarRef.current(id); // lee la versión más reciente sin depender de ella
}, []); // identidad estable de por vidaEste patrón —el «evento efectivo»— es útil, pero rompe el flujo de datos explícito y complica la lectura. Úsalo cuando las tres opciones anteriores no sirvan, no antes.
- Cuándo NO memorizar
| No memorices | Por qué |
|---|---|
Valores primitivos (const total = precio * horas) |
Multiplicar es más barato que consultar la memorización |
Cálculos triviales (array.length, una concatenación, un Boolean()) |
El hook cuesta más que el cálculo |
| Filtros sobre arrays cortos (menos de ~100 elementos y sin trabajo por elemento) | Recorrer 20 objetos es ruido estadístico |
Funciones que solo van a un elemento del DOM (onClick de un <button>) |
Al DOM le da igual la identidad de la función |
| Funciones que solo usa el propio componente | Nadie las compara |
| Props de un componente que no está memorizado | No hay ninguna comparación que aprovechar |
| Componentes que se renderizan una vez | No hay renders repetidos que ahorrar |
| «Por si acaso» | Es la definición literal de optimización prematura |
El caso más frecuente y el más inútil de todos:
// ❌ useCallback sin nadie que compare
function PaginaAcceso() {
const manejarEnvio = useCallback((evento) => {
evento.preventDefault();
iniciarSesion(usuario);
}, [usuario]);
return <form onSubmit={manejarEnvio}>…</form>; // un <form> del DOM: no compara nada
}Aquí useCallback añade un array de dependencias que mantener, una comparación por render y memoria, a cambio de exactamente nada. La versión correcta es una función normal.
Y la cuenta honesta del coste, que rara vez se hace explícita: cada useMemo o useCallback supone una llamada al hook, un array creado en cada render, una comparación por dependencia y memoria retenida mientras el componente viva. Es poco, pero es distinto de cero, y multiplicado por cientos de usos innecesarios es medible.
- Alternativas mejores que memorizar
Antes de escribir un useMemo, recorre esta tabla. Cada fila es una técnica que resuelve el mismo problema sin dependencias que mantener.
| Situación | En vez de memorizar | Por qué es mejor |
|---|---|---|
| Un estado hace repintar un subárbol grande | Bajar el estado (08-01) | Elimina el render en lugar de saltárselo. Menos código |
| Un contenedor con estado envuelve contenido caro | Pasar children (08-01) |
Aislamiento estructural, sin comparación ni memoria |
| El valor no depende de nada del componente | Sacarlo fuera del componente | Se crea una vez en toda la aplicación |
Varios useState con manejadores que se pasan hacia abajo |
useReducer |
despachar es estable por contrato |
| Un cálculo caro sobre el estado de Redux | createSelector en el slice |
Memorización compartida por todos los componentes |
| Un cálculo caro sobre datos del servidor | select de useQuery |
Se calcula al llegar el dato, no en cada render |
| Una lista larguísima | Paginar o virtualizar (08-01) | Reduce el trabajo real, no lo cachea |
| El resultado depende solo del identificador | Índice Map construido una vez |
Cambia la complejidad del algoritmo |
| Un filtrado que bloquea el tecleo | useDeferredValue (apartado 14) |
Reordena el trabajo en vez de evitarlo |
El árbol de decisión completo, que resume los apartados 4 a 13:
flowchart TD
A["Voy a escribir un useMemo<br/>o un useCallback"] --> B{"Depende de algo<br/>del componente?"}
B -->|No| C["Sacalo FUERA del componente<br/>constante o funcion pura del modulo"]
B -->|Si| D{"Por que quiero<br/>memorizar?"}
D -->|"El calculo es caro"| E{"Lo he medido?<br/>console.time + CPU 4x"}
E -->|No| F["Mide primero.<br/>Por debajo de 1 ms: no memorices"]
E -->|"Si, mas de 5 ms"| G{"Puedo reducir el<br/>trabajo en vez de cachearlo?"}
G -->|Si| H["Indice Map, Collator reutilizado,<br/>paginar, select de useQuery"]
G -->|No| I["useMemo justificado"]
D -->|"Alguien compara<br/>la identidad"| J{"Quien compara?"}
J -->|Nadie| K["No memorices:<br/>coste puro"]
J -->|"Componente con memo"| L["useCallback / useMemo<br/>y comprueba que memo esta puesto"]
J -->|"Dependencia de useEffect"| M["Mejor: mueve el valor<br/>DENTRO del efecto"]
J -->|"value de un contexto"| N["useMemo + useCallback:<br/>aqui es la opcion por defecto"]
J -->|"Selector de Redux"| O["createSelector en el slice"]
- Hooks de concurrencia:
useTransition y useDeferredValue
useTransition y useDeferredValueLos dos hooks anteriores intentan hacer menos trabajo. Los de concurrencia hacen algo distinto: dejan que React interrumpa y posponga trabajo no urgente para que la interfaz siga respondiendo. React puede empezar a renderizar una actualización marcada como no urgente, abandonarla a media si llega una entrada del usuario, atender esa entrada y retomar después.
useTransition
Devuelve un booleano —«hay una transición en curso»— y una función para envolver las actualizaciones no urgentes.
// src/componentes/SelectorTipo.jsx
import { useTransition } from 'react';
import { useSearchParams } from 'react-router';
function SelectorTipo() {
const [parametros, setParametros] = useSearchParams();
const [estaPendiente, iniciarTransicion] = useTransition();
const tipo = parametros.get('tipo') ?? 'todos';
function manejarCambio(evento) {
const nuevoTipo = evento.target.value;
// No urgente: repintar 2.000 tarjetas puede esperar e interrumpirse
iniciarTransicion(() => {
setParametros(nuevoTipo === 'todos' ? {} : { tipo: nuevoTipo });
});
}
return (
<select value={tipo} onChange={manejarCambio} aria-busy={estaPendiente}>
<option value="todos">Todos los tipos</option>
<option value="urbana">Urbana</option>
<option value="electrica">Eléctrica</option>
<option value="carga">Carga</option>
</select>
);
}Lo que cambia: sin la transición, elegir «Eléctrica» congela el <select> mientras React filtra y pinta el catálogo. Con ella, el desplegable responde al instante, estaPendiente permite atenuar la lista mientras se recalcula, y si el usuario cambia de opción otra vez, React abandona el render a medias y empieza el nuevo.
Requisito importante: la actualización debe ir dentro de iniciarTransicion y ser síncrona. Y no funciona con campos de texto controlados: el valor de un <input> es una actualización urgente por definición y marcarla como transición produce un campo que se siente roto.
useDeferredValue
Devuelve una copia del valor que se queda atrás a propósito. React renderiza primero con el valor viejo (rápido, urgente) y después, en segundo plano y de forma interrumpible, con el nuevo.
// src/paginas/PaginaCatalogo.jsx (fragmento)
function PaginaCatalogo() {
const termino = useSelector((estado) => estado.catalogo.termino);
const terminoDiferido = useDeferredValue(termino);
const estaObsoleto = termino !== terminoDiferido;
const visibles = useMemo(
() => filtrarYOrdenar(bicicletas, estaciones, terminoDiferido, tipo, orden),
[bicicletas, estaciones, terminoDiferido, tipo, orden]
);
return (
<>
<BuscadorBicicletas />
<div style={{ opacity: estaObsoleto ? 0.6 : 1, transition: 'opacity 150ms' }}>
<ListaBicicletas bicicletas={visibles} />
</div>
</>
);
}La combinación de useDeferredValue + useMemo + memo es el patrón completo: el valor diferido reduce cuántas veces se recalcula durante el tecleo rápido, el useMemo evita recalcular cuando nada relevante cambió, y el memo de TarjetaBicicleta evita reejecutar las tarjetas que no se han movido. Ninguno sustituye a los otros.
Los tres, comparados
useDebounce (05-06) |
useTransition |
useDeferredValue |
|
|---|---|---|---|
| Qué problema resuelve | Trabajo demasiado frecuente | Trabajo urgente mezclado con no urgente | Un valor cuyo consumo es caro |
| Mecanismo | Temporizador: espera al silencio | Marca la actualización como interrumpible | Renderiza en dos pasadas, la segunda diferible |
| Retraso | Fijo, lo eliges tú (300 ms) | Ninguno: se adapta a la carga real | Ninguno fijo: se adapta |
| Interrumpible | No: cuando se ejecuta, bloquea | Sí | Sí |
| Evita peticiones al servidor | Sí, es su punto fuerte | No | No |
| Indicador de progreso | Manual | estaPendiente |
Comparar valor y valor diferido |
| Dónde se coloca | En el productor del valor | En quien provoca la actualización | En quien consume el valor |
| En CicloUrbano | Buscador → petición a json-server |
SelectorTipo, cambio de orden |
Filtrado local del catálogo |
La conclusión práctica, que es más útil que la tabla: no son alternativas, son complementarios. En el BuscadorBicicletas definitivo conviven los dos: useDebounce para no lanzar una petición por tecla —eso es tráfico de red y el temporizador es la herramienta correcta— y useDeferredValue sobre el término para que el filtrado local de las 2.000 bicicletas no bloquee el campo entre petición y petición.
- El React Compiler aquí
El React Compiler de React 19 es, en esencia, un generador automático de useMemo y useCallback. En un proyecto compilado:
| Trabajo | ¿Lo hace el compilador? |
|---|---|
Estabilizar manejarSeleccion y manejarReserva en ListaBicicletas |
✅ Sí, sin useCallback |
Estabilizar el objeto valor de ProveedorTema |
✅ Sí, si se construye en el propio componente |
| Evitar repetir el filtrado y la ordenación cuando las entradas no cambian | ✅ Sí, memoriza la expresión |
| Hacer que la primera ordenación de 2.000 bicicletas sea rápida | ❌ No. El algoritmo sigue siendo tuyo |
Sustituir el Map de estaciones por un find en cada elemento |
❌ No: no reescribe algoritmos |
| Saber que una función importada de otro módulo es estable | ❌ No: no razona fuera del componente |
Decidir useTransition o useDeferredValue |
❌ No: son decisiones de experiencia de usuario |
Corregir un useEffect que se dispara de más por una dependencia externa |
❌ No |
| Optimizar un componente que rompe las reglas de React | ❌ No: lo omite entero y en silencio |
Cómo comprobar qué hizo. El compilador no es una caja negra:
- React DevTools marca los componentes optimizados con una insignia ✨ Memo ✨ junto a su nombre. Es la comprobación más rápida: si tu componente crítico no la tiene, el compilador lo ha omitido y hay que averiguar por qué.
- El Profiler (08-05) sigue siendo la prueba definitiva: mide la misma interacción con y sin compilador en un build de producción y compara.
- El linter (
eslint-plugin-react-hookscon las reglas del compilador) te dice de antemano qué componentes se van a omitir, y por qué.
Y la parte que sigue siendo tuya, resumida en una frase: el compilador decide cuándo reutilizar un valor; tú decides qué valor merece la pena calcular, con qué algoritmo, y si ese trabajo debería estar ocurriendo siquiera.
Errores Comunes y Consejos
Confundir los dos usos legítimos. «Memorizo porque es caro» y «memorizo porque alguien compara la identidad» son razones distintas y llevan a decisiones distintas. Un useMemo sobre un cálculo trivial es inútil… salvo que el resultado sea un objeto que va a un componente memorizado, en cuyo caso es imprescindible. Ten siempre clara cuál de las dos razones te aplica.
useCallback sobre funciones que nadie compara. Es el uso mayoritario y el más inútil. Si la función va a un onClick de un <button>, quítalo.
Silenciar exhaustive-deps. Un useMemo con dependencias incompletas produce datos obsoletos en pantalla: un fallo de corrección, no de rendimiento. Si el linter te molesta, el problema está en el diseño.
Memorizar dentro de un .map(). Los hooks no pueden llamarse en bucles (regla de 04-04). Si cada elemento necesita memorización, la memorización va dentro del componente hijo, no en el padre.
Creer que useMemo garantiza la ejecución única. React puede descartar el valor guardado. No pongas ahí nada de lo que dependa la corrección de tu código.
Usar useTransition en un campo de texto controlado. El valor del <input> es urgente por definición; diferirlo produce un campo que se siente estropeado. Lo que se difiere es su consumo, con useDeferredValue.
Consejo: primero el algoritmo, después la memorización. El Map de estaciones y el Intl.Collator reutilizado bajan más el coste que cualquier useMemo, y siguen funcionando cuando las dependencias cambian de verdad.
Consejo: estabiliza desde arriba. Una identidad inestable invalida toda la memorización que hay por debajo. Arregla el value del proveedor y el objeto de opciones del componente raíz antes de tocar las hojas.
Consejo: memoriza el paquete entero, no las piezas. En lugar de tres useMemo para tres campos, uno solo para el objeto que los contiene, si es eso lo que se va a comparar.
Ejercicios
Ejercicio 1. Para cada uso, di si el hook es necesario, sobra o está mal escrito, y corrige los dos últimos casos.
// a)
const total = useMemo(() => horas * bicicleta.precioHora, [horas, bicicleta.precioHora]);
// b)
const bicicletasOrdenadas = useMemo(
() => [...bicicletas].sort((x, y) => x.precioHora - y.precioHora),
[bicicletas]
);
// bicicletas tiene 2.000 elementos y se pasa a <ListaBicicletas> (memorizada)
// c)
const manejarCierre = useCallback(() => setModalAbierto(false), []);
// Uso: <Modal alCerrar={manejarCierre} /> ← Modal NO está memorizado
// d)
const filtradas = useCallback(bicicletas.filter((b) => b.estado === 'disponible'), [bicicletas]);
// e)
const valor = useMemo(() => ({ usuario, iniciarSesion, cerrarSesion }), []);
// Uso: <ContextoSesion value={valor}>Ejercicio 2. PaginaDetalleEstacion vuelve a pedir las incidencias a json-server en cada render, con lo que la pestaña parpadea sin parar. Diagnostica la causa exacta y propón dos soluciones: una con memorización y otra sin ella. Indica cuál elegirías y por qué.
function PaginaDetalleEstacion() {
const { estacionId } = useParams();
const [incidencias, setIncidencias] = useState([]);
const filtros = { estacionId, estado: 'abierta', orden: 'fecha' };
useEffect(() => {
let ignorar = false;
cargarIncidencias(filtros).then((datos) => { if (!ignorar) setIncidencias(datos); });
return () => { ignorar = true; };
}, [filtros]);
return <PestanaIncidencias incidencias={incidencias} />;
}Ejercicio 3. El BuscadorBicicletas de CicloUrbano debe cumplir tres requisitos a la vez: (1) el campo responde instantáneamente a cada tecla; (2) no se lanza una petición a json-server por cada tecla; (3) el filtrado local de las 2.000 bicicletas no bloquea el tecleo. Escribe la versión que cumple los tres, usando useDebounce, useDeferredValue y useMemo donde corresponda, y justifica en una tabla qué requisito cubre cada herramienta.
Soluciones
Solución 1.
a) Sobra. Una multiplicación de dos números. El useMemo cuesta más que el cálculo, y el resultado es un primitivo que se compara por valor. Corrección: const total = horas * bicicleta.precioHora;.
b) Es necesario, por partida doble. Ordenar 2.000 elementos es un cálculo de coste real (uso legítimo 1) y el resultado es un array cuya identidad importa para el memo de ListaBicicletas (uso legítimo 2). Además está bien escrito: [...bicicletas] copia antes de ordenar, porque sort muta el array original y mutar los datos de la caché de Query sería un error grave.
c) Sobra. Modal no está memorizado, así que nadie compara alCerrar: se reejecuta igualmente. Corrección: función normal, const manejarCierre = () => setModalAbierto(false);. (Si mañana Modal se envolviera en memo, el useCallback pasaría a ser necesario.)
d) Está mal escrito. useCallback guarda funciones, y aquí se le pasa el resultado de un .filter(): filtradas acaba siendo un array, no una función, y la memorización no hace lo que parece. Corrección:
e) Está mal escrito, y es el error más peligroso de los cinco. El array de dependencias está vacío, así que valor se congela con el usuario del primer render: cuando alguien inicie sesión, ningún consumidor del contexto se enterará. Un fallo de corrección disfrazado de optimización. Corrección:
const valor = useMemo(
() => ({ usuario, iniciarSesion, cerrarSesion }),
[usuario, iniciarSesion, cerrarSesion]
);Con iniciarSesion y cerrarSesion estabilizadas a su vez con useCallback en el proveedor.
Solución 2. Causa exacta: filtros es un objeto literal creado en cada render. El efecto lo tiene como dependencia, Object.is da false siempre, el efecto se ejecuta, setIncidencias provoca un render, el render crea otro filtros… y el ciclo no termina. Es un bucle de peticiones, no un parpadeo casual.
Solución A, con memorización:
Solución B, sin memorización: el objeto no tiene por qué existir fuera del efecto.
useEffect(() => {
let ignorar = false;
cargarIncidencias({ estacionId, estado: 'abierta', orden: 'fecha' })
.then((datos) => { if (!ignorar) setIncidencias(datos); });
return () => { ignorar = true; };
}, [estacionId]); // solo un primitivoElegiría la B, por tres razones: no añade un hook ni un array de dependencias que mantener, la única dependencia es un primitivo (imposible de romper por identidad) y expresa mejor la intención —«cuando cambie la estación, recarga»—. La regla general: cuando una dependencia inestable rompe un efecto, la primera pregunta es por qué está fuera del efecto.
Y la solución realmente correcta en el CicloUrbano de hoy, después de 07-06: esto no debería ser un useEffect. Es estado del servidor, y le corresponde una consulta con su clave jerárquica:
const { data: incidencias = [] } = useQuery({
queryKey: claves.estaciones.incidencias(estacionId),
queryFn: () => cargarIncidencias({ estacionId, estado: 'abierta', orden: 'fecha' })
});Solución 3.
// src/componentes/BuscadorBicicletas.jsx (versión definitiva)
import { useState, useEffect, useDeferredValue } from 'react';
import { useDispatch } from 'react-redux';
import { useDebounce } from '../hooks/useDebounce.js';
import { terminoCambiado } from '../almacen/sliceCatalogo.js';
function BuscadorBicicletas() {
const [texto, setTexto] = useState(''); // (1) responde a cada tecla
const terminoRetrasado = useDebounce(texto, 300); // (2) una petición por pausa
const despachar = useDispatch();
useEffect(() => {
despachar(terminoCambiado(terminoRetrasado));
}, [terminoRetrasado, despachar]);
return (
<input
type="search"
value={texto}
onChange={(evento) => setTexto(evento.target.value)}
aria-label="Buscar bicicletas por modelo"
/>
);
}// src/paginas/PaginaCatalogo.jsx (fragmento)
const termino = useSelector((estado) => estado.catalogo.termino);
const terminoDiferido = useDeferredValue(termino); // (3) no bloquea el tecleo
const estaObsoleto = termino !== terminoDiferido;
const visibles = useMemo( // (3) y estabilidad de identidad
() => filtrarYOrdenar(bicicletas, estaciones, terminoDiferido, tipo, orden),
[bicicletas, estaciones, terminoDiferido, tipo, orden]
);| Herramienta | Requisito que cubre | Por qué no sirve otra |
|---|---|---|
useState local en el buscador |
(1) El campo responde a cada tecla | Es estado de interfaz puro; ninguna otra herramienta debe intervenir aquí |
useDebounce |
(2) Una petición por pausa, no por tecla | Solo un temporizador evita tráfico de red; ni useDeferredValue ni useTransition reducen peticiones |
useDeferredValue |
(3) El filtrado no bloquea el tecleo | Es interrumpible y sin retraso fijo; un segundo useDebounce añadiría latencia percibida |
useMemo |
(3) No repetir el cálculo, y estabilizar visibles |
El filtrado de 2.000 elementos es caro y su resultado alimenta un componente memorizado |
memo en TarjetaBicicleta |
(3) No reejecutar las tarjetas que no cambian | Cierra la cadena: sin él, el array nuevo repinta las 2.000 igual |
Conclusión
useMemo(fabrica, dependencias) guarda el valor que produce la fábrica y solo la vuelve a ejecutar cuando alguna dependencia cambia según Object.is; useCallback(funcion, dependencias) es literalmente useMemo(() => funcion, deps) y guarda la función. La fábrica se ejecuta durante el render, así que debe ser pura, y la memorización es una optimización, no una garantía: React puede descartar el valor guardado, y nada de lo que dependa la corrección de tu código puede apoyarse en ella.
Lo que ordena todo lo demás son los dos usos legítimos, que hay que mantener siempre separados. El primero es evitar un cálculo realmente costoso: filtrar, indexar y ordenar 2.000 bicicletas con Intl.Collator, medido antes con console.time y con la CPU ralentizada 4×, con la referencia de los 16 ms del fotograma y la disciplina de quitar el useMemo y volver a medir. Y con la lección de fondo: el Map de estaciones y el colador reutilizado bajan más el coste que el propio useMemo, porque cambian el algoritmo en lugar de cachear el resultado. El segundo uso es mantener la identidad de un valor cuando alguien la compara: un componente envuelto en memo, un array de dependencias de useEffect, el value de un proveedor de contexto o un selector de Redux.
Con eso se han cerrado dos deudas del curso. La de 08-02: ListaBicicletas estabiliza manejarSeleccion y manejarReserva con useCallback([navegar]), y solo entonces el memo de TarjetaBicicleta empieza a servir de algo —los dos juntos, nunca por separado—. Y la de 07-02: ProveedorTema publica un value estabilizado con useMemo y una alternarTema estabilizada con useCallback, y ProveedorAvisos combina esa memorización con la división en contextos de estado y acciones, de modo que un componente que solo llama a mostrarAviso no se reejecuta nunca por los avisos.
Sobre las dependencias, la regla no admite matices: todos los valores reactivos que la fábrica lee, ni uno más ni uno menos. Que sobre una dependencia solo cuesta rendimiento; que falte produce datos obsoletos en pantalla, que es un fallo de corrección. Por eso exhaustive-deps tiene razón casi siempre y silenciarla es la señal de alarma más fiable del ecosistema. Y cuando una dependencia cambia demasiado, hay salidas mejores que insistir: sacar fuera del componente lo que no depende de nada, mover la función dentro del efecto, usar useReducer porque despachar es estable por contrato —igual que setEstado y el dispatch de Redux— y, como último recurso, la referencia siempre al día.
También sabes cuándo no memorizar: primitivos, cálculos triviales, listas cortas, funciones que solo van a un elemento del DOM, props de componentes no memorizados, y el «por si acaso», que es la definición literal de optimización prematura. Y qué hacer en su lugar: bajar el estado, pasar children, sacar constantes fuera, createSelector, select de useQuery, paginar o virtualizar. Por último, los hooks de concurrencia, que atacan el mismo síntoma desde otro ángulo: useTransition marca como interrumpible la actualización que tú provocas —el cambio de tipo o de orden del catálogo—, useDeferredValue difiere el consumo de un valor caro sin retraso fijo, y ninguno sustituye a useDebounce, que sigue siendo la única de las tres que evita peticiones al servidor. En el buscador definitivo conviven las tres. El React Compiler, por su parte, escribe por ti la memorización mecánica y la marca con una insignia ✨ en DevTools, pero no mejora tu algoritmo, no razona sobre valores importados y no decide por ti qué trabajo debería estar ocurriendo.
Con esto, el catálogo de CicloUrbano ya no repite trabajo innecesario cuando el usuario interactúa. Queda el otro problema que 07-06 dejó apuntado y que ninguna memorización toca: que el navegador descarga todo el JavaScript de la aplicación —el taller, el detalle de estaciones, el formulario de reserva, la biblioteca de gráficas— antes de mostrar la primera bicicleta. La próxima lección es División de Código y Carga Perezosa.
Curso de React
Módulo 1: Introducción a React
- ¿Qué es React?
- Configuración del Entorno de Desarrollo
- Hola Mundo en React
- JSX: Extensión de Sintaxis de JavaScript
- Cómo Renderiza React: Virtual DOM y Reconciliación
Módulo 2: Componentes de React
- Entendiendo los Componentes
- Componentes Funcionales vs de Clase
- Props: Pasando Datos a Componentes
- State: Gestión del Estado del Componente
- Estilos en los Componentes: CSS, Módulos y Utilidades
Módulo 3: Trabajando con Eventos
- Manejo de Eventos en React
- Renderizado Condicional
- Listas y Claves
- Formularios y Componentes Controlados
- Validación de Formularios y Componentes No Controlados
- Accesibilidad en Componentes Interactivos
Módulo 4: Conceptos Avanzados de Componentes
- Elevando el Estado
- Composición vs Herencia
- Métodos del Ciclo de Vida de React
- Hooks: Introducción y Uso Básico
- Límites de Error: Capturar Fallos en la Interfaz
Módulo 5: Hooks de React
- Hook useState
- Hook useEffect
- Hook useRef y Acceso al DOM
- Hook useContext
- Hook useReducer
- Hooks Personalizados
Módulo 6: Enrutamiento en React
- Introducción a React Router
- Configuración de React Router
- Rutas Anidadas
- Navegación Programática
- Rutas Protegidas y Control de Acceso
Módulo 7: Gestión del Estado
- Introducción a la Gestión del Estado
- API de Contexto
- Redux: Introducción y Configuración
- Redux: Acciones y Reductores
- Redux: Conectando a React
- Estado del Servidor: Peticiones, Caché y Sincronización
Módulo 8: Optimización del Rendimiento
- Técnicas de Optimización del Rendimiento en React
- Memorización con React.memo
- Hooks useMemo y useCallback
- División de Código y Carga Perezosa
- Medir el Rendimiento con React DevTools Profiler
Módulo 9: Pruebas en React
- Introducción a las Pruebas
- Pruebas Unitarias con Jest
- Pruebas de Componentes con React Testing Library
- Pruebas de Código Asíncrono y Simulación de APIs
- Pruebas de Extremo a Extremo con Cypress
Módulo 10: Temas Avanzados
- Renderizado del Lado del Servidor (SSR) con Next.js
- Generación de Sitios Estáticos (SSG) con Next.js
- Suspense y React Server Components
- TypeScript con React
- React Native: Creación de Aplicaciones Móviles
