Saltar al contenido principal

React — Avanzado 🟡

useCallback y useMemo

Optimizaciones para evitar re-renders y recalculos innecesarios.

import { useState, useCallback, useMemo } from 'react';

function ListaProductos({ categoriaId }: { categoriaId: number }) {
const [busqueda, setBusqueda] = useState('');
const [productos, setProductos] = useState<Producto[]>([]);

// useMemo: memoriza el resultado de un cálculo costoso
const productosFiltrados = useMemo(() => {
console.log('Calculando filtro...'); // solo cuando busqueda o productos cambia
return productos.filter(p =>
p.nombre.toLowerCase().includes(busqueda.toLowerCase())
);
}, [productos, busqueda]);

// useCallback: memoriza una función (evita que el hijo se re-renderice)
const handleEliminar = useCallback((id: number) => {
setProductos(prev => prev.filter(p => p.id !== id));
}, []); // no depende de nada externo

return (
<>
<input value={busqueda} onChange={e => setBusqueda(e.target.value)} />
{productosFiltrados.map(p => (
<ItemProducto key={p.id} producto={p} onEliminar={handleEliminar} />
))}
</>
);
}

// memo: evita re-render si las props no cambiaron
const ItemProducto = React.memo(({ producto, onEliminar }: ItemProps) => {
console.log('Render ItemProducto', producto.id);
return (
<div>
{producto.nombre}
<button onClick={() => onEliminar(producto.id)}>Eliminar</button>
</div>
);
});
Cuándo usar memo/useCallback/useMemo

Solo cuando tienes un problema de rendimiento comprobado. La optimización prematura añade complejidad. Úsalos cuando: el componente renderiza frecuentemente, el cálculo es costoso, o el hijo tiene React.memo.


useReducer

Para estado complejo con múltiples sub-valores o lógica de transición.

type Estado = {
cargando: boolean;
error: string | null;
productos: Producto[];
};

type Accion =
| { type: 'CARGANDO' }
| { type: 'EXITO'; payload: Producto[] }
| { type: 'ERROR'; payload: string }
| { type: 'ELIMINAR'; payload: number };

const estadoInicial: Estado = {
cargando: false,
error: null,
productos: [],
};

function reducer(estado: Estado, accion: Accion): Estado {
switch (accion.type) {
case 'CARGANDO':
return { ...estado, cargando: true, error: null };
case 'EXITO':
return { cargando: false, error: null, productos: accion.payload };
case 'ERROR':
return { cargando: false, error: accion.payload, productos: [] };
case 'ELIMINAR':
return {
...estado,
productos: estado.productos.filter(p => p.id !== accion.payload)
};
default:
return estado;
}
}

function Productos() {
const [estado, dispatch] = useReducer(reducer, estadoInicial);

useEffect(() => {
dispatch({ type: 'CARGANDO' });
fetch('/api/productos')
.then(r => r.json())
.then(data => dispatch({ type: 'EXITO', payload: data }))
.catch(e => dispatch({ type: 'ERROR', payload: e.message }));
}, []);

if (estado.cargando) return <Spinner />;
if (estado.error) return <Error mensaje={estado.error} />;
return <Lista productos={estado.productos} />;
}

Context API

Compartir estado global sin prop drilling.

// 1. Crear el context
interface AuthContextType {
usuario: Usuario | null;
login: (email: string, password: string) => Promise<void>;
logout: () => void;
}

const AuthContext = createContext<AuthContextType | null>(null);

// 2. Crear el Provider
export function AuthProvider({ children }: { children: React.ReactNode }) {
const [usuario, setUsuario] = useState<Usuario | null>(null);

const login = async (email: string, password: string) => {
const data = await authService.login(email, password);
setUsuario(data.usuario);
localStorage.setItem('token', data.token);
};

const logout = () => {
setUsuario(null);
localStorage.removeItem('token');
};

return (
<AuthContext.Provider value={{ usuario, login, logout }}>
{children}
</AuthContext.Provider>
);
}

