Skip to content

React Hoja de referencia

Librería JavaScript para construir interfaces de usuario con componentes.

01

JSX y componentes

Function components

Los componentes de React son funciones JavaScript que devuelven JSX. Los nombres de componentes deben comenzar con mayúscula (minúscula = tags HTML). Los props se pasan como atributos y se desestructuran en el parámetro. JSX es syntactic sugar para React.createElement(). Siempre devuelve un solo elemento root (o usa Fragments). Los componentes deben ser puros — mismos props = misma salida.

react
// Basic function component
function Welcome({ name }) {
  return <h1>Hello, {name}!</h1>;
}

// Arrow function component
const Greeting = ({ name = 'Guest' }) => (
  <p>Welcome, {name}!</p>
);

// Composing components
function App() {
  return (
    <div>
      <Welcome name="Alice" />
      <Welcome name="Bob" />
      <Greeting />
    </div>
  );
}

Expresiones JSX

JSX permite embeber expresiones JavaScript en llaves {}. Puedes poner variables, llamadas a funciones, operadores ternarios y cualquier expresión que devuelva un valor. Los statements (if, for, switch) no están permitidos directamente — usa ternary o IIFE. Los valores booleanos (true, false), null y undefined renderizan como nada. Los números y strings renderizan como texto. Los objetos no son React children válidos.

react
function Expression({ user, items }) {
  const fullName = user.first + ' ' + user.last;
  const itemCount = items.length;

  return (
    <div>
      {/* Expressions in curly braces */}
      <h1>{fullName}</h1>
      <p>{itemCount} items</p>

      {/* Conditional */}
      <p>{itemCount > 0 ? 'In stock' : 'Sold out'}</p>

      {/* Method calls */}
      <p>{fullName.toUpperCase()}</p>

      {/* Numbers and booleans render as nothing */}
      <p>{false}{null}{undefined}</p>
    </div>
  );
}

Fragments y listas en JSX

Los Fragments (<>...</>) agrupan múltiples elementos sin añadir nodos DOM adicionales — más limpio que envolver en un <div>. Usa <Fragment key={...}> cuando necesites pasar una key. Los arrays de elementos JSX necesitan props key únicos. Aunque el índice del array como key funciona para listas estáticas, usa IDs estables para listas dinámicas para prevenir errores de renderizado. Los Fragments mejoran el rendimiento al reducir elementos envolventes innecesarios.

react
// Fragment: group without extra DOM node
function App() {
  return (
    <>
      <header>Header</header>
      <main>Content</main>
      <footer>Footer</footer>
    </>
  );
}

// Array of elements (needs keys)
function List() {
  const fruits = ['Apple', 'Banana', 'Cherry'];
  return (
    <ul>
      {fruits.map((fruit, i) => (
        <li key={i}>{fruit}</li>
      ))}
    </ul>
  );
}

Renderizado Condicional

React ofrece múltiples patrones de renderizado condicional. Retornos tempranos para lógica if/else. Ternario (cond ? A : B) para uno/otro. AND lógico (cond && <Component/>) para mostrar/ocultar. IIFE para ramificaciones complejas. Evita incrustar lógica compleja en JSX — extrae a variables o funciones auxiliares. Para sentencias switch, usa un objeto de búsqueda o extrae a una función separada. Los valores falsy (0, '') se renderizan, así que usa el operador ternario en lugar de && para números.

react
function Greeting({ isLoggedIn, user }) {
  // 1. If/else (use early return)
  if (!isLoggedIn) return <Login />;

  // 2. Ternary operator
  return (
    <div>
      {user ? <Dashboard user={user} /> : <Loading />}

      {/* 3. Logical AND (render if truthy) */}
      {user.isAdmin && <AdminPanel />}

      {/* 4. IIFE for complex logic */}
      {(() => {
        if (user.role === 'admin') return <Admin />;
        if (user.role === 'mod') return <Mod />;
        return <User />;
      })()}
    </div>
  );
}

Children y Render Props

La prop children contiene elementos entre las etiquetas de apertura y cierre — esencial para componentes componibles (tarjetas, modales, layouts). Los render props pasan una función como prop que recibe datos y devuelve JSX — una alternativa a los HOCs y hooks para compartir lógica. Aunque los render props son menos comunes con hooks, siguen siendo útiles para patrones de inyección de componentes. children es una prop especial que no necesita pasarse explícitamente.

react
// children prop: content between tags
function Card({ title, children }) {
  return (
    <div className="card">
      <h2>{title}</h2>
      <div className="card-body">{children}</div>
    </div>
  );
}

// Usage
<Card title="Profile">
  <p>Name: Alice</p>
  <p>Age: 30</p>
</Card>

// Render prop pattern
function DataProvider({ render }) {
  const data = fetchData();
  return <div>{render(data)}</div>;
}
02

Props

Pasar Props

Las props son datos de solo lectura pasados del padre al hijo. Pueden ser cualquier valor de JavaScript: strings, números, booleanos, arrays, objetos o funciones. Los valores string usan comillas (name='Alice'), todos los demás valores usan llaves (age={30}). Las funciones como props permiten la comunicación de hijo a padre (callbacks). Las props fluyen hacia abajo — los hijos no pueden modificar las props. Para data binding bidireccional, eleva el estado al padre común.

react
// Parent passes props to child
function App() {
  return (
    <User
      name="Alice"
      age={30}
      isActive={true}
      tags={['admin', 'dev']}
      onClick={() => console.log('clicked')}
    />
  );
}

// Child receives props
function User({ name, age, isActive, tags, onClick }) {
  return (
    <div onClick={onClick}>
      <h1>{name}</h1>
      <p>Age: {age}</p>
      <p>Status: {isActive ? 'Active' : 'Inactive'}</p>
    </div>
  );
}

Props por Defecto y Opcionales

Los valores por defecto de las props se establecen mediante destructuring (param = defaultValue). Si una prop no se pasa, es undefined. Usa short-circuit (bio && <p>) o ternario para renderizar condicionalmente las props opcionales. PropTypes (legacy) o las interfaces de TypeScript pueden validar los tipos de prop en tiempo de desarrollo. defaultProps (componentes de clase) está deprecado para componentes función — usa defaults mediante destructuring en su lugar.

react
// Default values via destructuring
function Button({ color = 'blue', size = 'md', children }) {
  return (
    <button className={'btn btn-' + color + ' btn-' + size}>
      {children}
    </button>
  );
}

// Optional props (undefined if not passed)
function Profile({ name, bio }) {
  return (
    <div>
      <h1>{name}</h1>
      {bio && <p>{bio}</p>}
    </div>
  );
}

// Usage
<Button>Click</Button>           {/* color='blue', size='md' */}
<Profile name="Alice" />         {/* bio is undefined */}

Props Spread y Rest

El operador spread (...props) pasa todas las props a un elemento hijo — útil para componentes envolventes (HOCs, styled components). El operador rest recoge las props restantes después de destructurar las específicas. Este patrón es común en sistemas de diseño donde un componente envolvente reenvía props desconocidas a un elemento DOM. Ten cuidado: el spread puede sobrescribir atributos explícitos — el orden importa ({...props} className='x' vs className='x' {...props}).

react
// Spread: pass all props to child
function Input(props) {
  return <input {...props} className="input" />;
}

// Usage
<Input type="text" placeholder="Name" value="Alice" />

// Rest: collect remaining props
function Button({ label, ...rest }) {
  return <button {...rest}>{label}</button>;
}

// Selective spreading
function Card({ title, children, ...divProps }) {
  return (
    <div {...divProps}>
      <h2>{title}</h2>
      {children}
    </div>
  );
}

Prop Types y TypeScript

Las interfaces de TypeScript proporcionan verificación de tipos en tiempo de compilación para las props — el enfoque recomendado para nuevos proyectos de React. Las props opcionales usan ? (isActive?: boolean). PropTypes proporcionan validación en tiempo de ejecución (solo en desarrollo) y son útiles para proyectos de JavaScript sin TypeScript. isRequired asegura que la prop sea proporcionada. TypeScript detecta errores de tipo antes de la ejecución, haciéndolo superior para codebases grandes.

react
// TypeScript interface (recommended)
interface UserProps {
  name: string;
  age: number;
  isActive?: boolean;       // optional
  onClick: (id: number) => void;
}

function User({ name, age, isActive = true, onClick }: UserProps) {
  return <div onClick={() => onClick(1)}>{name}, {age}</div>;
}

// PropTypes (runtime checking, legacy)
import PropTypes from 'prop-types';
User.propTypes = {
  name: PropTypes.string.isRequired,
  age: PropTypes.number,
  isActive: PropTypes.bool,
};

Prop Drilling y Context

El prop drilling ocurre cuando las props pasan por múltiples capas de componentes que no las usan. Para 2-3 niveles, es aceptable. Para árboles más profundos, usa Context API, librerías de gestión de estado (Redux, Zustand), o composición de componentes. La composición (pasar componentes como props o children) a menudo resuelve el drilling de manera más elegante que Context. Pregúntate: ¿cada componente intermedio necesita estos datos? Si no, reconsidera tu estructura de componentes.

react
// Prop drilling: passing through multiple levels
function App() {
  const [user, setUser] = useState(null);
  return <Layout user={user} />;
}

function Layout({ user }) {
  return <Sidebar user={user} />;
}

function Sidebar({ user }) {
  return <UserInfo user={user} />;
}

// Solution: Context API (see State Management section)
// Avoid drilling more than 2-3 levels
03

useState y Estado

useState Básico

useState es el hook fundamental para añadir estado a los componentes función. Devuelve un array: [currentValue, setterFunction]. El valor inicial puede ser de cualquier tipo. Llamar al setter dispara un re-render con el nuevo valor. Las actualizaciones de estado son asíncronas — el valor no cambia inmediatamente después de llamar a setCount. Cada instancia de componente tiene su propio estado independiente. El setter es estable (misma referencia entre renders).

react
import { useState } from 'react';

function Counter() {
  // [currentValue, setterFunction] = useState(initialValue)
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>+1</button>
      <button onClick={() => setCount(count - 1)}>-1</button>
      <button onClick={() => setCount(0)}>Reset</button>
    </div>
  );
}

Actualizaciones Funcionales

Cuando el nuevo estado depende del estado anterior, usa una actualización funcional: setCount(prev => prev + 1). Esto garantiza que estás trabajando con el último estado, incluso si múltiples actualizaciones se agrupan (batch). Sin actualizaciones funcionales, llamadas sucesivas rápidas pueden usar estado obsoleto. React 18 agrupa automáticamente las actualizaciones de estado (incluso en promesas y timeouts), por lo que las actualizaciones funcionales son esenciales para la corrección.

react
function Counter() {
  const [count, setCount] = useState(0);

  // BAD: may not work correctly with rapid updates
  const incrementBad = () => setCount(count + 1);

  // GOOD: functional update uses previous state
  const incrementGood = () => setCount(prev => prev + 1);

  // Batch updates
  const addThree = () => {
    setCount(prev => prev + 1);
    setCount(prev => prev + 1);
    setCount(prev => prev + 1);
  };

  return <button onClick={addThree}>Count: {count}</button>;
}

Estado con Objetos y Arrays

Nunca muttes el estado directamente — siempre crea un nuevo objeto/array. Para objetos, usa el operador spread para copiar las propiedades existentes: {...prev, [field]: value}. Para arrays, usa spread para añadir ([...prev, newItem]), filter para eliminar, y map para actualizar. React compara referencias para detectar cambios — los objetos mutados tienen la misma referencia, por lo que React no hará re-render. Esta es la fuente #1 de errores de React para principiantes.

react
function Form() {
  const [form, setForm] = useState({ name: '', email: '', age: 0 });

  // BAD: mutates state directly
  // form.name = 'Alice'; setForm(form);

  // GOOD: spread to create new object
  const updateField = (field, value) => {
    setForm(prev => ({ ...prev, [field]: value }));
  };

  return (
    <input
      value={form.name}
      onChange={e => updateField('name', e.target.value)}
    />
  );
}

// Array state
function TodoList() {
  const [todos, setTodos] = useState([]);
  const addTodo = (text) => setTodos(prev => [...prev, { id: Date.now(), text }]);
  const removeTodo = (id) => setTodos(prev => prev.filter(t => t.id !== id));
}

Estado Inicial Lazy

Si el estado inicial requiere un cálculo costoso, pasa una función a useState (inicialización lazy). La función se ejecuta solo en el primer render, no en cada re-render. Esto es importante para parsear localStorage, obtener datos de IndexedDB, o cualquier configuración intensiva de CPU. La forma de función: useState(() => initialValue). Para valores simples (números, strings), simplemente pasa el valor directamente — la inicialización lazy es innecesaria.

react
import { useState } from 'react';

function ExpensiveInit() {
  // BAD: runs on every render (even though result is ignored)
  const [data, setData] = useState(computeExpensiveValue());

  // GOOD: lazy initialization — function runs only once
  const [data2, setData2] = useState(() => computeExpensiveValue());

  // Reading from localStorage
  const [user, setUser] = useState(() => {
    const saved = localStorage.getItem('user');
    return saved ? JSON.parse(saved) : null;
  });

  return <div>{data2}</div>;
}

Múltiples Variables de Estado

Usa múltiples llamadas a useState para valores independientes en lugar de un gran objeto. Esto hace las actualizaciones más simples (sin necesidad de spread) y previene re-renders innecesarios. Agrupa valores relacionados en un único objeto de estado (p. ej., campos de formulario). Para lógica de estado compleja con múltiples sub-valores, considera useReducer en su lugar. Regla general: si las actualizaciones de estado son independientes, usa useState separados; si están relacionadas/interdependientes, usa useReducer o un único objeto.

react
function LoginForm() {
  // Multiple independent state variables
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [errors, setErrors] = useState({});
  const [isSubmitting, setIsSubmitting] = useState(false);
  const [rememberMe, setRememberMe] = useState(false);

  const handleSubmit = (e) => {
    e.preventDefault();
    setIsSubmitting(true);
    // ... validation and submission
  };

  return (
    <form onSubmit={handleSubmit}>
      <input value={email} onChange={e => setEmail(e.target.value)} />
      <input type="password" value={password}
        onChange={e => setPassword(e.target.value)} />
      <button disabled={isSubmitting}>Submit</button>
    </form>
  );
}
04

useEffect y Efectos Secundarios

useEffect Básico

useEffect realiza efectos secundarios después del render. La función del efecto se ejecuta después de que el componente se pinte. La función de limpieza (devuelta) se ejecuta antes del siguiente efecto y al desmontar — esencial para limpiar timers, suscripciones y listeners. El array de dependencias controla cuándo se vuelve a ejecutar el efecto: [] = una vez al montar, [dep] = cuando dep cambia, sin array = en cada render. Siempre limpia para prevenir memory leaks.

react
import { useState, useEffect } from 'react';

function Timer() {
  const [seconds, setSeconds] = useState(0);

  // Runs after every render
  useEffect(() => {
    const interval = setInterval(() => {
      setSeconds(s => s + 1);
    }, 1000);

    // Cleanup function runs before next effect or unmount
    return () => clearInterval(interval);
  }, []); // empty array = run once on mount

  return <p>Seconds: {seconds}</p>;
}

Array de Dependencias

El array de dependencias es crítico para el comportamiento de useEffect. Array vacío [] = solo al montar (como componentDidMount). Con dependencias [a, b] = se ejecuta al montar y cuando a o b cambia. Sin array = en cada render (raramente lo que quieres). Las dependencias faltantes causan closures obsoletos. Incluir dependencias innecesarias causa re-ejecuciones excesivas. Usa la regla exhaustive-deps de ESLint para detectar errores. Cada valor del scope del componente usado en el efecto debería estar en las deps.

react
function UserProfile({ userId }) {
  const [user, setUser] = useState(null);

  // Runs once on mount (empty deps)
  useEffect(() => {
    console.log('Component mounted');
  }, []);

  // Runs when userId changes
  useEffect(() => {
    fetch('/api/users/' + userId)
      .then(r => r.json())
      .then(setUser);
  }, [userId]); // re-run when userId changes

  // Runs on every render (no deps) - rarely needed
  useEffect(() => {
    console.log('Every render');
  });

  return <div>{user?.name}</div>;
}

Limpieza y Suscripciones

La limpieza es esencial para suscripciones, event listeners, timers y conexiones WebSocket. Sin limpieza, obtienes memory leaks y handlers duplicados. La función de limpieza se ejecuta: (1) antes de la siguiente re-ejecución del efecto, (2) al desmontar el componente. Para WebSocket/event listeners, siempre elimínalos en la limpieza. Para el estado que depende del efecto, reinícialo en la limpieza para evitar mostrar datos obsoletos de un roomId anterior.

