TechLead

Patrones de React

Componentes compuestos, render props, HOCs y patrones avanzados de custom hooks

Patrones de Diseño en React

A medida que las aplicaciones React crecen, repetir lógica entre componentes se vuelve un problema. Los patrones de React son soluciones probadas para compartir comportamiento, crear APIs flexibles y mantener el código mantenible.

Compound Components (Componentes Compuestos)

Los componentes compuestos comparten estado implícito entre un grupo de componentes relacionados. Dan control total sobre el renderizado mientras mantienen la lógica encapsulada.

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

const AccordionContext = createContext();

function Accordion({ children, defaultOpen = null }) {
  const [openItem, setOpenItem] = useState(defaultOpen);
  return (
    <AccordionContext.Provider value={{ openItem, setOpenItem }}>
      <div className="border rounded-lg divide-y">{children}</div>
    </AccordionContext.Provider>
  );
}

function AccordionTrigger({ id, children }) {
  const { openItem, setOpenItem } = useContext(AccordionContext);
  return (
    <button onClick={() => setOpenItem(openItem === id ? null : id)}>
      {children} {openItem === id ? '▲' : '▼'}
    </button>
  );
}

function AccordionContent({ id, children }) {
  const { openItem } = useContext(AccordionContext);
  if (openItem !== id) return null;
  return <div className="p-4 bg-gray-50">{children}</div>;
}

Accordion.Trigger = AccordionTrigger;
Accordion.Content = AccordionContent;

Custom Hooks para Lógica Compartida

// useLocalStorage.js — Persistir estado en localStorage
export function useLocalStorage(key, initialValue) {
  const [value, setValue] = useState(() => {
    try {
      const saved = localStorage.getItem(key);
      return saved !== null ? JSON.parse(saved) : initialValue;
    } catch {
      return initialValue;
    }
  });

  useEffect(() => {
    localStorage.setItem(key, JSON.stringify(value));
  }, [key, value]);

  return [value, setValue];
}

// useDebounce.js — Debounce de valores que cambian rápidamente
export function useDebounce(value, delay = 300) {
  const [debounced, setDebounced] = useState(value);

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

  return debounced;
}

Render Props

Un componente recibe una función como prop y la llama con datos:

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

  useEffect(() => {
    const handleMove = (e) => setPosition({ x: e.clientX, y: e.clientY });
    window.addEventListener('mousemove', handleMove);
    return () => window.removeEventListener('mousemove', handleMove);
  }, []);

  return render(position);
}

// Uso
<MousePosition
  render={({ x, y }) => <p>Mouse en ({x}, {y})</p>}
/>

Higher-Order Components (HOC)

function withAuth(WrappedComponent) {
  return function AuthenticatedComponent(props) {
    const { user, loading } = useAuth();
    if (loading) return <LoadingSpinner />;
    if (!user) return <Navigate to="/login" />;
    return <WrappedComponent {...props} user={user} />;
  };
}

const ProtectedDashboard = withAuth(Dashboard);

💡 ¿Cuándo Usar Cada Patrón?

  • Custom Hooks: Primera opción para compartir lógica con estado
  • Compound Components: Construir librerías de componentes con APIs flexibles
  • Render Props: Cuando un hook no es suficiente y necesitas flexibilidad de renderizado
  • HOCs: Preocupaciones transversales como guardias de autenticación