// 3. Custom hook para consumir el context
export function useAuth() {
const context = useContext(AuthContext);
if (!context)
throw new Error('useAuth debe usarse dentro de AuthProvider');
return context;
}

// 4. Uso en cualquier componente
function NavBar() {
const { usuario, logout } = useAuth();
return (
<nav>
<span>Hola, {usuario?.nombre}</span>
<button onClick={logout}>Cerrar sesión</button>
</nav>
);
}

Custom Hooks

Extraer lógica reutilizable.

// useFetch — hook genérico para fetch de datos
function useFetch<T>(url: string) {
const [datos, setDatos] = useState<T | null>(null);
const [cargando, setCargando] = useState(true);
const [error, setError] = useState<Error | null>(null);

useEffect(() => {
const controller = new AbortController();

fetch(url, { signal: controller.signal })
.then(r => {
if (!r.ok) throw new Error(`HTTP ${r.status}`);
return r.json();
})
.then(data => { setDatos(data); setCargando(false); })
.catch(e => {
if (e.name !== 'AbortError') {
setError(e);
setCargando(false);
}
});

return () => controller.abort(); // cleanup: cancelar fetch al desmontar
}, [url]);

return { datos, cargando, error };
}

// useLocalStorage
function useLocalStorage<T>(key: string, valorInicial: T) {
const [valor, setValor] = useState<T>(() => {
try {
const item = localStorage.getItem(key);
return item ? JSON.parse(item) : valorInicial;
} catch {
return valorInicial;
}
});

const setValorYGuardar = (nuevoValor: T) => {
setValor(nuevoValor);
localStorage.setItem(key, JSON.stringify(nuevoValor));
};

return [valor, setValorYGuardar] as const;
}

React Query (TanStack Query)

La solución moderna para state management de servidor.

import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';

// GET datos
function Productos() {
const { data, isLoading, error } = useQuery({
queryKey: ['productos'],
queryFn: () => fetch('/api/productos').then(r => r.json()),
staleTime: 5 * 60 * 1000, // 5 minutos en caché
});

if (isLoading) return <Spinner />;
if (error) return <Error />;
return <Lista productos={data} />;
}

// Mutación (POST/PUT/DELETE)
function CrearProducto() {
const queryClient = useQueryClient();

const mutation = useMutation({
mutationFn: (nuevoProducto: CrearProductoDto) =>
fetch('/api/productos', {
method: 'POST',
body: JSON.stringify(nuevoProducto),
}).then(r => r.json()),
onSuccess: () => {
// Invalidar la caché para que React Query refetch
queryClient.invalidateQueries({ queryKey: ['productos'] });
},
});

return (
<button onClick={() => mutation.mutate({ nombre: 'Nuevo', precio: 99 })}>
{mutation.isPending ? 'Guardando...' : 'Crear'}
</button>
);
}

Suspense y Error Boundaries

Suspense

Suspense permite "pausar" el renderizado hasta que algún recurso (datos, código) esté listo.

import { Suspense } from 'react';

function App() {
return (
<Suspense fallback={<Cargando />}>
<ListaUsuarios /> {/* Mientras se cargan, muestra <Cargando/> */}
</Suspense>
);
}

function Cargando() {
return <div>Espera, estamos cargando...</div>;
}

// Componente que "suspende" (lanza una promesa)
// Típicamente usado con React Query o Promise-based code splitting
function ListaUsuarios() {
// Nota: no se puede usar async/await directamente en componentes
// Usar React Query que maneja Suspense automáticamente
const usuarios = useSuspenseQuery({
queryKey: ['usuarios'],
queryFn: () => fetch('/api/usuarios').then(r => r.json()),
});

return <div>{usuarios.data.map(u => <div key={u.id}>{u.nombre}</div>)}</div>;
}

// Code splitting con Suspense
import { lazy } from 'react';