react
function ChatRoom({ roomId }) {
  const [messages, setMessages] = useState([]);

  useEffect(() => {
    const ws = new WebSocket('wss://chat.example.com/' + roomId);

    ws.onmessage = (event) => {
      setMessages(prev => [...prev, JSON.parse(event.data)]);
    };

    // Cleanup: close connection when roomId changes or unmount
    return () => {
      ws.close();
      setMessages([]); // reset for new room
    };
  }, [roomId]);

  // Window event listener
  useEffect(() => {
    const handleResize = () => console.log(window.innerWidth);
    window.addEventListener('resize', handleResize);
    return () => window.removeEventListener('resize', handleResize);
  }, []);

  return <div>{messages.length} messages</div>;
}

Obtención de Datos

La obtención de datos en useEffect requiere un flag de cancelación para prevenir establecer estado después del desmontaje (causa warnings de React). El flag 'cancelled' asegura que setUsers/setError/setLoading solo se ejecuten si el componente sigue montado. Para apps de producción, considera usar una librería de obtención de datos (React Query, SWR) que maneja caching, deduplicación y race conditions automáticamente. El array de dependencias vacío [] asegura que la obtención ocurra una vez al montar.

react
function UserList() {
  const [users, setUsers] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    let cancelled = false;

    const fetchData = async () => {
      try {
        setLoading(true);
        const res = await fetch('/api/users');
        const data = await res.json();
        if (!cancelled) setUsers(data);
      } catch (err) {
        if (!cancelled) setError(err.message);
      } finally {
        if (!cancelled) setLoading(false);
      }
    };

    fetchData();
    return () => { cancelled = true; };
  }, []);

  if (loading) return <p>Loading...</p>;
  if (error) return <p>Error: {error}</p>;
  return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
}

useLayoutEffect vs useEffect

useEffect se ejecuta asíncronamente después de que el navegador pinta — los usuarios pueden ver un parpadeo breve si estás midiendo elementos DOM. useLayoutEffect se ejecuta sincrónicamente después de las mutaciones del DOM pero antes del pintado — previniendo parpadeo visual. Usa useLayoutEffect para mediciones DOM (getBoundingClientRect, scroll position) que afectan el layout. Usa useEffect para todo lo demás (no bloquea el pintado). En el servidor, useLayoutEffect lanza un warning — usa el patrón useIsomorphicLayoutEffect.

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

function Tooltip({ text }) {
  const [position, setPosition] = useState({ x: 0, y: 0 });

  // useEffect: runs AFTER paint (user may see flash)
  useEffect(() => {
    const rect = document.getElementById('tip').getBoundingClientRect();
    setPosition({ x: rect.x, y: rect.y });
  }, [text]);

  // useLayoutEffect: runs BEFORE paint (no flash)
  useLayoutEffect(() => {
    const rect = document.getElementById('tip').getBoundingClientRect();
    setPosition({ x: rect.x, y: rect.y });
  }, [text]);

  return <div id="tip" style={{ left: position.x, top: position.y }}>{text}</div>;
}
05

useRef, useMemo y useCallback

Fundamentos de useRef

useRef devuelve un objeto mutable { current: value } que persiste entre renders. A diferencia del estado, cambiar ref.current NO dispara un re-render. Usos comunes: (1) acceder a elementos DOM (vía atributo ref), (2) almacenar valores mutables que no afectan el renderizado (timers, valores previos), (3) almacenar el último valor para usar en callbacks. El objeto ref tiene la misma identidad entre renders. El valor inicial se pasa a useRef(initialValue).

react
import { useRef } from 'react';

function FocusInput() {
  // ref to access DOM element
  const inputRef = useRef(null);

  const focus = () => inputRef.current.focus();
  const clear = () => {
    inputRef.current.value = '';
    inputRef.current.focus();
  };

  return (
    <div>
      <input ref={inputRef} type="text" />
      <button onClick={focus}>Focus</button>
      <button onClick={clear}>Clear</button>
    </div>
  );
}

useRef para Valores Mutables

useRef almacena valores mutables que persisten entre renders sin disparar re-renders. Esto es perfecto para: IDs de timers, referencias WebSocket, seguimiento del estado previo, y conteo de renders. Dado que cambiar ref.current no causa un re-render, la UI no se actualizará cuando lo cambies — usa estado para valores que deberían afectar la UI. El patrón de conteo de renders (ref.current++) es útil para debugging pero no debería usarse en lógica de producción.

react
function Stopwatch() {
  const [seconds, setSeconds] = useState(0);
  const intervalRef = useRef(null);
  const renderCount = useRef(0);

  // Track render count (doesn't trigger re-render)
  renderCount.current++;

  const start = () => {
    if (intervalRef.current) return;
    intervalRef.current = setInterval(() => {
      setSeconds(s => s + 1);
    }, 1000);
  };

  const stop = () => {
    clearInterval(intervalRef.current);
    intervalRef.current = null;
  };

  return (
    <div>
      <p>{seconds}s (render #{renderCount.current})</p>
      <button onClick={start}>Start</button>
      <button onClick={stop}>Stop</button>
    </div>
  );
}

useMemo

useMemo memoiza (cachea) un valor calculado, recalculando solo cuando cambian las dependencias. Úsalo para cálculos costosos (filtrado, ordenamiento, matemática compleja) para evitar re-ejecutar en cada render. El array de dependencias funciona como en useEffect. El uso excesivo puede perjudicar el rendimiento (la memoización tiene su propio overhead) — solo memoiza operaciones genuinamente costosas. useMemo también es útil para preservar referencias de objetos y prevenir re-renders hijos.

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

function ProductList({ products, filter }) {
  const [search, setSearch] = useState('');

  // Memoize expensive computation
  const filtered = useMemo(() => {
    console.log('Filtering...');
    return products
      .filter(p => p.category === filter)
      .filter(p => p.name.includes(search));
  }, [products, filter, search]); // recompute only when these change

  // Memoize a value
  const totalPrice = useMemo(() =>
    filtered.reduce((sum, p) => sum + p.price, 0),
  [filtered]);

  return (
    <div>
      <input value={search} onChange={e => setSearch(e.target.value)} />
      <p>Total: ${totalPrice}</p>
      {filtered.map(p => <div key={p.id}>{p.name}</div>)}
    </div>
  );
}

useCallback

useCallback memoiza una función, devolviendo la misma referencia entre renders a menos que cambien las dependencias. Esto previene re-renders innecesarios de componentes hijos memoizados (envueltos en memo()). Sin useCallback, cada render del padre crea una nueva referencia de función, causando que los hijos de memo() se re-rendericen. Usa useCallback cuando pases callbacks a componentes hijos optimizados. Como useMemo, no lo uses en exceso — solo para funciones pasadas como props a hijos memoizados.

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

// Memoized child component
const Button = memo(function Button({ onClick, label }) {
  console.log('Button rendered');
  return <button onClick={onClick}>{label}</button>;
});

function App() {
  const [count, setCount] = useState(0);
  const [text, setText] = useState('');

  // Without useCallback: new function every render
  // const handleClick = () => setCount(c => c + 1);

  // With useCallback: stable function reference
  const handleClick = useCallback(() => {
    setCount(c => c + 1);
  }, []); // empty deps = stable forever

  return (
    <div>
      <input value={text} onChange={e => setText(e.target.value)} />
      <Button onClick={handleClick} label="Click" />
      <p>Count: {count}</p>
    </div>
  );
}

Reenvío de Refs

forwardRef permite a los componentes padres pasar un ref a un elemento DOM de un componente hijo. useImperativeHandle personaliza lo que el ref expone — en lugar del nodo DOM, puedes exponer métodos específicos (focus, clear, getValue). Esto es útil para crear componentes de input reutilizables con APIs imperativas. React 19 simplificó los refs (ref ahora es una prop regular), pero forwardRef sigue siendo necesario para librerías. Evita el uso excesivo de handles imperativos — prefiere props declarativas.

react
import { useRef, forwardRef, useImperativeHandle } from 'react';

// forwardRef: pass ref to child component
const FancyInput = forwardRef(function FancyInput(props, ref) {
  return <input ref={ref} className="fancy" {...props} />;
});

// useImperativeHandle: expose specific methods
const CustomInput = forwardRef(function CustomInput(props, ref) {
  const inputRef = useRef(null);

  useImperativeHandle(ref, () => ({
    focus: () => inputRef.current.focus(),
    clear: () => { inputRef.current.value = ''; },
    getValue: () => inputRef.current.value,
  }));

  return <input ref={inputRef} />;
});

// Usage
function App() {
  const ref = useRef(null);
  return (
    <>
      <CustomInput ref={ref} />
      <button onClick={() => ref.current.focus()}>Focus</button>
      <button onClick={() => ref.current.clear()}>Clear</button>
    </>
  );
}
06

useReducer y Context

Fundamentos de useReducer

useReducer es una alternativa a useState para lógica de estado compleja. Un reducer es una función pura: (state, action) => newState. Las actions describen qué ocurrió; el reducer decide cómo actualizar el estado. Este patrón hace las transiciones de estado predecibles y testeables. Dispatch es estable (misma referencia). Siempre devuelve un nuevo objeto de estado (nunca muttes). El caso default debería lanzar un error para actions desconocidas. Usa useReducer cuando el estado tiene múltiples sub-valores o el siguiente estado depende de lógica compleja.

react
import { useReducer } from 'react';

// Reducer function: (state, action) => newState
function counterReducer(state, action) {
  switch (action.type) {
    case 'increment':
      return { count: state.count + 1 };
    case 'decrement':
      return { count: state.count - 1 };
    case 'reset':
      return { count: 0 };
    case 'set':
      return { count: action.payload };
    default:
      throw new Error('Unknown action: ' + action.type);
  }
}

function Counter() {
  const [state, dispatch] = useReducer(counterReducer, { count: 0 });

  return (
    <div>
      <p>Count: {state.count}</p>
      <button onClick={() => dispatch({ type: 'increment' })}>+</button>
      <button onClick={() => dispatch({ type: 'decrement' })}>-</button>
      <button onClick={() => dispatch({ type: 'reset' })}>Reset</button>
      <button onClick={() => dispatch({ type: 'set', payload: 10 })}>Set 10</button>
    </div>
  );
}

Reducer Complejo

Los reducers complejos gestionan múltiples piezas relacionadas de estado. Cada tipo de action maneja una transición de estado específica. Siempre usa spread del estado previo ({...state}) para preservar los campos no relacionados. Para actualizaciones anidadas (como togglear un todo), usa map para crear un nuevo array con el item actualizado. Los reducers deben ser puros — sin efectos secundarios, sin llamadas a API. Extrae el reducer a un archivo separado para testeabilidad. Considera librerías como Redux Toolkit para estado muy complejo.

react
const initialState = {
  todos: [],
  filter: 'all',
  loading: false,
};

function todoReducer(state, action) {
  switch (action.type) {
    case 'add':
      return { ...state, todos: [...state.todos, action.todo] };
    case 'toggle':
      return {
        ...state,
        todos: state.todos.map(t =>
          t.id === action.id ? { ...t, done: !t.done } : t
        ),
      };
    case 'delete':
      return { ...state, todos: state.todos.filter(t => t.id !== action.id) };
    case 'set_filter':
      return { ...state, filter: action.filter };
    case 'set_loading':
      return { ...state, loading: action.loading };
    default:
      return state;
  }
}

function TodoApp() {
  const [state, dispatch] = useReducer(todoReducer, initialState);
  // dispatch({ type: 'add', todo: { id: 1, text: 'Learn React', done: false } })
}

Context API

Context API comparte estado a través del árbol de componentes sin prop drilling. Crea con createContext(defaultValue). Envuelve los consumidores en Provider con una prop value. Consume con useContext(Context). Los cambios de valor del Context disparan re-renders de todos los consumidores. Para rendimiento, divide los contextos (ThemeContext, UserContext) para que los componentes solo se re-rendericen cuando su contexto específico cambia. El valor por defecto se usa cuando ningún Provider envuelve al consumidor.

react
import { createContext, useContext, useState } from 'react';

// 1. Create context with default value
const ThemeContext = createContext('light');
const UserContext = createContext(null);

// 2. Provider component
function App() {
  const [theme, setTheme] = useState('light');
  const [user, setUser] = useState(null);

  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      <UserContext.Provider value={{ user, setUser }}>
        <Page />
      </UserContext.Provider>
    </ThemeContext.Provider>
  );
}

// 3. Consume context
function Page() {
  const { theme } = useContext(ThemeContext);
  const { user } = useContext(UserContext);
  return <div className={'page theme-' + theme}>Hello {user?.name}</div>;
}

Context con Reducer

Combinar Context con useReducer crea un sistema ligero de gestión de estado global (mini Redux). El Provider expone tanto el estado como dispatch. Un hook personalizado (useStore) proporciona manejo de errores si se usa fuera del provider. Este patrón es genial para apps medianas. Para apps muy grandes con actualizaciones frecuentes, considera dividir contextos o usar Redux/Zustand para evitar re-renderizar todos los consumidores en cada cambio de estado.

react
import { createContext, useContext, useReducer } from 'react';

// Combine Context + useReducer for global state
const StoreContext = createContext(null);

function StoreProvider({ children }) {
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <StoreContext.Provider value={{ state, dispatch }}>
      {children}
    </StoreContext.Provider>
  );
}

// Custom hook for easy consumption
function useStore() {
  const context = useContext(StoreContext);
  if (!context) throw new Error('useStore must be used within StoreProvider');
  return context;
}

// Usage
function Component() {
  const { state, dispatch } = useStore();
  return <button onClick={() => dispatch({ type: 'action' })}>Click</button>;
}

Rendimiento de useContext

Cuando el valor del contexto cambia, TODOS los consumidores se re-renderizan — incluso si solo usan una pequeña parte del valor. Para optimizar: (1) Divide los contextos para que los componentes solo se suscriban a lo que necesitan. (2) Memoiza el valor del contexto con useMemo para prevenir re-renders cuando el valor no ha cambiado realmente. (3) Usa selectores (librería use-context-selector) para suscripciones finas. Para actualizaciones de alta frecuencia (como posición del mouse), Context puede causar problemas de rendimiento — considera refs o stores externos.

react
// SPLIT contexts for performance
const ThemeContext = createContext();
const UserContext = createContext();
const CartContext = createContext();

// Each provider manages its own state
function App() {
  return (
    <ThemeProvider>
      <UserProvider>
        <CartProvider>
          <App />
        </CartProvider>
      </UserProvider>
    </ThemeProvider>
  );
}

// Component only re-renders when its context changes
function ThemedButton() {
  const { theme } = useContext(ThemeContext);
  // Won't re-render when user or cart changes
  return <button className={theme}>Button</button>;
}

// Memoize context value to prevent unnecessary re-renders
function UserProvider({ children }) {
  const [user, setUser] = useState(null);
  const value = useMemo(() => ({ user, setUser }), [user]);
  return <UserContext.Provider value={value}>{children}</UserContext.Provider>;
}
07

Eventos y Formularios

Manejo de Eventos

Los eventos de React usan camelCase (onClick, no onclick). El objeto del evento es un SyntheticEvent (wrapper del evento nativo). e.preventDefault() detiene el comportamiento por defecto (submit de formulario, navegación de enlace). e.stopPropagation() previene el event bubbling. Para pasar parámetros a los handlers, usa arrow functions: onClick={() => handleDelete(id)}. Evita definir handlers complejos inline — extraelos para legibilidad. Los eventos de React son pooled (pre-17), así que llama e.persist() si necesitas acceso asíncrono.

react
function App() {
  // Click event
  const handleClick = (e) => {
    e.preventDefault();
    console.log('Button clicked', e.target);
  };

  // With parameters (use arrow function)
  const handleDelete = (id) => {
    console.log('Delete item', id);
  };

  return (
    <div>
      <button onClick={handleClick}>Click</button>
      <button onClick={() => handleDelete(42)}>Delete</button>
      <div onMouseEnter={() => console.log('hover')}
           onMouseLeave={() => console.log('leave')}>
        Hover me
      </div>
    </div>
  );
}

Inputs Controlados

Los inputs controlados tienen su valor controlado por el estado de React. La prop value establece el valor del input, y onChange actualiza el estado. Esto hace de React la 'single source of truth' para los datos del formulario. Cada pulsación de tecla dispara una actualización de estado y re-render. Para formularios complejos, esto puede ser verboso — considera librerías como React Hook Form o Formik. Los inputs controlados permiten validación en tiempo real y comportamiento dinámico. Siempre usa onChange con value (o readOnly) para evitar warnings de React.

react
function LoginForm() {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');

  const handleSubmit = (e) => {
    e.preventDefault();
    console.log('Email:', email, 'Password:', password);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="email"
        value={email}
        onChange={e => setEmail(e.target.value)}
        placeholder="Email"
      />
      <input
        type="password"
        value={password}
        onChange={e => setPassword(e.target.value)}
        placeholder="Password"
      />
      <button type="submit">Login</button>
    </form>
  );
}

Formulario con Múltiples Campos

Para formularios con muchos campos, usa un único objeto de estado y una función genérica handleChange. El atributo name en cada input coincide con la key del estado. El handler usa computed property names ([name]: value) para actualizar el campo correcto. Para checkboxes, usa checked en lugar de value. Este patrón reduce significativamente el boilerplate. Para inputs de archivo, usa inputs no controlados (no pueden ser completamente controlados). Considera React Hook Form para formularios complejos con validación.

react
function RegistrationForm() {
  const [formData, setFormData] = useState({
    username: '',
    email: '',
    password: '',
    country: 'us',
    agree: false,
  });

  const handleChange = (e) => {
    const { name, value, type, checked } = e.target;
    setFormData(prev => ({
      ...prev,
      [name]: type === 'checkbox' ? checked : value,
    }));
  };

  const handleSubmit = (e) => {
    e.preventDefault();
    console.log(formData);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input name="username" value={formData.username} onChange={handleChange} />
      <input name="email" type="email" value={formData.email} onChange={handleChange} />
      <select name="country" value={formData.country} onChange={handleChange}>
        <option value="us">USA</option>
        <option value="uk">UK</option>
      </select>
      <label>
        <input type="checkbox" name="agree" checked={formData.agree} onChange={handleChange} />
        Agree to terms
      </label>
      <button type="submit">Register</button>
    </form>
  );
}

Inputs No Controlados

Los inputs no controlados usan refs para acceder al valor del DOM directamente, sin estado de React. La prop defaultValue establece el valor inicial (no value). Esto es más simple para formularios que no necesitan validación en tiempo real o comportamiento dinámico. Los inputs de archivo deben ser no controlados (su valor es read-only por seguridad). Los inputs no controlados también son útiles para integrar con código no-React. El tradeoff: no puedes validar o transformar el input fácilmente en tiempo real. Prefiere inputs controlados para la mayoría de los casos.

react
import { useRef } from 'react';

function UncontrolledForm() {
  const emailRef = useRef(null);
  const passwordRef = useRef(null);

  const handleSubmit = (e) => {
    e.preventDefault();
    console.log('Email:', emailRef.current.value);
    console.log('Password:', passwordRef.current.value);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input ref={emailRef} type="email" defaultValue="" />
      <input ref={passwordRef} type="password" defaultValue="" />
      <button type="submit">Submit</button>
    </form>
  );
}

// File input (must be uncontrolled)
function FileUpload() {
  const fileRef = useRef(null);
  return <input ref={fileRef} type="file" />;
}

Validación y Manejo de Errores

La validación de formularios puede hacerse al submit o en cada cambio. La función validate devuelve un objeto de errores — vacío significa válido. Muestra los errores condicionalmente junto a cada campo. Para mejor UX, valida en blur (después de que el usuario abandone el campo) en lugar de en cada pulsación de tecla. Librerías como React Hook Form + Zod, o Formik + Yup, proporcionan schemas de validación robustos, gestión de errores y seguimiento de touch/blur. Siempre valida también en el servidor — la validación del cliente es para UX, no para seguridad.

react
function ValidatedForm() {
  const [values, setValues] = useState({ email: '', password: '' });
  const [errors, setErrors] = useState({});

  const validate = () => {
    const errs = {};
    if (!values.email) errs.email = 'Email is required';
    else if (!/\S+@\S+\.\S+/.test(values.email)) errs.email = 'Invalid email';
    if (!values.password) errs.password = 'Password is required';
    else if (values.password.length < 8) errs.password = 'Min 8 characters';
    return errs;
  };

  const handleSubmit = (e) => {
    e.preventDefault();
    const errs = validate();
    setErrors(errs);
    if (Object.keys(errs).length === 0) {
      console.log('Form valid', values);
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <input value={values.email}
        onChange={e => setValues(v => ({ ...v, email: e.target.value }))} />
      {errors.email && <span className="error">{errors.email}</span>}
      <input type="password" value={values.password}
        onChange={e => setValues(v => ({ ...v, password: e.target.value }))} />
      {errors.password && <span className="error">{errors.password}</span>}
      <button type="submit">Submit</button>
    </form>
  );
}
08

Listas y Renderizado Condicional

Renderizado de Listas

Usa .map() para transformar arrays en elementos JSX. Cada elemento necesita una prop key única — usa IDs estables (todo.id), no índices de array. Las keys ayudan a React a identificar qué items cambian (añadidos, eliminados, reordenados) para actualizaciones DOM eficientes. Usar el índice como key causa errores cuando los items de la lista se reordenan o se insertan al principio. Para listas vacías, renderiza un mensaje de fallback. Considera useMemo para listas filtradas/ordenadas para evitar recálculo en cada render.

react
function TodoList({ todos }) {
  return (
    <ul>
      {todos.map(todo => (
        <li key={todo.id}>
          <span>{todo.text}</span>
          <button onClick={() => toggle(todo.id)}>
            {todo.done ? 'Undo' : 'Done'}
          </button>
        </li>
      ))}
    </ul>
  );
}

// Filtering and sorting
function FilteredList({ items, filter }) {
  const visible = items
    .filter(item => item.category === filter)
    .sort((a, b) => a.name.localeCompare(b.name));

  return (
    <ul>
      {visible.map(item => <li key={item.id}>{item.name}</li>)}
    </ul>
  );
}

Keys Explicadas

Las keys deben ser únicas entre hermanos (mismo padre), pero pueden repetirse entre listas diferentes. Las keys ayudan al algoritmo de reconciliation de React: cuando una key cambia, React destruye y recrea el componente (perdiendo estado). Con keys de índice, insertar un item al principio desplaza todos los índices, causando que React re-renderice todo. Con keys de ID estables, React solo renderiza el nuevo item. Las keys no necesitan ser globalmente únicas — solo únicas dentro de la lista. No uses keys aleatorias (Math.random()) — cambian en cada render.

react
// GOOD: stable, unique keys
{todos.map(todo => (
  <TodoItem key={todo.id} todo={todo} />
))}

// BAD: index as key (causes bugs with reordering)
{todos.map((todo, index) => (
  <TodoItem key={index} todo={todo} />
))}

// When index keys are OK:
// - Static list (never reordered/filtered)
// - List items have no state
// - List is never prepended to

// Key must be unique among siblings
function List() {
  return (
    <div>
      {users.map(u => <User key={u.id} user={u} />)}
      {posts.map(p => <Post key={p.id} post={p} />)}
      {/* IDs can repeat across different lists */}
    </div>
  );
}

Patrones de Renderizado Condicional

Existen múltiples patrones de renderizado condicional. Los retornos tempranos son los más limpios para guard clauses (loading, error, auth). Las variables de elemento funcionan para if/else en medio del componente. Ternario (cond ? A : B) para uno/otro en JSX. AND lógico (cond && <X/>) para mostrar/ocultar. Búsqueda en objeto para comportamiento tipo switch. Evita ternarios anidados — extrae a variables o componentes. Para números, usa ternario en lugar de && (0 && <X/> renderiza 0).

react
function UserDashboard({ user, loading, error }) {
  // 1. Early returns for loading/error states
  if (loading) return <Spinner />;
  if (error) return <ErrorMessage error={error} />;
  if (!user) return <Login />;

  // 2. Element variables
  let greeting;
  if (user.isAdmin) {
    greeting = <h1>Welcome Admin {user.name}</h1>;
  } else {
    greeting = <h1>Welcome {user.name}</h1>;
  }

  return (
    <div>
      {greeting}

      {/* 3. Ternary for either/or */}
      {user.hasNotifications ? <NotificationBadge /> : null}

      {/* 4. && for show/hide */}
      {user.isAdmin && <AdminPanel />}

      {/* 5. Switch via object lookup */}
      {{ free: <FreePlan />, pro: <ProPlan />, enterprise: <EnterprisePlan /> }
        [user.plan]}
    </div>
  );
}

Filtrado y Búsqueda en Listas

Las listas buscables/filtrables combinan useState para filtros con useMemo para rendimiento. La función filter comprueba tanto la consulta como la categoría. Siempre maneja el estado vacío (sin resultados). Para listas grandes (1000+ items), considera virtualización (react-window, react-virtualized) para renderizar solo los items visibles. Aplica debounce al input de búsqueda para llamadas a API. La búsqueda case-insensitive usa toLowerCase(). Para filtrado complejo, extrae a una función separada o hook personalizado.

react
function SearchableList({ items }) {
  const [query, setQuery] = useState('');
  const [category, setCategory] = useState('all');

  // Memoize filtered results
  const filtered = useMemo(() => {
    return items.filter(item => {
      const matchesQuery = item.name.toLowerCase().includes(query.toLowerCase());
      const matchesCategory = category === 'all' || item.category === category;
      return matchesQuery && matchesCategory;
    });
  }, [items, query, category]);

  return (
    <div>
      <input
        value={query}
        onChange={e => setQuery(e.target.value)}
        placeholder="Search..."
      />
      <select value={category} onChange={e => setCategory(e.target.value)}>
        <option value="all">All</option>
        <option value="food">Food</option>
        <option value="tech">Tech</option>
      </select>

      {filtered.length === 0 ? (
        <p>No results found</p>
      ) : (
        <ul>
          {filtered.map(item => <li key={item.id}>{item.name}</li>)}
        </ul>
      )}
    </div>
  );
}

Componentes Dinámicos

El renderizado dinámico de componentes usa un objeto de búsqueda para mapear tipos a componentes. Esto es común en contenido impulsado por CMS, constructores de formularios y constructores de páginas. El mapa de componentes evita largas cadenas switch/if. Siempre maneja los tipos desconocidos con un componente de fallback. Usa spread props ({...block.props}) para pasar todas las propiedades al componente dinámico. Este patrón es flexible y extensible — añadir un nuevo tipo de bloque solo requiere añadirlo al mapa. Capitaliza la variable (Component) para que JSX la trate como un componente.

react
// Render different components based on type
const componentMap = {
  text: TextBlock,
  image: ImageBlock,
  video: VideoBlock,
  quote: QuoteBlock,
};

function ContentRenderer({ blocks }) {
  return (
    <div>
      {blocks.map(block => {
        const Component = componentMap[block.type];
        if (!Component) return <UnknownBlock key={block.id} type={block.type} />;

        return <Component key={block.id} {...block.props} />;
      })}
    </div>
  );
}

// Usage
const blocks = [
  { id: 1, type: 'text', props: { content: 'Hello' } },
  { id: 2, type: 'image', props: { src: 'pic.jpg', alt: 'Picture' } },
];
09

Optimización de Rendimiento

React.memo

React.memo envuelve un componente para prevenir re-renders cuando las props no han cambiado (comparación superficial). Úsalo para componentes que se renderizan a menudo con las mismas props. El segundo argumento es una función de comparación personalizada: devuelve true para saltar el re-render, false para re-renderizar. memo solo ayuda si el componente es costoso de renderizar o es hijo de un padre que se renderiza frecuentemente. No envuelvas cada componente — la memoización tiene overhead. Combina con useCallback/useMemo para máximo efecto.

react
import { memo } from 'react';

// Memoized component: only re-renders if props change
const ExpensiveCard = memo(function ExpensiveCard({ title, content }) {
  console.log('Card rendered');
  return (
    <div className="card">
      <h2>{title}</h2>
      <p>{content}</p>
    </div>
  );
});

// Custom comparison function
const MyComponent = memo(function MyComponent(props) {
  return <div>{props.value}</div>;
}, (prevProps, nextProps) => {
  // Return true if props are equal (skip re-render)
  return prevProps.value === nextProps.value;
});

Code Splitting

Code splitting reduce el tamaño inicial del bundle cargando componentes bajo demanda. React.lazy + Suspense permite imports dinámicos. La prop fallback se muestra mientras el componente carga. El splitting basado en rutas (cargar componentes de página lazy) es el más impactante. El splitting basado en componentes es útil para componentes pesados (charts, editores) que no se necesitan inmediatamente. Cada import lazy crea un chunk separado. Usa React.lazy para default exports; para named exports, envuelve en un módulo.

react
import { lazy, Suspense } from 'react';

// Lazy load component (code-split)
const AdminPanel = lazy(() => import('./AdminPanel'));
const Dashboard = lazy(() => import('./Dashboard'));

function App() {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <Router>
        <Route path="/admin" element={<AdminPanel />} />
        <Route path="/dashboard" element={<Dashboard />} />
      </Router>
    </Suspense>
  );
}

// Route-based splitting (most common)
// Component-based splitting
const HeavyChart = lazy(() => import('./HeavyChart'));

function Page({ showChart }) {
  return (
    <div>
      {showChart && (
        <Suspense fallback={<Spinner />}>
          <HeavyChart />
        </Suspense>
      )}
    </div>
  );
}

Virtualización para Listas Largas

La virtualización renderiza solo los items visibles en una lista larga, mejorando drásticamente el rendimiento. react-window y react-virtualized son librerías populares. En lugar de renderizar 10,000 nodos DOM, solo se renderizan ~12 (los visibles), con un contenedor scrollable. Esto reduce el tamaño del DOM y el tiempo de render. Usa virtualización para listas con 100+ items. El tradeoff: implementación más compleja, problemas potenciales con search/find (items no en el DOM). Las listas de altura variable necesitan VariableSizeList.

react
import { FixedSizeList } from 'react-window';

// Virtualized list: only renders visible items
function BigList({ items }) {
  const Row = ({ index, style }) => (
    <div style={style}>
      {items[index].name}
    </div>
  );

  return (
    <FixedSizeList
      height={600}
      width="100%"
      itemCount={items.length}
      itemSize={50}
    >
      {Row}
    </FixedSizeList>
  );
}

// Without virtualization: rendering 10,000 items is slow
// With virtualization: only ~12 visible items are rendered

Debouncing y Throttling

Debouncing retrasa la ejecución hasta una pausa en la actividad (p. ej., el usuario deja de escribir). Throttling limita la ejecución a una vez por intervalo. Ambos previenen llamadas excesivas a API o cálculos. El hook useDebounce actualiza el valor con debounce solo después de que el usuario deja de escribir durante el delay especificado. Esto es esencial para inputs de búsqueda, handlers de resize y eventos de scroll. Para throttling, usa una librería como lodash.throttle o implementa con timestamps. Siempre limpia los timers en useEffect.

react
import { useState, useEffect } from 'react';

// Debounce hook: delays execution until user stops typing
function useDebounce(value, delay) {
  const [debounced, setDebounced] = useState(value);

  useEffect(() => {
    const timer = setTimeout(() => setDebounced(value), delay);
    return () => clearTimeout(timer);
  }, [value, delay]);

  return debounced;
}

function SearchInput() {
  const [query, setQuery] = useState('');
  const debouncedQuery = useDebounce(query, 300);

  // API call only fires when user stops typing for 300ms
  useEffect(() => {
    if (debouncedQuery) {
      fetch('/api/search?q=' + debouncedQuery);
    }
  }, [debouncedQuery]);

  return <input value={query} onChange={e => setQuery(e.target.value)} />;
}

Profiling y Optimización

El componente Profiler mide los tiempos de render. phase es 'mount' o 'update'. actualDuration es el tiempo de render en milisegundos. Usa React DevTools Profiler para flame charts visuales. Antes de optimizar, perfila para encontrar los bottlenecks reales — no adivines. Problemas comunes de rendimiento: (1) re-renders innecesarios (arregla con memo/useMemo/useCallback), (2) cálculos costosos (arregla con useMemo), (3) listas grandes (arregla con virtualización), (4) bundles grandes (arregla con code splitting). La optimización prematura pierde tiempo — mide primero.

react
import { Profiler } from 'react';

function App() {
  const onRender = (id, phase, actualDuration) => {
    console.log(id + ' ' + phase + ': ' + actualDuration + 'ms');
  };

  return (
    <Profiler id="App" onRender={onRender}>
      <ExpensiveComponent />
    </Profiler>
  );
}

/* Optimization checklist:
1. React.memo for expensive components
2. useMemo for expensive calculations
3. useCallback for props passed to memoized children
4. Code splitting (lazy) for routes
5. Virtualization for long lists
6. Debounce rapid events (search, resize)
7. Avoid inline objects/functions as props
8. Use keys correctly in lists
9. Profile with React DevTools Profiler
10. Check unnecessary re-renders with why-did-you-render
*/
10