const AdminPanel = lazy(() => import('./AdminPanel'));

function App() {
return (
<Suspense fallback={<div>Cargando panel...</div>}>
<AdminPanel /> {/* Solo descarga cuando es necesario */}
</Suspense>
);
}

Error Boundary

Error Boundary captura errores en componentes hijos y muestra una UI alternativa.

import { Component } from 'react';

class ErrorBoundary extends Component<{children: React.ReactNode}, {hasError: boolean; error: Error | null}> {
constructor(props: any) {
super(props);
this.state = { hasError: false, error: null };
}

static getDerivedStateFromError(error: Error) {
return { hasError: true, error };
}

componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
console.error('Error capturado:', error, errorInfo);
// Aquí puedes enviar a un servicio de logging (Sentry, etc)
}

render() {
if (this.state.hasError) {
return (
<div style={{ padding: 20, color: 'red' }}>
<h1>Algo salió mal</h1>
<details>
{this.state.error?.message}
</details>
<button onClick={() => window.location.reload()}>Recargar</button>
</div>
);
}

return this.props.children;
}
}

// Usar Error Boundary
function App() {
return (
<ErrorBoundary>
<ListaUsuarios />
<CrudProductos />
</ErrorBoundary>
);
}

// Nota: Error Boundary no captura:
// - Errores en event handlers (usar try/catch)
// - Errores async (promesas no resueltas)
// - Errores del servidor-side rendering

// Para errores async, usar un estado:
function FormProducto() {
const [error, setError] = useState<string | null>(null);

const guardar = async () => {
try {
await guardarProducto();
} catch (err) {
setError(err instanceof Error ? err.message : 'Error desconocido');
}
};

if (error) return <div style={{color: 'red'}}>{error}</div>;
return <form onSubmit={guardar}>...</form>;
}

useLayoutEffect

useLayoutEffect es similar a useEffect pero se ejecuta ANTES de que React pinte el DOM (synchronous).

import { useLayoutEffect, useState, useRef } from 'react';

function MedirAltura() {
const ref = useRef<HTMLDivElement>(null);
const [altura, setAltura] = useState(0);

useLayoutEffect(() => {
// Se ejecuta ANTES de que el navegador pinte
if (ref.current) {
setAltura(ref.current.offsetHeight);
}
}, []);

return (
<div ref={ref}>
<p>Contenido</p>
<p>Altura medida: {altura}px</p>
</div>
);
}

// Casos de uso:
// - Medir elementos del DOM
// - Leer valores de propiedades calculated del CSS
// - Evitar "flashes" de contenido sin estilo
// - Scrolling sincronizado

// ⚠️ Usar con cuidado: bloquea el navegador
// Si no necesitas bloquear el navegador, usa useEffect

Preguntas frecuentes de entrevista 🎯

1. ¿Cuándo usar Context API vs Redux vs React Query?

  • Context: estado global simple de UI (tema, idioma, usuario autenticado)
  • Redux/Zustand: estado global complejo con muchas interacciones
  • React Query: estado del servidor (datos que vienen de APIs) — cacheo, refetch, sincronización

2. ¿Qué es prop drilling y cómo se soluciona?

Pasar props a través de múltiples niveles de componentes intermedios que no las necesitan. Se soluciona con Context API, estado global (Zustand/Redux) o componentes de composición.

3. ¿Cuándo usarías useReducer en vez de useState?

Cuando el estado tiene múltiples sub-valores relacionados, cuando el siguiente estado depende del anterior de forma compleja, o cuando hay muchas acciones que modifican el estado de formas distintas.

4. ¿Qué es un Custom Hook?

Una función que empieza con use y puede llamar a otros hooks. Permite extraer y reutilizar lógica stateful entre componentes. No crea un estado compartido — cada uso del custom hook tiene su propio estado.

🧠 Mini-Quiz — React Avanzado1/4

¿Cuándo usarías useMemo en lugar de useCallback?