Patrones y Error Boundaries

Hooks Personalizados

Los hooks personalizados extraen lógica con estado reutilizable en una función prefijada con 'use'. Pueden llamar a otros hooks. Los hooks personalizados son la forma principal de compartir lógica entre componentes (reemplazando HOCs y render props). Devuelve un objeto para múltiples valores, o un valor/array para valores únicos. Siempre maneja los estados de loading y error. El patrón de cancelación (flag cancelled) previene actualizaciones de estado después del desmontaje. Nombra los hooks con el prefijo 'use' para que las reglas de ESLint funcionen.

react
import { useState, useEffect } from 'react';

// Reusable data fetching hook
function useFetch(url) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    let cancelled = false;
    setLoading(true);
    fetch(url)
      .then(res => {
        if (!res.ok) throw new Error('HTTP ' + res.status);
        return res.json();
      })
      .then(data => { if (!cancelled) { setData(data); setError(null); }})
      .catch(err => { if (!cancelled) setError(err.message); })
      .finally(() => { if (!cancelled) setLoading(false); });
    return () => { cancelled = true; };
  }, [url]);

  return { data, loading, error };
}

// Usage
function UserProfile({ id }) {
  const { data: user, loading, error } = useFetch('/api/users/' + id);
  if (loading) return <p>Loading...</p>;
  if (error) return <p>Error: {error}</p>;
  return <h1>{user.name}</h1>;
}

Hook useLocalStorage

useLocalStorage persiste el estado a localStorage. El inicializador lazy lee de localStorage al montar. El useEffect escribe a localStorage cuando el valor cambia. El try/catch maneja errores de cuota excedida y errores de parseo JSON (datos corruptos). Este patrón funciona para cualquier estado persistente: temas, preferencias de usuario, contenido de borrador. Para compatibilidad con SSR, comprueba typeof window !== 'undefined'. Para sync entre pestañas, escucha el evento 'storage'. Hooks similares: useSessionStorage, useCookie.

react
import { useState, useEffect } from 'react';

function useLocalStorage(key, initialValue) {
  const [value, setValue] = useState(() => {
    try {
      const saved = localStorage.getItem(key);
      return saved ? JSON.parse(saved) : initialValue;
    } catch {
      return initialValue;
    }
  });

  useEffect(() => {
    try {
      localStorage.setItem(key, JSON.stringify(value));
    } catch (e) {
      console.error('LocalStorage error:', e);
    }
  }, [key, value]);

  return [value, setValue];
}

// Usage
function ThemeToggle() {
  const [theme, setTheme] = useLocalStorage('theme', 'light');
  return (
    <button onClick={() => setTheme(t => t === 'light' ? 'dark' : 'light')}>
      Current: {theme}
    </button>
  );
}

Error Boundaries

Los error boundaries capturan errores en los métodos render/lifecycle de los componentes hijos, previniendo que toda la app se cuelgue. Deben ser componentes de clase (no hay equivalente en hooks todavía). getDerivedStateFromError actualiza el estado para mostrar UI de fallback. componentDidCatch registra los errores (envía a Sentry, LogRocket, etc.). Los error boundaries NO capturan: event handlers, código asíncrono, setTimeout, errores en el propio boundary. Envuelve secciones específicas para aislar fallos. El botón 'Try again' reinicia el estado de error.

react
import { Component } from 'react';

class ErrorBoundary extends Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false, error: null };
  }

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

  componentDidCatch(error, errorInfo) {
    console.error('Caught error:', error, errorInfo);
    // Send to error reporting service
    // logErrorToService(error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      return (
        this.props.fallback || (
          <div>
            <h1>Something went wrong.</h1>
            <p>{this.state.error?.message}</p>
            <button onClick={() => this.setState({ hasError: false })}>
              Try again
            </button>
          </div>
        )
      );
    }
    return this.props.children;
  }
}

// Usage: wrap components
<ErrorBoundary fallback={<ErrorPage />}>
  <App />
</ErrorBoundary>

Higher-Order Components (HOC)

Los Higher-Order Components (HOCs) son funciones que toman un componente y devuelven uno mejorado. Eran el patrón principal para compartir lógica antes de los hooks. Usos comunes: autenticación, estados de loading, theming. Los HOCs pueden causar 'wrapper hell' (componentes profundamente anidados) y colisiones de props. Para código nuevo, prefiere hooks personalizados — son más simple, más componibles y no añaden al árbol de componentes. Los HOCs siguen siendo útiles para componentes de clase o cuando se integran con librerías que los requieren.

react
// HOC: function that takes a component and returns a new one
function withLoading(Component) {
  return function WithLoading({ isLoading, ...props }) {
    if (isLoading) return <div>Loading...</div>;
    return <Component {...props} />;
  };
}

// HOC for authentication
function withAuth(Component) {
  return function WithAuth(props) {
    const { user } = useContext(AuthContext);
    if (!user) return <Redirect to="/login" />;
    return <Component {...props} user={user} />;
  };
}

// Usage
const UserList = withLoading(withAuth(BaseUserList));

// Note: Prefer hooks over HOCs for new code
// HOCs are mainly for class components or library compatibility

Compound Components

Los compound components permiten a los usuarios componer un componente complejo a partir de partes simples. El padre (Select) proporciona contexto, y los componentes hijos (Trigger, Options, Option) lo consumen. Este patrón es usado por librerías como Radix UI, Headless UI y React Aria. Beneficios: API flexible (los usuarios pueden reordenar/omitir partes), compartición implícita de estado vía context, JSX limpio. Los componentes se adjuntan como propiedades estáticas (Select.Trigger). Este es un patrón avanzado — úsalo para librerías de UI reutilizables, no para componentes de un solo uso.

react
// Compound components: components that work together
function Select({ children, value, onChange }) {
  const [isOpen, setIsOpen] = useState(false);
  const context = { value, onChange, isOpen, setIsOpen };

  return (
    <SelectContext.Provider value={context}>
      <div className="select">{children}</div>
    </SelectContext.Provider>
  );
}

Select.Trigger = function Trigger({ children }) {
  const { isOpen, setIsOpen } = useContext(SelectContext);
  return <button onClick={() => setIsOpen(!isOpen)}>{children}</button>;
};

Select.Options = function Options({ children }) {
  const { isOpen } = useContext(SelectContext);
  return isOpen ? <div className="options">{children}</div> : null;
};

Select.Option = function Option({ value, children }) {
  const { onChange, setIsOpen } = useContext(SelectContext);
  return (
    <div onClick={() => { onChange(value); setIsOpen(false); }}>
      {children}
    </div>
  );
}

// Usage: clean, declarative API
<Select value={val} onChange={setVal}>
  <Select.Trigger>Choose...</Select.Trigger>
  <Select.Options>
    <Select.Option value="a">Option A</Select.Option>
    <Select.Option value="b">Option B</Select.Option>
  </Select.Options>
</Select>
11

Hooks Personalizados

Hook useFetch

Los hooks personalizados extraen lógica con estado reutilizable en una función prefijada con 'use'. useFetch encapsula la obtención de datos con estados loading/error. El AbortController cancela las peticiones en curso cuando el componente se desmonta o la URL cambia (previniendo race conditions y memory leaks). Siempre incluye limpieza en useEffect para operaciones asíncronas. Los hooks personalizados pueden llamar a otros hooks (useState, useEffect, useContext). Son la forma principal de compartir lógica entre componentes sin render props o HOCs. Nómbrales con el prefijo 'use' para que el linter de rules-of-hooks de React funcione.

react
function useFetch(url, options = {}) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    let abortController = new AbortController();
    setLoading(true);
    fetch(url, { ...options, signal: abortController.signal })
      .then((res) => {
        if (!res.ok) throw new Error(res.statusText);
        return res.json();
      })
      .then((data) => { setData(data); setError(null); })
      .catch((err) => {
        if (err.name !== "AbortError") setError(err.message);
      })
      .finally(() => setLoading(false));
    return () => abortController.abort();
  }, [url]);

  return { data, loading, error };
}

// Usage
function Profile({ userId }) {
  const { data, loading, error } = useFetch(`/api/users/${userId}`);
  if (loading) return <p>Loading...</p>;
  if (error) return <p>Error: {error}</p>;
  return <div>{data.name}</div>;
}

Hook useLocalStorage

useLocalStorage sincroniza el estado de React con localStorage. El inicializador lazy lee de localStorage solo en el primer render. El useEffect escribe a localStorage cuando el valor cambia. El try/catch maneja casos donde localStorage está lleno o deshabilitado (navegación privada). Este hook hace el estado persistente tan fácil como useState. Para sincronización entre pestañas, añade un listener del evento storage. Para seguridad en SSR, protege el acceso a window. Este patrón funciona para sessionStorage también — solo cambia la API.

react
function useLocalStorage(key, initialValue) {
  const [value, setValue] = useState(() => {
    try {
      const stored = window.localStorage.getItem(key);
      return stored ? JSON.parse(stored) : initialValue;
    } catch {
      return initialValue;
    }
  });

  useEffect(() => {
    try {
      window.localStorage.setItem(key, JSON.stringify(value));
    } catch (e) {
      console.error("LocalStorage write failed:", e);
    }
  }, [key, value]);

  return [value, setValue];
}

// Usage: persists state across page reloads
function Settings() {
  const [theme, setTheme] = useLocalStorage("theme", "light");
  const [fontSize, setFontSize] = useLocalStorage("fontSize", 14);
  return (
    <div>
      <button onClick={() => setTheme("dark")}>Dark</button>
      <button onClick={() => setTheme("light")}>Light</button>
    </div>
  );
}

Hook useDebounce

useDebounce retrasa la actualización de un valor hasta que el usuario deja de escribir durante el delay especificado. Esto es esencial para inputs de búsqueda, autoguardado y llamadas a API disparadas por input del usuario — previene llamadas excesivas en cada pulsación de tecla. La función de limpieza limpia el timeout si el valor cambia de nuevo antes de que expire el delay. El valor con debounce solo se actualiza después de la pausa, disparando efectos posteriores (como llamadas a API) con menos frecuencia. Para ejecución inmediata con una llamada trailing, usa useThrottle en su lugar. Combina con useFetch para búsqueda-as-you-type eficiente.

react
function useDebounce(value, delay = 500) {
  const [debounced, setDebounced] = useState(value);
  useEffect(() => {
    const timer = setTimeout(() => setDebounced(value), delay);
    return () => clearTimeout(timer);
  }, [value, delay]);
  return debounced;
}

// Usage: debounce search input
function Search() {
  const [query, setQuery] = useState("");
  const debouncedQuery = useDebounce(query, 300);

  useEffect(() => {
    if (debouncedQuery) {
      fetch(`/api/search?q=${debouncedQuery}`)
        .then((res) => res.json())
        .then(setResults);
    }
  }, [debouncedQuery]);

  return <input value={query} onChange={(e) => setQuery(e.target.value)} />;
}

Hook usePrevious

usePrevious aprovecha el hecho de que useEffect se ejecuta después del render — ref.current todavía mantiene el valor antiguo durante el render, luego se actualiza al nuevo valor después. Este es un patrón común para comparar el estado actual y previo. useWindowSize rastrea las dimensiones del viewport con un listener de resize. Siempre limpia los event listeners en el return de useEffect para prevenir memory leaks. Estos hooks de utilidad demuestran cómo los hooks personalizados encapsulan lógica relacionada con el DOM, haciendo los componentes más limpios y la lógica reutilizable y testeable.

react
function usePrevious(value) {
  const ref = useRef(null);
  useEffect(() => {
    ref.current = value; // update AFTER render
  }, [value]);
  return ref.current; // returns previous value during render
}

// Usage: compare current vs previous
function Counter() {
  const [count, setCount] = useState(0);
  const prevCount = usePrevious(count);

  return (
    <div>
      <p>Now: {count}, before: {prevCount}</p>
      {count > prevCount && <p>Increased!</p>}
      <button onClick={() => setCount(count + 1)}>+</button>
    </div>
  );
}

// useWindowSize hook
function useWindowSize() {
  const [size, setSize] = useState({
    width: window.innerWidth,
    height: window.innerHeight,
  });
  useEffect(() => {
    const handler = () =>
      setSize({ width: innerWidth, height: innerHeight });
    window.addEventListener("resize", handler);
    return () => removeEventListener("resize", handler);
  }, []);
  return size;
}

useToggle y useClipboard

useToggle simplifica el estado booleano con una función toggle envuelta en useCallback para identidad estable. useClipboard envuelve la clipboard API con un estado de feedback 'copied' que se auto-reinicia después de un timeout. Estos pequeños hooks de utilidad reducen el boilerplate y estandarizan patrones comunes en tu app. El useCallback en ambos hooks previene re-renders innecesarios de hijos memoizados. Construir una librería de hooks pequeños y enfocados (useToggle, useClipboard, useMediaQuery, useOnClickOutside) acelera el desarrollo y asegura comportamiento consistente.

react
function useToggle(initial = false) {
  const [on, setOn] = useState(initial);
  const toggle = useCallback(() => setOn((p) => !p), []);
  return [on, toggle, setOn];
}

function useClipboard(timeout = 2000) {
  const [copied, setCopied] = useState(false);
  const copy = useCallback((text) => {
    navigator.clipboard.writeText(text).then(() => {
      setCopied(true);
      setTimeout(() => setCopied(false), timeout);
    });
  }, [timeout]);
  return [copied, copy];
}

// Usage
function CopyButton({ text }) {
  const [copied, copy] = useClipboard();
  return (
    <button onClick={() => copy(text)}>
      {copied ? "Copied!" : "Copy"}
    </button>
  );
}

function Modal({ children }) {
  const [isOpen, toggle] = useToggle(false);
  return (
    <>
      <button onClick={toggle}>Open</button>
      {isOpen && <div className="modal">{children}</div>}
    </>
  );
}
12

Portals

Crear un Portal

createPortal renderiza children en un nodo DOM fuera de la jerarquía del componente actual (típicamente document.body). Esto es esencial para modales, tooltips y dropdowns que deben escapar de las restricciones CSS del padre (overflow: hidden, stacking contexts de z-index, transform creando nuevos contexts). A pesar de renderizarse en otro lugar del DOM, el event bubbling de React del portal sigue funcionando como si estuviera en el árbol original — los handlers onClick en los ancestros siguen disparándose. Esto te da lo mejor de ambos mundos: escape visual de las restricciones del padre, pero flujo lógico de eventos preservado.

react
import { createPortal } from "react-dom";

function Modal({ children, onClose }) {
  return createPortal(
    <div className="modal-overlay" onClick={onClose}>
      <div className="modal-content" onClick={(e) => e.stopPropagation()}>
        <button onClick={onClose}>✕</button>
        {children}
      </div>
    </div>,
    document.body  // render target outside the DOM hierarchy
  );
}

// Usage: the modal renders at body level,
// escaping any overflow:hidden or z-index stacking contexts
function App() {
  const [show, setShow] = useState(false);
  return (
    <div style={{ overflow: "hidden", position: "relative" }}>
      <button onClick={() => setShow(true)}>Open Modal</button>
      {show && <Modal onClose={() => setShow(false)}>Hello!</Modal>}
    </div>
  );
}

Modal con Portal y Focus Trap

Un modal de producción necesita más que solo un portal: gestión de focus (trap focus dentro, restaurar al cerrar), manejo de la tecla Escape, bloqueo de scroll del body, y click-outside-to-close. Esta implementación guarda el elemento previamente enfocado, enfoca el modal al abrir, y restaura el focus al cerrar — esencial para usuarios de screen reader. Body overflow hidden previene el scroll de fondo. La función de limpieza restaura todo. Para focus trapping completo (cycling de tab dentro del modal), usa una librería como focus-trap-react. Siempre devuelve null cuando esté cerrado para eliminarlo del DOM.

react
function Modal({ isOpen, onClose, children }) {
  const modalRef = useRef(null);

  useEffect(() => {
    if (!isOpen) return;
    const modal = modalRef.current;
    const previouslyFocused = document.activeElement;
    modal.focus();

    const handleKey = (e) => {
      if (e.key === "Escape") onClose();
    };
    document.addEventListener("keydown", handleKey);

    // Prevent body scroll
    document.body.style.overflow = "hidden";

    return () => {
      document.removeEventListener("keydown", handleKey);
      document.body.style.overflow = "";
      previouslyFocused.focus(); // restore focus
    };
  }, [isOpen, onClose]);

  if (!isOpen) return null;

  return createPortal(
    <div className="overlay" onClick={onClose}>
      <div ref={modalRef} tabIndex={-1} className="modal">
        {children}
      </div>
    </div>,
    document.body
  );
}

Tooltips con Portals

Los tooltips se benefician de los portals porque deben desbordar los contenedores padres y evitar el clipping. La posición del tooltip se calcula a partir del getBoundingClientRect() del trigger y se renderiza con position: fixed a nivel del body. Esto evita problemas de z-index y overflow. Para posicionamiento dinámico (flip cuando está cerca del borde de la pantalla), usa una librería como Floating UI (anteriormente Popper.js). El portal asegura que el tooltip nunca sea clipped por ancestros con overflow: hidden. Las coordenadas de fixed positioning son relativas al viewport, haciendo el cálculo sencillo.

react
function Tooltip({ children, text }) {
  const [visible, setVisible] = useState(false);
  const [coords, setCoords] = useState({ x: 0, y: 0 });
  const targetRef = useRef(null);

  const show = () => {
    const rect = targetRef.current.getBoundingClientRect();
    setCoords({ x: rect.left, y: rect.top - 40 });
    setVisible(true);
  };

  return (
    <>
      <span
        ref={targetRef}
        onMouseEnter={show}
        onMouseLeave={() => setVisible(false)}
      >
        {children}
      </span>
      {visible && createPortal(
        <div style={{ position: "fixed", left: coords.x, top: coords.y }}
             className="tooltip">
          {text}
        </div>,
        document.body
      )}
    </>
  );
}

// Usage
<Tooltip text="Click to save">💾</Tooltip>

Menús Dropdown con Portals

Los menús dropdown enfrentan los mismos problemas de overflow/z-index que los tooltips. Los portals resuelven el problema visual. El handler de click-outside comprueba si el target del click está fuera del ref del trigger. El listener de scroll en fase de captura (true como tercer argumento) cierra el menú en cualquier scroll, previniendo que el menú se separe de su trigger. Para producción, usa Floating UI que maneja edge detection, flipping, shifting y actualizaciones automáticas de posicionamiento en scroll/resize. Portals + lógica de posicionamiento adecuada = dropdowns robustos que funcionan en cualquier contexto de layout.

react
function Dropdown({ trigger, children }) {
  const [open, setOpen] = useState(false);
  const [pos, setPos] = useState({ top: 0, left: 0 });
  const ref = useRef(null);

  const handleOpen = () => {
    const rect = ref.current.getBoundingClientRect();
    setPos({ top: rect.bottom + 4, left: rect.left });
    setOpen(true);
  };

  useEffect(() => {
    if (!open) return;
    const handleClick = (e) => {
      if (!ref.current?.contains(e.target)) setOpen(false);
    };
    const handleScroll = () => setOpen(false); // close on scroll
    document.addEventListener("mousedown", handleClick);
    window.addEventListener("scroll", handleScroll, true);
    return () => {
      document.removeEventListener("mousedown", handleClick);
      window.removeEventListener("scroll", handleScroll, true);
    };
  }, [open]);

  return (
    <>
      <div ref={ref} onClick={handleOpen}>{trigger}</div>
      {open && createPortal(
        <div style={{ position: "fixed", ...pos }} className="dropdown">
          {children}
        </div>,
        document.body
      )}
    </>
  );
}

Event Bubbling en Portals

Una característica clave de React Portals: el event bubbling sigue el árbol de componentes de React, no el árbol DOM. onClick en un componente padre se dispara incluso cuando el hijo está portaled a document.body. Esto significa que context, state y event delegation funcionan naturalmente. Sin embargo, la herencia CSS NO cruza el límite del portal — los estilos del padre no se cascaden al contenido portaled ya que están en diferentes subárboles DOM. Debes aplicar CSS explícitamente (vía classes o CSS variables en :root) para estilizar el contenido del portal. Esta separación es usualmente deseable para modales/tooltips.

react
function PortalExample() {
  // Despite rendering in document.body, events bubble
  // through the React tree, not the DOM tree
  return (
    <div onClick={() => console.log("Parent clicked!")}>
      <p>Click the button — parent handler fires!</p>
      {createPortal(
        <button onClick={() => console.log("Button clicked!")}>
          I'm in a portal
        </button>,
        document.body
      )}
    </div>
  );
}
// Clicking logs: "Button clicked!" then "Parent clicked!"

// This means context, state, and event delegation
// all work as if the portal were inline

// But CSS inheritance does NOT cross the portal boundary:
// document.body styles won't inherit into the portal content
// unless you explicitly apply them
13

Suspense y Lazy Loading

React.lazy y Suspense

React.lazy importa dinámicamente un componente, creando un bundle separado que se carga bajo demanda (code splitting). Envuelve los componentes lazy en <Suspense> con un fallback (estado de loading) que se muestra mientras el chunk se descarga. Esto reduce el tamaño inicial del bundle — los usuarios solo descargan código para las páginas que visitan. Cada llamada lazy() crea un chunk separado. Para splitting basado en rutas, lazy-load cada componente de página. El fallback puede ser cualquier nodo de React (spinner, skeleton, texto). Suspense puede envolver múltiples componentes lazy — el fallback se muestra hasta que todos estén listos.

react
import { lazy, Suspense } from "react";

// Lazy-load component (code-split)
const Dashboard = lazy(() => import("./Dashboard"));
const Settings = lazy(() => import("./Settings"));

function App() {
  return (
    <Suspense fallback={<div>Loading page...</div>}>
      <nav>
        <button onClick={() => setPage("dash")}>Dashboard</button>
        <button onClick={() => setPage("settings")}>Settings</button>
      </nav>
      {page === "dash" && <Dashboard />}
      {page === "settings" && <Settings />}
    </Suspense>
  );
}

Suspense Anidado

Los límites de Suspense anidados crean un efecto de 'pelado' donde el contenido se revela progresivamente a medida que cada chunk carga. El Suspense exterior muestra su fallback primero; a medida que los componentes interiores cargan, se revelan independientemente. Esto previene que un solo componente lento bloquee toda la página. Coloca los límites de Suspense estratégicamente: alrededor de páginas a nivel de ruta (coarse), alrededor de secciones principales (medium), y alrededor de widgets independientes (fine). Demasiados límites crean loading entrecortado; muy pocos crean esperas largas. La clave es matching los límites a las unidades de contenido percibidas por el usuario.

react
<Suspense fallback={<PageSkeleton />}>
  <Header />
  <Suspense fallback={<MainSkeleton />}>
    <MainContent />  {/* loads first */}
    <Suspense fallback={<CommentsSkeleton />}>
      <Comments />  {/* loads independently, doesn't block MainContent */}
    </Suspense>
  </Suspense>
  <Sidebar />
</Suspense>

// Suspense "peeling" effect:
// 1. PageSkeleton shows
// 2. Header + Sidebar load → PageSkeleton peels away
// 3. MainSkeleton shows until MainContent loads
// 4. MainContent shows, CommentsSkeleton shows
// 5. Comments load → everything visible

Lazy con Error Boundaries

El lazy loading puede fallar (problemas de red, deployments invalidando URLs de chunk). Los Error Boundaries capturan estos errores y muestran UI de fallback. Siempre envuelve Suspense + lazy en un ErrorBoundary. El componentDidCatch registra los errores para monitoreo. Para lógica de retry, puedes reiniciar el estado del ErrorBoundary o recargar la página. Un patrón común es un botón de retry que re-importa el chunk. Sin error boundaries, una carga de chunk fallida cuelga toda la app. Esto es crítico para producción — la fiabilidad de red nunca es del 100%.

react
class ErrorBoundary extends React.Component {
  state = { hasError: false, error: null };
  static getDerivedStateFromError(error) {
    return { hasError: true, error };
  }
  componentDidCatch(error, info) {
    console.error("Chunk load failed:", error, info);
  }
  render() {
    if (this.state.hasError) {
      return (
        <div>
          <p>Failed to load. {this.state.error.message}</p>
          <button onClick={() => window.location.reload()}>
            Retry
          </button>
        </div>
      );
    }
    return this.props.children;
  }
}

// Wrap lazy components: network failures need handling
<ErrorBoundary>
  <Suspense fallback={<Loader />}>
    <LazyComponent />
  </Suspense>
</ErrorBoundary>

Obtención de Datos con Suspense

El hook use() de React 19 habilita Suspense para obtención de datos. A diferencia de useEffect, use() suspende el componente hasta que la promesa se resuelve — el límite de Suspense más cercano muestra su fallback. Múltiples llamadas use() en el mismo componente se resuelven concurrentemente (parallel fetching). Esto elimina la gestión manual del estado de loading. La promesa puede ser cacheada fuera de React para prevenir refetching en re-render. Nota: use() solo puede llamarse en render o dentro de hooks. Para React 18, usa librerías como React Query o SWR que se integran con Suspense.

react
// React 18+ Suspense for data fetching (experimental)
import { use } from "react"; // React 19+

// Wrap a promise with use()
function UserProfile({ userId }) {
  // 'use' suspends until the promise resolves
  const user = use(fetchUser(userId));
  return <div>{user.name}</div>;
}

function fetchUser(id) {
  return fetch(`/api/users/${id}`).then((r) => r.json());
}

// Parent provides Suspense boundary
function App() {
  return (
    <Suspense fallback={<Spinner />}>
      <UserProfile userId={1} />
    </Suspense>
  );
}

// Concurrent: multiple suspends resolve together
function Dashboard() {
  const user = use(fetchUser(1));
  const posts = use(fetchPosts(user.id));
  // Both fetch in parallel, Suspense shows until all resolve
  return <div>{user.name}: {posts.length} posts</div>;
}

Suspense List (Orquestación)

SuspenseList orquesta el orden de revelación de múltiples límites de Suspense. revealOrder='forwards' muestra los items en orden (el item 2 no se revelará hasta que el item 1 esté listo, incluso si el 2 carga primero) — previene saltos de contenido. 'together' espera a todos antes de revelar. 'backwards' revela de abajo hacia arriba. tail='collapsed' oculta los estados de loading para items que aún no han comenzado; 'hidden' oculta todos los fallbacks. Esto es útil para feeds y listas donde el orden importa. Nota: SuspenseList era experimental y su API puede cambiar — comprueba los docs actuales de React para disponibilidad.

react
// SuspenseList controls reveal order of multiple Suspense
import { SuspenseList, Suspense } from "react";

function Article({ id }) {
  const data = use(fetchArticle(id));
  return <article>{data.title}</article>;
}

function Feed() {
  return (
    <SuspenseList revealOrder="forwards" tail="collapsed">
      {/* "forwards": reveals top-to-bottom as they load */}
      {/* "together": waits for all, reveals together */}
      {/* "backwards": reveals bottom-to-top */}

      <Suspense fallback={<Skeleton />}>
        <Article id={1} />
      </Suspense>
      <Suspense fallback={<Skeleton />}>
        <Article id={2} />
      </Suspense>
      <Suspense fallback={<Skeleton />}>
        <Article id={3} />
      </Suspense>
    </SuspenseList>
  );
}
14

React Router

Routing Básico

React Router v6 usa <BrowserRouter> como raíz, <Routes> para definir el matching de rutas, y <Route> con la prop element (no component). <Link> crea enlaces de navegación que usan la History API (sin recarga de página). Los segmentos dinámicos (:id) se acceden vía useParams(). path='*' es un catch-all para 404s. Las rutas se matchean por mejor coincidencia, no por orden. Para URL search params (?q=search), usa useSearchParams(). BrowserRouter requiere configuración del servidor para servir index.html para todas las rutas (SPA fallback).

react
import { BrowserRouter, Routes, Route, Link } from "react-router-dom";

function App() {
  return (
    <BrowserRouter>
      <nav>
        <Link to="/">Home</Link>
        <Link to="/about">About</Link>
        <Link to="/users/123">User 123</Link>
      </nav>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/about" element={<About />} />
        <Route path="/users/:id" element={<UserPage />} />
        <Route path="*" element={<NotFound />} />
      </Routes>
    </BrowserRouter>
  );
}

function UserPage() {
  const { id } = useParams();
  return <h1>User ID: {id}</h1>;
}

Rutas Anidadas y Outlet

Las rutas anidadas crean jerarquías de layout. El element de la ruta padre debe incluir <Outlet /> donde se renderizan las rutas hijas. La ruta index se renderiza en el path del padre. Las rutas profundamente anidadas (users/:id) crean layouts anidados — el layout de Users envuelve a UserDetail. Esto es potente para dashboards con sidebars/headers persistentes. useOutlet() da acceso al element hijo. La URL /users/123 renderiza Layout → Users → UserDetail, cada uno contribuyendo su layout. Esto reemplaza el renderizado condicional manual de layouts.

react
function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<Layout />}>
          <Route index element={<Home />} />
          <Route path="about" element={<About />} />
          <Route path="users" element={<Users />}>
            <Route path=":id" element={<UserDetail />} />
          </Route>
        </Route>
      </Routes>
    </BrowserRouter>
  );
}

function Layout() {
  return (
    <div>
      <nav>Navigation here</nav>
      <Outlet />  {/* Child routes render here */}
    </div>
  );
}

function Users() {
  return (
    <div>
      <h2>Users</h2>
      <Outlet />  {/* Nested :id route renders here */}
    </div>
  );
}

Navegación y Redirects

useNavigate devuelve una función para navegación programática. navigate('/path', { replace: true }) reemplaza el history (sin botón de atrás). Pasa state para llevar datos a la siguiente ruta (p. ej., dónde volver después del login). <Navigate> es el componente declarativo de redirect — úsalo en render para auth guards. NavLink proporciona isActive para estilizar enlaces activos. useLocation da la URL actual, pathname, search, hash y state. Para redirects después de acciones (submit de formulario), usa navigate. Para redirects condicionales en render, usa <Navigate>.

react
import { useNavigate, Navigate, NavLink, useLocation } from "react-router-dom";

function Login() {
  const navigate = useNavigate();
  const location = useLocation();

  const handleLogin = async () => {
    await auth.login();
    // Redirect to intended page or home
    const from = location.state?.from || "/";
    navigate(from, { replace: true });
  };
  return <button onClick={handleLogin}>Login</button>;
}

// Declarative redirect
function ProtectedRoute({ user, children }) {
  if (!user) {
    return <Navigate to="/login" state={{ from: location }} replace />;
  }
  return children;
}

// NavLink: active styling
<NavLink to="/about" className={({ isActive }) =>
  isActive ? "nav-active" : "nav"
}>
  About
</NavLink>

Loaders y Carga de Datos

React Router v6.4+ (data router) añade loaders (se ejecutan antes de que la ruta renderice) y actions (manejan submits de formulario). useLoaderData() accede a los datos del loader — no más useEffect para obtener datos de ruta. Los loaders se ejecutan en paralelo para rutas anidadas. errorElement captura errores de loaders/actions. Las actions procesan submits de formulario vía <Form method='post'> — useActionData() devuelve el resultado. Este patrón (inspirado en Remix) co-ubica la lógica de datos con las rutas. Las APIs de datos requieren createBrowserRouter/createHashRouter, no <BrowserRouter>.

react
import { createBrowserRouter, RouterProvider } from "react-router-dom";

const router = createBrowserRouter([
  {
    path: "/users/:id",
    element: <UserPage />,
    loader: async ({ params }) => {
      const res = await fetch(`/api/users/${params.id}`);
      if (!res.ok) throw new Response("Not found", { status: 404 });
      return res.json();
    },
    errorElement: <ErrorPage />,
  },
]);

function UserPage() {
  const user = useLoaderData(); // data from loader
  return <h1>{user.name}</h1>;
}

// Action for form submissions
{
  path: "/users/new",
  element: <NewUser />,
  action: async ({ request }) => {
    const formData = await request.formData();
    const res = await fetch("/api/users", {
      method: "POST",
      body: formData,
    });
    return redirect(`/users/${res.id}`);
  },
}

function App() {
  return <RouterProvider router={router} />;
}

Route Guards y Rutas Protegidas

Los route guards protegen rutas basándose en el estado de auth o roles. RequireAuth redirige a los usuarios no autenticados al login, preservando el destino pretendido en location.state para redirect post-login. RequireRole añade control de acceso basado en roles. Compón guards envolviéndolos (RequireAuth > RequireRole > component). Para rutas de layout, también puedes usar element={<RequireAuth><Outlet/></RequireAuth>} para proteger todas las rutas hijas a la vez. Siempre comprueba auth en el servidor también — los guards del lado del cliente son para UX, no para seguridad. El patrón escala a cualquier condición: suscripción, feature flags, etc.

react
function RequireAuth({ children }) {
  const { user } = useAuth();
  const location = useLocation();

  if (!user) {
    return <Navigate to="/login" state={{ from: location }} replace />;
  }
  return children;
}

function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/login" element={<Login />} />
        <Route path="/" element={<Layout />}>
          <Route index element={<Home />} />
          <Route path="dashboard" element={
            <RequireAuth><Dashboard /></RequireAuth>
          } />
          <Route path="admin" element={
            <RequireAuth><RequireRole role="admin"><Admin /></RequireRole></RequireAuth>
          } />
        </Route>
      </Routes>
    </BrowserRouter>
  );
}

function RequireRole({ role, children }) {
  const { user } = useAuth();
  if (user?.role !== role) return <Navigate to="/forbidden" />;
  return children;
}
15

Gestión de Estado (Context y Redux)

Patrón Context API

Context proporciona estado global sin prop drilling. Crea un context, envuelve los consumidores en un Provider, y accede vía useContext. El hook personalizado useAuth añade comprobación de errores y es la superficie de API recomendada. Context es ideal para actualizaciones de baja frecuencia (auth, theme, locale). Para cambios de estado de alta frecuencia, Context causa que todos los consumidores se re-rendericen en cada cambio — usa useReducer para estado complejo o divide contextos. Siempre co-ubica el provider con el estado que gestiona. El valor del Context debería ser memoizado con useMemo/useCallback si contiene funciones.

react
const AuthContext = createContext(null);

function AuthProvider({ children }) {
  const [user, setUser] = useState(null);
  const login = async (credentials) => {
    const user = await api.login(credentials);
    setUser(user);
  };
  const logout = () => setUser(null);
  return (
    <AuthContext.Provider value={{ user, login, logout }}>
      {children}
    </AuthContext.Provider>
  );
}

function useAuth() {
  const ctx = useContext(AuthContext);
  if (!ctx) throw new Error("useAuth must be inside AuthProvider");
  return ctx;
}

// Usage
function Navbar() {
  const { user, logout } = useAuth();
  return user
    ? <button onClick={logout}>Logout {user.name}</button>
    : <Link to="/login">Login</Link>;
}

// Wrap app: <AuthProvider><App /></AuthProvider>

useReducer + Context

useReducer + Context es el patrón recomendado para estado global complejo sin librerías externas. El reducer centraliza la lógica de estado (transiciones predecibles, testeables). El provider memoiza el valor para prevenir re-renders innecesarios. Los valores derivados (total) se calculan en useMemo. Este patrón maneja carrito, estado de formulario, wizards multi-paso, etc. Para apps verdaderamente complejas con middleware, time-travel debugging, o muchas slices independientes, considera Redux Toolkit o Zustand. Pero para la mayoría de apps, useReducer + Context es suficiente y tiene cero dependencias.

react
const CartContext = createContext();

function cartReducer(state, action) {
  switch (action.type) {
    case "ADD":
      const existing = state.find((i) => i.id === action.item.id);
      if (existing) {
        return state.map((i) =>
          i.id === action.item.id ? { ...i, qty: i.qty + 1 } : i
        );
      }
      return [...state, { ...action.item, qty: 1 }];
    case "REMOVE":
      return state.filter((i) => i.id !== action.id);
    case "CLEAR":
      return [];
    default:
      return state;
  }
}

function CartProvider({ children }) {
  const [items, dispatch] = useReducer(cartReducer, []);
  const value = useMemo(() => ({
    items,
    total: items.reduce((s, i) => s + i.price * i.qty, 0),
    addItem: (item) => dispatch({ type: "ADD", item }),
    removeItem: (id) => dispatch({ type: "REMOVE", id }),
  }), [items]);
  return <CartContext.Provider value={value}>{children}</CartContext.Provider>;
}

Fundamentos de Redux Toolkit

Redux Toolkit (RTK) es la forma moderna y recomendada de usar Redux. createSlice auto-genera action creators y reducers. Usa Immer internamente, así que 'mutas' el estado directamente (state.value += 1) e Immer produce la actualización inmutable. configureStore configura el store con defaults sensatos (Redux DevTools, thunk middleware). useSelector lee el estado; useDispatch dispatcha actions. RTK elimina el boilerplate de Redux (sin switch statements, sin constantes de action type). Para lógica asíncrona, usa createAsyncThunk. RTK Query (incluido) maneja obtención de datos y caching.

react
import { configureStore, createSlice } from "@reduxjs/toolkit";
import { useSelector, useDispatch } from "react-redux";

const counterSlice = createSlice({
  name: "counter",
  initialState: { value: 0 },
  reducers: {
    increment: (state) => { state.value += 1; }, // Immer: mutate safely
    decrement: (state) => { state.value -= 1; },
    addBy: (state, action) => { state.value += action.payload; },
  },
});

const store = configureStore({
  reducer: { counter: counterSlice.reducer },
});

export const { increment, decrement, addBy } = counterSlice.actions;

// Usage in component
function Counter() {
  const count = useSelector((state) => state.counter.value);
  const dispatch = useDispatch();
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => dispatch(increment())}>+</button>
      <button onClick={() => dispatch(addBy(5))}>+5</button>
    </div>
  );
}

Zustand (Alternativa Ligera)

Zustand es una librería minimalista de gestión de estado — sin providers, sin boilerplate. Crea un store con create(), accede vía hooks con funciones selector. Los selectores previenen re-renders: solo los componentes que usan la slice cambiada se re-renderizan. Esto resuelve el problema de re-render de Context sin la complejidad de Redux. Para selectores de objeto (que devuelven {a, b}), usa shallow comparison para prevenir re-renders innecesarios. Zustand soporta middleware (persist, devtools, immer). Es ideal para apps pequeñas-a-medianas donde Redux es excesivo pero Context causa demasiados re-renders. La API es diminuta pero potente.

react
import { create } from "zustand";

const useStore = create((set, get) => ({
  count: 0,
  user: null,
  increment: () => set((state) => ({ count: state.count + 1 })),
  setUser: (user) => set({ user }),
  reset: () => set({ count: 0, user: null }),
  // Access other state with get()
  doubleCount: () => get().count * 2,
}));

// Usage: select only what you need (prevents re-renders)
function Counter() {
  const count = useStore((state) => state.count);
  const increment = useStore((state) => state.increment);
  return <button onClick={increment}>{count}</button>;
}

// Multiple selections
function Profile() {
  const { user, setUser } = useStore(
    (state) => ({ user: state.user, setUser: state.setUser })
  );
  // Use shallow comparison for object selectors
  // import { shallow } from "zustand/shallow";
  // useStore(selector, shallow);
}

React Query (Estado del Servidor)

React Query (TanStack Query) gestiona el estado del servidor — datos obtenidos de APIs. Maneja caching, refetching en background, stale data, optimistic updates y paginación automáticamente. queryKey identifica los datos cacheados (como una cache key). staleTime controla cuánto tiempo los datos se consideran fresh. invalidateQueries después de mutations refetchea las queries dependientes. A diferencia de Redux (estado del cliente), React Query está construido específicamente para datos asíncronos del servidor. Elimina los estados manuales de loading/error, fetching con useEffect y gestión de cache. Para la mayoría de apps, React Query + estado local (useState/useReducer) reemplaza a Redux completamente.

react
import { useQuery, useMutation, QueryClient, QueryClientProvider } from "@tanstack/react-query";

const queryClient = new QueryClient();

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <Users />
    </QueryClientProvider>
  );
}

function Users() {
  const { data, isLoading, error, refetch } = useQuery({
    queryKey: ["users"],
    queryFn: () => fetch("/api/users").then((r) => r.json()),
    staleTime: 60000, // fresh for 60s
    refetchOnWindowFocus: true,
  });

  const mutation = useMutation({
    mutationFn: (newUser) =>
      fetch("/api/users", { method: "POST", body: JSON.stringify(newUser) }),
    onSuccess: () => queryClient.invalidateQueries(["users"]),
  });

  if (isLoading) return <p>Loading...</p>;
  if (error) return <p>Error: {error.message}</p>;
  return (
    <div>
      {data.map((u) => <div key={u.id}>{u.name}</div>)}
      <button onClick={() => mutation.mutate({ name: "New" })}>Add</button>
    </div>
  );
}
16

Testing (React Testing Library)

Test Básico de Componente

React Testing Library (RTL) testea los componentes como los usuarios interactúan con ellos — por role, label y text, no por detalles de implementación. getByRole es la query preferida (testea accesibilidad también). userEvent simula interacciones reales de usuario (typing, clicking) más precisamente que fireEvent. Los tests deberían evitar testear el estado interno; en su lugar, verifica la salida visible y el comportamiento. Si no puedes hacer query por role, usa getByLabelText, getByText o getByDisplayValue. Evita getByTestId a menos que sea necesario. Este enfoque hace los tests resilientes al refactoring — testean lo que los usuarios ven y hacen.

react
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { Counter } from "./Counter";

test("counter increments on click", async () => {
  const user = userEvent.setup();
  render(<Counter />);

  // Find by accessible role (not test-id)
  expect(screen.getByRole("heading")).toHaveTextContent("0");

  await user.click(screen.getByRole("button", { name: /increment/i }));

  expect(screen.getByRole("heading")).toHaveTextContent("1");
});

test("displays error for invalid input", async () => {
  const user = userEvent.setup();
  render(<Form />);

  await user.type(screen.getByLabelText(/email/i), "not-an-email");
  await user.click(screen.getByRole("button", { name: /submit/i }));

  expect(screen.getByRole("alert")).toHaveTextContent(/invalid email/i);
});

Testear Hooks

renderHook testea hooks personalizados de forma aislada. result.current mantiene el valor de retorno del hook. Todas las actualizaciones de estado deben envolverse en act() para asegurar que React las procese sincrónicamente. rerender te permite testear efectos que dependen de props cambiantes. Para hooks asíncronos (useEffect con fetch), usa waitFor o queries findBy (que esperan actualizaciones). Testear hooks directamente es más rápido y enfocado que testear a través de un componente. Sin embargo, también testea hooks a través de tests de integración de componentes para verificar el uso real. renderHook está disponible en @testing-library/react v13+.

react
import { renderHook, act } from "@testing-library/react";
import { useCounter } from "./useCounter";

test("useCounter increments and decrements", () => {
  const { result } = renderHook(() => useCounter(0));

  expect(result.current.count).toBe(0);

  // Wrap state updates in act()
  act(() => result.current.increment());
  expect(result.current.count).toBe(1);

  act(() => result.current.decrement());
  expect(result.current.count).toBe(0);

  act(() => result.current.reset());
  expect(result.current.count).toBe(0);
});

// Testing with initial props that change
test("useEffect runs on dependency change", () => {
  const { result, rerender } = renderHook(
    ({ id }) => useFetchUser(id),
    { initialProps: { id: 1 } }
  );
  rerender({ id: 2 });
  // Effect re-ran with new id
});

Testear Async y Mocking

MSW (Mock Service Worker) intercepta peticiones de red a nivel de service worker — los tests usan fetch() real pero obtienen respuestas mockeadas. Esto es más realista que mockear fetch directamente. setupServer para Node (Jest), setupWorker para navegador. El lifecycle beforeAll/afterAll gestiona el servidor. server.use() sobrescribe los handlers por test. Las queries findBy (async) esperan a que aparezcan elementos — úsalas para renderizado asíncrono. queryBy devuelve null si no se encuentra (para asertar ausencia). waitFor hace polling de una condición. MSW también puede usarse para mocking en desarrollo y Storybook.

react
import { render, screen, waitFor } from "@testing-library/react";
import { rest } from "msw";
import { setupServer } from "msw/node";
import { UserProfile } from "./UserProfile";

// Mock API with MSW (Mock Service Worker)
const server = setupServer(
  rest.get("/api/users/:id", (req, res, ctx) => {
    return res(ctx.json({ id: 1, name: "Alice" }));
  })
);

beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());

test("displays user after fetch", async () => {
  render(<UserProfile id={1} />);

  // findBy waits for async update
  expect(await screen.findByText("Alice")).toBeInTheDocument();
  expect(screen.queryByText("Loading")).not.toBeInTheDocument();
});

test("shows error on fetch failure", async () => {
  server.use(
    rest.get("/api/users/:id", (req, res, ctx) =>
      res(ctx.status(500))
    )
  );
  render(<UserProfile id={1} />);
  expect(await screen.findByText(/error/i)).toBeInTheDocument();
});

Testear Context y Providers

Crea una utilidad de render personalizada que envuelva los componentes con los providers requeridos (Theme, Auth, Router, etc.). Esto evita repetir la configuración de providers en cada test. Re-exporta las funciones de RTL desde tu archivo test-utils para que los tests importen desde allí. Para testing de router, usa MemoryRouter (no BrowserRouter) con initialEntries para establecer la URL inicial — no se necesita historial real de navegador. Para Redux, envuelve en un Provider de test con un store real o mock. Este patrón mantiene los tests limpios y asegura que todos los componentes tengan su context requerido. Es el setup estándar para cualquier infraestructura de testing de React.

react
// Custom render that wraps with providers
import { render } from "@testing-library/react";
import { ThemeProvider } from "./ThemeProvider";

function customRender(ui, { theme = "light", ...options } = {}) {
  function Wrapper({ children }) {
    return <ThemeProvider initialTheme={theme}>{children}</ThemeProvider>;
  }
  return render(ui, { wrapper: Wrapper, ...options });
}

// Re-export everything
export * from "@testing-library/react";
export { customRender as render };

// In test files, import from your test-utils:
// import { render, screen } from "../test-utils";

test("button uses theme color", () => {
  customRender(<Button>Click</Button>, { theme: "dark" });
  expect(screen.getByRole("button")).toHaveClass("btn-dark");
});

// Testing with router
import { MemoryRouter } from "react-router-dom";
render(
  <MemoryRouter initialEntries={["/users/123"]}>
    <App />
  </MemoryRouter>
);

Testear Eventos e Interacciones

userEvent.setup() crea una instancia de usuario para interacciones realistas: type (carácter por carácter), click, tab, keyboard (con key codes como {Escape}, {Enter}), selectOptions, upload y más. Siempre await las interacciones de usuario — son asíncronas. Testear la navegación por teclado es crucial para accesibilidad. Para testing de formularios, rellena todos los campos y verifica que onSubmit reciba los datos correctos. userEvent es preferido sobre fireEvent porque simula el comportamiento real del navegador (focus, blur, input events en el orden correcto). Testea el flujo completo de usuario, no los event handlers individuales.

react
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";

test("form submission flow", async () => {
  const onSubmit = jest.fn();
  const user = userEvent.setup();
  render(<LoginForm onSubmit={onSubmit} />);

  // Fill form fields
  await user.type(screen.getByLabelText(/email/i), "[email protected]");
  await user.type(screen.getByLabelText(/password/i), "password123");

  // Submit
  await user.click(screen.getByRole("button", { name: /login/i }));

  expect(onSubmit).toHaveBeenCalledWith({
    email: "[email protected]",
    password: "password123",
  });
});

test("keyboard navigation", async () => {
  const user = userEvent.setup();
  render(<Modal />);
  await user.tab(); // focus first element
  expect(screen.getByRole("button", { name: /close/i })).toHaveFocus();
  await user.keyboard("{Escape}"); // press Escape
  expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
});
17

TypeScript + React

Tipado de Props de Componente

TypeScript con React proporciona seguridad de tipos para las props. Usa interfaces o type aliases para las props. Las props opcionales usan ?. Los union types (variant) limitan los valores. React.ReactNode acepta cualquier contenido renderizable (strings, elements, arrays). Extender atributos HTML (React.InputHTMLAttributes) permite que tu componente acepte todos los atributos nativos (placeholder, onChange, etc.) mientras añade props personalizadas. El spread {...rest} pasa los atributos restantes al elemento nativo. Este patrón crea componentes type-safe y flexibles. Siempre exporta los tipos de prop para que los consumidores puedan referenciarlos.

react
// Basic props
interface ButtonProps {
  text: string;
  onClick: () => void;
  variant?: "primary" | "secondary"; // union type
  disabled?: boolean;
}

function Button({ text, onClick, variant = "primary", disabled }: ButtonProps) {
  return (
    <button
      className={`btn btn-${variant}`}
      onClick={onClick}
      disabled={disabled}
    >
      {text}
    </button>
  );
}

// Children prop
interface CardProps {
  title: string;
  children: React.ReactNode; // any renderable content
}

// Extending HTML attributes
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
  label: string;
  error?: string;
}

function Input({ label, error, ...rest }: InputProps) {
  return (
    <label>
      {label}
      <input {...rest} />
      {error && <span className="error">{error}</span>}
    </label>
  );
}

Hooks con TypeScript

TypeScript añade seguridad de tipos a los hooks. useState<T> especifica el tipo de estado; useState<T | null>(null) para estado nullable. useRef<T>(null) tipa el ref — current es T | null. Para useContext, define un tipo de context y lanza un error si es undefined (para que los consumidores obtengan el tipo non-undefined). Para useReducer, tipa la Action como discriminated union — el switch sobre action.type narrow el tipo en cada case, dándote acceso type-safe al payload. Estos patrones eliminan errores de runtime por acceso undefined y payloads de action incorrectos.

react
// useState with types
const [count, setCount] = useState<number>(0);
const [user, setUser] = useState<User | null>(null);
const [items, setItems] = useState<string[]>([]);

// useRef
const inputRef = useRef<HTMLInputElement>(null);
// Access: inputRef.current?.focus()

// useContext
interface ThemeContextType {
  theme: "light" | "dark";
  toggle: () => void;
}
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);

function useTheme() {
  const ctx = useContext(ThemeContext);
  if (!ctx) throw new Error("useTheme must be inside ThemeProvider");
  return ctx; // ctx is now ThemeContextType, not undefined
}

// useReducer
type Action = { type: "increment" } | { type: "set"; value: number };
const [state, dispatch] = useReducer((state: number, action: Action) => {
  switch (action.type) {
    case "increment": return state + 1;
    case "set": return action.value;
  }
}, 0);

Componentes Genéricos

Los componentes y hooks genéricos funcionan con cualquier tipo de dato manteniendo seguridad de tipos. El parámetro de tipo <T> se infiere de la prop items, así que renderItem y keyExtractor reciben automáticamente el tipo correcto. Así es como TypeScript recrea componentes utility genéricos (List, Table, Select) con total seguridad de tipos. Los hooks genéricos (useArray<T>) preservan similarmente los tipos a través de operaciones. La idea clave: TypeScript infiere T del uso, así que los consumidores raramente necesitan especificarlo explícitamente. Este patrón es esencial para construir librerías de componentes reutilizables y type-safe.

react
// Generic component: works with any type
interface ListProps<T> {
  items: T[];
  renderItem: (item: T) => React.ReactNode;
  keyExtractor: (item: T) => string;
}

function List<T>({ items, renderItem, keyExtractor }: ListProps<T>) {
  return (
    <ul>
      {items.map((item) => (
        <li key={keyExtractor(item)}>{renderItem(item)}</li>
      ))}
    </ul>
  );
}

// Usage: TypeScript infers T from items
<List
  items={[{ id: "1", name: "Alice" }, { id: "2", name: "Bob" }]}
  renderItem={(user) => <span>{user.name}</span>}
  keyExtractor={(user) => user.id}
/>

// Generic hook
function useArray<T>(initial: T[]) {
  const [array, setArray] = useState(initial);
  const push = (item: T) => setArray((prev) => [...prev, item]);
  return { array, push, setArray };
}

Tipos de Eventos y Refs

Los tipos de eventos de React son específicos: ChangeEvent para inputs, FormEvent para formularios, MouseEvent para clicks. Cada uno es genérico sobre el tipo de elemento (e.target está correctamente tipado). forwardRef con TypeScript requiere dos parámetros de tipo: el tipo de ref y el tipo de props. forwardRef se necesita cuando un componente debe exponer un ref a un elemento DOM (para focus, medición, etc.). Siempre establece displayName para componentes forwardRef/memo para mejor debugging en DevTools. React 19 permite ref como prop regular, reduciendo la necesidad de forwardRef, pero sigue siendo común en código existente.

react
// Event types
function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
  console.log(e.target.value); // string
}

function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
  e.preventDefault();
  const formData = new FormData(e.currentTarget);
}

function handleClick(e: React.MouseEvent<HTMLButtonElement>) {
  console.log(e.clientX, e.clientY);
}

// forwardRef with TypeScript
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
  label: string;
}

const Input = React.forwardRef<HTMLInputElement, InputProps>(
  ({ label, ...props }, ref) => (
    <label>
      {label}
      <input ref={ref} {...props} />
    </label>
  )
);
Input.displayName = "Input";

// Usage: const ref = useRef<HTMLInputElement>(null);
// <Input ref={ref} label="Email" />

Utility Types para Props

Los utility types de TypeScript son potentes para la composición de props. Pick selecciona props específicas (para componentes subset). Omit excluye props (para reemplazar comportamiento). Partial hace todas las props opcionales (para patrones de default prop). ComponentProps<typeof Component> extrae el tipo de prop de un componente — útil para envolver/extender componentes existentes. Record<K, V> crea un tipo mapeando keys a valores (genial para maps de variant-a-clase). Estas utilidades permiten definiciones de props DRY y type-safe sin repetir interfaces. Domínalas para escribir código mantenible de React + TypeScript.

react
// Pick: select specific props
interface ButtonProps {
  text: string;
  onClick: () => void;
  color: string;
  size: "sm" | "md" | "lg";
}
type IconButtonProps = Pick<ButtonProps, "onClick" | "size"> & {
  icon: React.ReactNode;
};

// Omit: exclude specific props
type LinkButtonProps = Omit<ButtonProps, "onClick"> & {
  href: string;
};

// Partial: all props optional (for defaults)
type DefaultProps = Partial<ButtonProps>;

// ComponentProps: extract props from existing component
type MyButtonProps = React.ComponentProps<typeof Button> & {
  variant?: "custom";
};

// ReturnType: type of a function's return
type User = ReturnType<typeof fetchUser>;

// Record for prop maps
type ButtonVariants = Record<"primary" | "danger" | "ghost", string>;
18

Concurrent Features (useTransition, useDeferredValue)

useTransition

useTransition marca una actualización de estado como no urgente (transition). Las actualizaciones urgentes (valor del input) se renderizan inmediatamente para responsiveness; las actualizaciones no urgentes (filtrar 10,000 items) pueden ser interrumpidas si el usuario escribe de nuevo. isPending indica que la transition está en progreso (muestra un indicador de loading sutil). Esto previene que la UI se congele durante renders costosos. La idea clave: React puede interrumpir y descartar transitions obsoletas, manteniendo la UI responsive. Úsalo para filtrado de búsqueda, switching de tabs y cualquier actualización de estado que dispare renderizado pesado. No envuelvas actualizaciones urgentes (typing, clicking) en transitions.

react
import { useTransition, useState } from "react";

function SearchResults() {
  const [isPending, startTransition] = useTransition();
  const [query, setQuery] = useState("");
  const [results, setResults] = useState([]);

  const handleSearch = (value) => {
    setQuery(value); // urgent: update input immediately
    startTransition(() => {
      // non-urgent: heavy filtering can be interrupted
      const filtered = heavyFilter(allItems, value);
      setResults(filtered);
    });
  };

  return (
    <div>
      <input value={query} onChange={(e) => handleSearch(e.target.value)} />
      {isPending && <span>Updating...</span>}
      <ul>{results.map((r) => <li key={r.id}>{r.name}</li>)}</ul>
    </div>
  );
}

useDeferredValue

useDeferredValue es la contraparte declarativa de useTransition. Devuelve una copia diferida de un valor que se actualiza con menor prioridad. El input se actualiza inmediatamente (urgente); la lista costosa se re-renderiza con el valor diferido (no urgente). React.memo en el componente Results es crucial — previene el re-renderizado en cada pulsación de tecla, solo cuando deferredQuery cambia. isStale (comparando current vs deferred) te permite mostrar un indicador visual (atenuado, spinner). Usa useDeferredValue cuando no puedes controlar la actualización de estado (p. ej., el valor viene de props). Usa useTransition cuando controlas la actualización.

react
function Search() {
  const [query, setQuery] = useState("");
  // query updates immediately; deferredQuery lags behind
  const deferredQuery = useDeferredValue(query);
  const isStale = query !== deferredQuery;

  return (
    <div>
      <input value={query} onChange={(e) => setQuery(e.target.value)} />
      <Results query={deferredQuery} isStale={isStale} />
    </div>
  );
}

// Memoize the expensive component so it only re-renders
// when deferredQuery changes (not on every keystroke)
const Results = React.memo(function Results({ query, isStale }) {
  const items = expensiveSearch(query); // heavy computation
  return (
    <div style={{ opacity: isStale ? 0.5 : 1 }}>
      {items.map((i) => <div key={i.id}>{i.name}</div>)}
    </div>
  );
});

useOptimistic (React 19)

useOptimistic (React 19) implementa optimistic updates — la UI se actualiza inmediatamente con el resultado esperado, luego se reconcilia con la respuesta real del servidor. El estado optimista se muestra durante la operación asíncrona; cuando los datos reales llegan (el componente se re-renderiza con nuevas props), el valor optimista se reemplaza automáticamente. Si la operación falla, la actualización optimista simplemente se revierte en el siguiente render con props sin cambios. Esto elimina la lógica manual de optimistic update (trackear estado pending, revertir en error). Marca los items pending (pending: true) para mostrar indicadores de loading. Perfecto para likes, comments y toggles.

react
import { useOptimistic } from "react";

function ThumbsUp({ likes, addLike }) {
  // Optimistic state: updates immediately, reverts on error
  const [optimisticLikes, addOptimisticLike] = useOptimistic(
    likes,
    (state, newLike) => [...state, newLike]
  );

  const handleClick = async () => {
    const newLike = { id: Date.now(), pending: true };
    addOptimisticLike(newLike); // instant UI update
    try {
      await addLike(newLike); // actual API call
    } catch {
      // Reverts automatically on re-render with real data
    }
  };

  return (
    <div>
      <button onClick={handleClick}>👍 {optimisticLikes.length}</button>
      {optimisticLikes.some((l) => l.pending) && <span>Saving...</span>}
    </div>
  );
}

use (Hook de React 19)

use() es el nuevo hook de React 19 que lee context o promesas. A diferencia de useContext, use() puede llamarse condicionalmente (dentro de if statements, loops) — no tiene la restricción de rules-of-hooks. Para promesas, use() suspende el componente hasta que la promesa se resuelve (requiere un límite de Suspense). La promesa se crea en el padre y se pasa como prop — esto empieza el fetching durante el render (no en useEffect), permitiendo que los waterfalls empiecen antes. La misma promesa puede pasarse a múltiples componentes (deduplicación). use() cierra la brecha entre context síncrono y datos asíncronos.

react
import { use } from "react";

// Read context with use (works in conditions!)
function Theme() {
  // Unlike useContext, use() can be inside conditions
  if (showTheme) {
    const theme = use(ThemeContext); // conditional context!
    return <div style={{ background: theme.color }} />;
  }
  return null;
}

// Read promises with use (Suspense integration)
function UserProfile({ userPromise }) {
  // Suspends until promise resolves
  const user = use(userPromise);
  return <h1>{user.name}</h1>;
}

// Parent passes promise (starts fetching during render)
function App() {
  const userPromise = fetchUser(); // starts immediately
  return (
    <Suspense fallback={<Loading />}>
      <UserProfile userPromise={userPromise} />
    </Suspense>
  );
}

Patrones de Concurrent Rendering

Patrones concurrentes: useTransition para switching de tabs no urgente (mantiene el nav responsive mientras el contenido pesado se renderiza). useSyncExternalStore se suscribe de forma segura a stores externos (browser APIs, Redux, Zustand) en modo concurrente — proporciona una función de snapshot para cliente y servidor (SSR-safe). Nunca uses estado mutable externo directamente en render (riesgo de tearing); siempre ve a través de useSyncExternalStore. Los tres argumentos: subscribe (devuelve cleanup), getSnapshot (valor actual), getServerSnapshot (valor inicial SSR). Esto asegura lecturas consistentes durante concurrent rendering. Librerías como Redux y Zustand usan esto internamente.

react
// Pattern 1: Deferred search with transition
function App() {
  const [tab, setTab] = useState("home");
  const [isPending, startTransition] = useTransition();

  return (
    <>
      <nav>
        <button
          onClick={() => startTransition(() => setTab("analytics"))}
          disabled={isPending}
        >
          {isPending ? "Loading..." : "Analytics"}
        </button>
      </nav>
      {tab === "home" && <Home />}
      {tab === "analytics" && <HeavyAnalytics />}
    </>
  );
}

// Pattern 2: useSyncExternalStore for external state
function useOnlineStatus() {
  return useSyncExternalStore(
    (callback) => {
      window.addEventListener("online", callback);
      window.addEventListener("offline", callback);
      return () => {
        window.removeEventListener("online", callback);
        window.removeEventListener("offline", callback);
      };
    },
    () => navigator.onLine, // client snapshot
    () => true // server snapshot (SSR)
  );
}
19

Context API en Profundidad

createContext y Provider

createContext crea un objeto de context con un valor por defecto usado cuando no se encuentra ningún Provider. La prop value del Provider es consumida por todos los descendientes. Envuelve el valor en useCallback/useMemo para prevenir re-renders innecesarios.

react
const ThemeContext = React.createContext({ theme: 'light', toggle: () => {} });

function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light');
  const toggle = useCallback(() => setTheme((t) => (t === 'light' ? 'dark' : 'light')), []);
  return (
    <ThemeContext.Provider value={{ theme, toggle }}>
      {children}
    </ThemeContext.Provider>
  );
}

useContext

useContext lee el valor del Provider más cercano y re-renderiza el componente cuando ese valor cambia. Anida múltiples Providers para diferentes concerns. Divide los contextos por frecuencia de actualización para rendimiento.

react
function ThemedButton() {
  const { theme, toggle } = useContext(ThemeContext);
  return (
    <button onClick={toggle} style={{ background: theme === 'dark' ? '#333' : '#eee' }}>
      Toggle Theme
    </button>
  );
}

Context con Reducer

Emparejar useReducer con Context crea un store global sin Redux. El reducer centraliza la lógica de estado; el Context distribuye state y dispatch. Los consumidores pueden dispatchar actions sin prop-drilling.

react
const StoreContext = React.createContext(null);

function storeReducer(state, action) {
  switch (action.type) {
    case 'add': return { items: [...state.items, action.item] };
    case 'remove': return { items: state.items.filter((_, i) => i !== action.index) };
    default: return state;
  }
}

function StoreProvider({ children }) {
  const [state, dispatch] = useReducer(storeReducer, { items: [] });
  return <StoreContext.Provider value={{ state, dispatch }}>{children}</StoreContext.Provider>;
}

Optimizar Renders de Context

Cuando state y dispatch viven en el mismo context, cada cambio de estado re-renderiza todos los consumidores. Dividirlos significa que los componentes solo-dispatch nunca se re-renderizan en cambios de estado. dispatch de useReducer es estable.

react
const StateContext = React.createContext(null);
const DispatchContext = React.createContext(null);

function Provider({ children }) {
  const [state, dispatch] = useReducer(reducer, initial);
  return (
    <StateContext.Provider value={state}>
      <DispatchContext.Provider value={dispatch}>
        {children}
      </DispatchContext.Provider>
    </StateContext.Provider>
  );
}

Hook Personalizado para Context

Envolver useContext en un hook personalizado da una API limpia y un error claro cuando falta el Provider. Exporta tanto el Provider como el hook. Esta es la forma recomendada de consumir context.

react
export function useAuth() {
  const ctx = useContext(AuthContext);
  if (!ctx) throw new Error('useAuth must be used within AuthProvider');
  return ctx;
}
20

useReducer

useReducer Básico

useReducer es una alternativa a useState para lógica de estado compleja. El reducer es una función pura: (state, action) => newState. dispatch es estable, así que puedes pasarlo hacia abajo sin preocuparte por re-renders.

react
function reducer(state, action) {
  switch (action.type) {
    case 'increment': return { count: state.count + 1 };
    case 'decrement': return { count: state.count - 1 };
    case 'reset':     return { count: 0 };
    default: throw new Error('Unknown action: ' + action.type);
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, { count: 0 });
  return <button onClick={() => dispatch({ type: 'increment' })}>{state.count}</button>;
}

Inicialización Lazy

El tercer argumento de useReducer es una función init que se ejecuta una vez durante el render inicial. Esto es útil cuando el estado inicial es costoso de calcular o cuando quieres que reset vuelva a un estado calculado.

react
function init(initialCount) {
  return { count: initialCount, history: [] };
}

function reducer(state, action) {
  switch (action.type) {
    case 'increment': return { ...state, count: state.count + 1 };
    case 'reset':     return init(action.payload);
    default: return state;
  }
}

const [state, dispatch] = useReducer(reducer, initialCount, init);

Shape de Estado Complejo

useReducer brilla cuando el estado tiene múltiples campos relacionados. Cada action describe una transición de estado completa, haciendo la lógica más fácil de seguir que llamadas setState dispersas. Mantén el reducer puro.

react
const initialState = { users: [], loading: false, error: null, filter: 'all' };

function reducer(state, action) {
  switch (action.type) {
    case 'fetch-start': return { ...state, loading: true, error: null };
    case 'fetch-success': return { ...state, loading: false, users: action.users };
    case 'fetch-error': return { ...state, loading: false, error: action.error };
    case 'set-filter': return { ...state, filter: action.filter };
    default: return state;
  }
}

Reducer con Context

Combinar useReducer con Context crea un store ligero tipo Redux. El reducer mantiene la lógica; el Context distribuye state y dispatch. Este es el patrón recomendado para estado app-wide en apps medianas.

react
function todoReducer(state, action) {
  switch (action.type) {
    case 'add': return [...state, { id: Date.now(), text: action.text, done: false }];
    case 'toggle': return state.map((t) => t.id === action.id ? { ...t, done: !t.done } : t);
    case 'delete': return state.filter((t) => t.id !== action.id);
    default: return state;
  }
}

export function TodoProvider({ children }) {
  const [todos, dispatch] = useReducer(todoReducer, []);
  return <TodoContext.Provider value={{ todos, dispatch }}>{children}</TodoContext.Provider>;
}

Action Types y Patrones

Define los action types como constantes para evitar typos y habilitar el autocomplete del IDE. El shape de action { type, payload? } es una convención común. Para TypeScript, define una discriminated union de action types.

react
const ACTIONS = { ADD: 'add', UPDATE: 'update', DELETE: 'delete' };

function reducer(state, action) {
  switch (action.type) {
    case ACTIONS.ADD:
      return [...state, { id: action.id, ...action.payload }];
    case ACTIONS.UPDATE:
      return state.map((item) => item.id === action.id ? { ...item, ...action.payload } : item);
    case ACTIONS.DELETE:
      return state.filter((item) => item.id !== action.id);
    default: return state;
  }
}
21

useMemo y useCallback

useMemo

useMemo cachea el resultado de un cálculo y solo recalcula cuando cambian las dependencias. Úsalo para cálculos costosos (sorting, filtrado de arrays grandes). El array de dependencias debe incluir todo lo que el callback usa.

react
function ProductList({ products, filter }) {
  const filtered = useMemo(() => {
    return products.filter((p) => p.category === filter);
  }, [products, filter]);

  const sorted = useMemo(() => [...filtered].sort((a, b) => a.price - b.price), [filtered]);
  return <ul>{sorted.map((p) => <li key={p.id}>{p.name}</li>)}</ul>;
}

useCallback

useCallback memoiza una función para que mantenga la misma identidad entre renders a menos que cambien las dependencias. Esto es crítico cuando se pasan callbacks a hijos memoizados — sin ello, el hijo se re-renderiza cada vez.

react
function Parent() {
  const [count, setCount] = useState(0);
  const handleClick = useCallback(() => setCount((c) => c + 1), []);
  return <MemoizedChild onClick={handleClick} />;
}

React.memo

React.memo envuelve un componente para que solo se re-renderice cuando sus props cambian (comparación superficial). El segundo argumento es un comparator personalizado que devuelve true para saltar el re-renderizado. Combina memo con useCallback/useMemo para las props.

react
const ExpensiveItem = React.memo(function ExpensiveItem({ value, onClick }) {
  return <li onClick={onClick}>{value}</li>;
});

// With custom comparison
const DeepChild = React.memo(
  ({ user }) => <div>{user.name}</div>,
  (prev, next) => prev.user.id === next.user.id
);

Cuándo Memoizar

La memoización tiene un coste que puede exceder los ahorros. Solo memoiza cuando: (1) el cálculo es costoso, (2) el valor se pasa a un hijo memoizado, o (3) el valor se usa como dependencia en useEffect/useMemo/useCallback.

react
// GOOD: expensive computation
const sorted = useMemo(() => heavySort(data), [data]);

// GOOD: callback passed to memoized child
const onSelect = useCallback((id) => setSelected(id), []);

// BAD: cheap operation, no perf issue
const label = useMemo(() => first + ' ' + last, [first, last]);

useMemo para Igualdad Referencial

useMemo asegura que objetos y arrays mantengan la misma referencia entre renders, lo cual importa cuando se usan como dependencias en useEffect/useMemo/useCallback. Sin ello, { q: query } crea un nuevo objeto en cada render.

react
function Search({ query }) {
  const params = useMemo(() => ({ q: query, limit: 10 }), [query]);
  useEffect(() => {
    api.search(params).then(setData);
  }, [params]); // Without useMemo, this fires every render
}
22

Portals y Refs

createPortal

createPortal renderiza children en un nodo DOM fuera del árbol DOM del componente padre (usualmente document.body). Esto es esencial para modales, tooltips y dropdowns que deben escapar de los stacking contexts del padre.

react
import { createPortal } from 'react-dom';

function Modal({ open, onClose, children }) {
  if (!open) return null;
  return createPortal(
    <div className="modal-overlay" onClick={onClose}>
      <div className="modal" onClick={(e) => e.stopPropagation()}>{children}</div>
    </div>,
    document.body
  );
}

useRef

useRef devuelve un objeto mutable cuyo .current persiste entre renders sin causar re-renders. El uso principal es acceder a nodos DOM. También almacena valores mutables que no afectan la UI.

react
function FocusInput() {
  const inputRef = useRef(null);
  const focus = () => inputRef.current?.focus();
  return (
    <>
      <input ref={inputRef} type="text" />
      <button onClick={focus}>Focus</button>
    </>
  );
}

forwardRef

forwardRef permite a un componente padre pasar un ref a través de un componente envolvente a un nodo DOM hijo. Sin ello, React prohíbe pasar ref como prop. El ref es el segundo argumento de la función envuelta.

react
const FancyInput = React.forwardRef(function FancyInput({ label, ...props }, ref) {
  return (
    <label>{label}<input ref={ref} {...props} className="fancy-input" /></label>
  );
});

useImperativeHandle

useImperativeHandle personaliza la instancia expuesta al padre vía ref — en lugar del nodo DOM raw, el padre ve solo los métodos que defines. Úsalo con moderación; prefiere props declarativas cuando sea posible.

react
const VideoPlayer = React.forwardRef(function VideoPlayer(props, ref) {
  const videoRef = useRef(null);
  useImperativeHandle(ref, () => ({
    play: () => videoRef.current?.play(),
    pause: () => videoRef.current?.pause(),
    seek: (time) => { if (videoRef.current) videoRef.current.currentTime = time; },
  }));
  return <video ref={videoRef} src={props.src} />;
});

Refs para Valores Mutables

useRef almacena valores mutables que no deberían disparar re-renders — como IDs de timer, instancias WebSocket o flags de "is mounted". Siempre limpia los side effects en la función de limpieza de useEffect.

react
function Stopwatch() {
  const [seconds, setSeconds] = useState(0);
  const intervalRef = useRef(null);
  const start = () => {
    if (intervalRef.current) return;
    intervalRef.current = setInterval(() => setSeconds((s) => s + 1), 1000);
  };
  const stop = () => { clearInterval(intervalRef.current); intervalRef.current = null; };
  useEffect(() => () => clearInterval(intervalRef.current), []);
  return <>{seconds}<button onClick={start}>Start</button><button onClick={stop}>Stop</button></>;
}
23

Error Boundaries

Error Boundary de Clase

Los error boundaries son componentes de clase que capturan errores en el árbol de componentes hijo durante el renderizado. getDerivedStateFromError actualiza el estado para renderizar el fallback; componentDidCatch registra el error. NO capturan errores en event handlers o código asíncrono.

react
class ErrorBoundary extends React.Component {
  constructor(props) { super(props); this.state = { hasError: false, error: null }; }
  static getDerivedStateFromError(error) { return { hasError: true, error }; }
  componentDidCatch(error, info) { console.error('Caught:', error, info); }
  render() {
    if (this.state.hasError) return this.props.fallback || <h1>Something went wrong.</h1>;
    return this.props.children;
  }
}

Usar Error Boundaries

Coloca los error boundaries estratégicamente para que un cuelgue en una parte de la UI no tire toda la app. Los límites granulares alrededor de widgets permiten que el resto de la app siga funcionando.

react
function App() {
  return (
    <ErrorBoundary fallback={<ErrorPage />}>
      <Header />
      <ErrorBoundary fallback={<SidebarCrash />}><Sidebar /></ErrorBoundary>
      <ErrorBoundary fallback={<ContentCrash />}><MainContent /></ErrorBoundary>
    </ErrorBoundary>
  );
}

Reiniciar Estado de Error

Los error boundaries permanecen en el estado de error hasta que su estado cambia. Proporciona un botón "Try again" que reinicie hasError a false, dejando que React re-renderice los children. Cambiar la prop key también lo reinicia.

react
class ErrorBoundary extends React.Component {
  state = { hasError: false, error: null };
  static getDerivedStateFromError(error) { return { hasError: true, error }; }
  reset = () => this.setState({ hasError: false, error: null });
  render() {
    if (this.state.hasError) return <div><p>Failed.</p><button onClick={this.reset}>Try again</button></div>;
    return this.props.children;
  }
}

Librería react-error-boundary

La librería react-error-boundary proporciona un error boundary pulido y hook-friendly sin escribir una clase. FallbackComponent recibe el error y una función resetErrorBoundary. resetKeys se auto-reinicia cuando esos valores cambian.

react
import { ErrorBoundary } from 'react-error-boundary';

<ErrorBoundary
  FallbackComponent={ErrorFallback}
  onError={(error, info) => logError(error, info)}
  onReset={() => window.location.reload()}
  resetKeys={[location.pathname]}
>
  <Routes />
</ErrorBoundary>

Manejo de Errores Asíncronos

Los error boundaries no capturan errores en promesas, setTimeout o event handlers. Para llevar los errores asíncronos a un boundary, almacena el error en estado y re-lánzalo durante el render. El boundary entonces lo captura.

react
function AsyncComponent() {
  const [state, setState] = useState({ data: null, error: null });
  useEffect(() => {
    let active = true;
    fetchData()
      .then((data) => { if (active) setState({ data, error: null }); })
      .catch((error) => { if (active) setState({ data: null, error }); });
    return () => { active = false; };
  }, []);
  if (state.error) throw state.error;
  if (!state.data) return <Loading />;
  return <div>{state.data}</div>;
}
24

Optimización de Rendimiento

Virtualización

La virtualización renderiza solo las filas visibles de una lista larga, reduciendo dramáticamente los nodos DOM. Esencial para listas de más de 1000 items — sin ello, el navegador se ahoga con decenas de miles de nodos.

react
import { useVirtualizer } from '@tanstack/react-virtual';

function BigList({ items }) {
  const parentRef = useRef(null);
  const virtualizer = useVirtualizer({
    count: items.length,
    getScrollElement: () => parentRef.current,
    estimateSize: () => 50,
  });
  return (
    <div ref={parentRef} style={{ height: 600, overflow: 'auto' }}>
      <div style={{ height: virtualizer.getTotalSize(), position: 'relative' }}>
        {virtualizer.getVirtualItems().map((vi) => (
          <div key={vi.key} style={{ position: 'absolute', top: vi.start, height: vi.size }}>
            {items[vi.index].name}
          </div>
        ))}
      </div>
    </div>
  );
}

Code Splitting

Code splitting divide el bundle en chunks cargados bajo demanda. Los splits más impactantes son a nivel de ruta (cada página es un chunk separado) y widgets pesados (librerías de charts, editores). Mide con bundle analyzers.

react
import { lazy, Suspense } from 'react';
const Admin = lazy(() => import('./Admin'));
const Chart = lazy(() => import('./Chart'));

function Page({ showChart }) {
  return (
    <Suspense fallback={<Skeleton />}>
      {showChart && <Chart data={data} />}
    </Suspense>
  );
}

Profiling con DevTools

El React DevTools Profiler registra los tiempos de render y muestra qué componentes se re-renderizaron y por qué. Busca renders "desperdiciados" donde las props no cambiaron realmente — esos son candidatos para React.memo.

react
// Use React DevTools Profiler to record renders
// Look for:
// - Components rendering too often
// - Long commit phases
// - Wasted renders (props didn't change)

// Wrap expensive renders to find bottlenecks
function MyComponent({ data }) {
  console.time('render');
  const result = heavyCompute(data);
  console.timeEnd('render');
  return <div>{result}</div>;
}

useDeferredValue

useDeferredValue retrasa la actualización de un valor, dejando que las actualizaciones urgentes (typing) ocurran primero. El render costoso usa el valor diferido, así que no bloquea el input. isStale te permite mostrar una pista visual sutil.

react
function Search({ query }) {
  const deferredQuery = useDeferredValue(query);
  const isStale = query !== deferredQuery;
  const results = useMemo(() => expensiveSearch(deferredQuery), [deferredQuery]);
  return <div style={{ opacity: isStale ? 0.7 : 1 }}>{results.map((r) => <div key={r.id}>{r.name}</div>)}</div>;
}

Concurrent Features

Las concurrent features de React 18 mantienen la UI responsive durante trabajo pesado. useTransition y useDeferredValue permiten a React interrumpir renders para manejar input urgente. Automatic batching agrupa múltiples llamadas setState en un re-render.

react
import { useTransition, useDeferredValue } from 'react';

// useTransition: mark updates as non-urgent
const [isPending, startTransition] = useTransition();
const filterResults = (q) => startTransition(() => setResults(search(q)));

// Automatic batching (React 18): state updates in promises batch automatically
fetch('/api').then(() => {
  setLoading(false);   //  \
  setData(data);       //   > single re-render
  setError(null);      //  /
});
25

Testing (React Testing Library)

Render y Query Básicos

render() monta un componente en un DOM falso; screen lo querya. Prefiere las queries por role (getByRole) — reflejan cómo la assistive tech ve la página y refuerzan la accesibilidad. userEvent simula interacciones reales de usuario.

react
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

test('increments on click', async () => {
  const user = userEvent.setup();
  render(<Counter />);
  expect(screen.getByText('Count: 0')).toBeInTheDocument();
  const button = screen.getByRole('button', { name: /increment/i });
  await user.click(button);
  expect(screen.getByText('Count: 1')).toBeInTheDocument();
});

Variantes de Query

getBy aserta que el elemento existe (lanza un error en caso contrario). queryBy es para asertar ausencia (devuelve null). findBy espera a que aparezcan elementos asíncronos. Las variantes All manejan múltiples coincidencias.

react
// getBy: throws if 0 or >1 matches (strict)
screen.getByRole('button', { name: 'Submit' });

// queryBy: returns null if 0 matches (for assertions of absence)
expect(screen.queryByText('Error')).not.toBeInTheDocument();

// findBy: returns a Promise, waits for match (async)
const element = await screen.findByText('Loaded');

// getAllBy: returns array (multiple matches)
const items = screen.getAllByRole('listitem');

Disparar Eventos

userEvent (no el fireEvent de bajo nivel) es la forma recomendada de simular interacciones — dispara todos los eventos que un usuario real haría (focus, input, keydown, click) en el orden correcto. Siempre usa await con los métodos de userEvent.

react
import userEvent from '@testing-library/user-event';

test('form submission', async () => {
  const user = userEvent.setup();
  const onSubmit = vi.fn();
  render(<Form onSubmit={onSubmit} />);
  await user.type(screen.getByLabelText(/email/i), '[email protected]');
  await user.click(screen.getByRole('button', { name: /submit/i }));
  expect(onSubmit).toHaveBeenCalled();
});

waitFor y Async

waitFor hace polling hasta que la aserción pasa o hace timeout. findBy* combina waitFor y getBy para el caso común de "esperar a que esto aparezca". within limita las queries a un elemento específico.

react
import { waitFor, within } from '@testing-library/react';

test('shows data after fetch', async () => {
  render(<UserList />);
  await waitFor(() => {
    const list = screen.getByRole('list');
    expect(within(list).getAllByRole('listitem')).toHaveLength(3);
  });
});

// findBy is often cleaner than waitFor + getBy
test('shows data (cleaner)', async () => {
  render(<UserList />);
  expect(await screen.findAllByRole('listitem')).toHaveLength(3);
});

Mocking y Setup

MSW (Mock Service Worker) intercepta peticiones de red a nivel de service-worker, así que tu código fetch se ejecuta sin modificar. Configura handlers por test, resetea entre tests y cierra después de todo.

react
import { rest } from 'msw';
import { setupServer } from 'msw/node';

const server = setupServer(
  rest.get('/api/user', (req, res, ctx) => res(ctx.json({ name: 'Alice' })))
);

beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());

test('shows user name', async () => {
  render(<UserProfile />);
  expect(await screen.findByText('Alice')).toBeInTheDocument();
});

Was this helpful?

Learning path

Learn from scratch

Learn this language from the ground up with structured lessons.