¿Te has preguntado por qué algunos componentes de React son fáciles de mantener mientras que otros se convierten en un dolor de cabeza? Los patrones de diseño han sido la solución a este problema.

Imagina que estás construyendo una app y necesitas compartir lógica entre varios componentes. ¿Usas un HOC? ¿Render Props? ¿O mejor los modernos Hooks?

Hoy vamos a desmenuzar tres que, honestamente, deberías tener en tu arsenal: Higher-Order Components (HOC), Render Props y Hooks.

¿Por qué son importantes los patrones de diseño en React?

Antes de sumergirnos en cada patrón, es importante entender por qué existen.

React se basa en la composición de componentes, y a medida que nuestras aplicaciones crecen, nos encontramos con la necesidad de compartir lógica entre componentes sin repetir código. Los patrones de diseño nos proporcionan soluciones probadas para estos desafíos comunes.

Higher-Order Components (HOC): El patrón clásico

Un Higher-Order Component es una función que toma un componente y retorna un nuevo componente con funcionalidad adicional. Piensa en los HOC como decoradores que envuelven tus componentes para añadirles superpoderes.

¿Cómo funcionan los HOC?

Los HOC son una técnica avanzada en React para reutilizar la lógica de componentes. No forman parte de la API de React en sí, sino que emergen de su naturaleza composicional. Mientras que un componente transforma props en UI, un HOC transforma un componente en otro componente.

Aquí tienes un ejemplo básico de un HOC:

// HOC que añade funcionalidad de autenticación
function withAuthentication(Component) {
  return function AuthenticatedComponent(props) {
    const [isAuthenticated, setIsAuthenticated] = useState(false);
    
    useEffect(() => {
      // Lógica de autenticación
      checkAuth().then(setIsAuthenticated);
    }, []);
    
    if (!isAuthenticated) {
      return <div>Por favor, inicia sesión</div>;
    }
    
    return <Component {...props} isAuthenticated={isAuthenticated} />;
  };
}

// Uso del HOC
const ProtectedDashboard = withAuthentication(Dashboard);

Cuando usar HOC

  • Cuando necesitas aplicar la misma lógica a múltiples componentes
  • Para integrar con bibliotecas de terceros que usan este patrón
  • Cuando trabajas en código legacy que ya usa HOC

Ventajas de los HOC

  • Reutilización de lógica: Puedes usar la misma lógica a varios componentes sin duplicar código
  • Separación de concerns: Mantiene la lógica de negocio separada de la presentación
  • Composición: Puedes componer múltiples HOC para añadir diferentes funcionalidades

Desventajas de los HOC

  • Wrapper Hell: Anidar múltiples HOC puede resultar en una jerarquía de componentes difícil de debuggear
  • Colisión de props: Diferentes HOC pueden sobrescribir props con el mismo nombre
  • Complejidad: Pueden hacer que el código sea más difícil de entender para desarrolladores menos experimentados
Diagrama mostrando cómo un HOC envuelve un componente y le añade funcionalidad

Render Props: Compartiendo Código con Funciones

El patrón Render Props es una técnica para compartir código entre componentes usando una prop cuyo valor es una función. Un componente con una render prop toma una función que retorna un elemento React y la llama en lugar de implementar su propia lógica de renderizado.

Implementando Render Props

Es especialmente útil cuando quieres compartir comportamiento, pero dar control total sobre cómo se renderiza la UI. Veamos un ejemplo:

// Componente con Render Prop
function MouseTracker({ render }) {
  const [position, setPosition] = useState({ x: 0, y: 0 });
  
  const handleMouseMove = (event) => {
    setPosition({
      x: event.clientX,
      y: event.clientY
    });
  };
  
  return (
    <div onMouseMove={handleMouseMove}>
      {render(position)}
    </div>
  );
}

// Uso del componente
function App() {
  return (
    <MouseTracker 
      render={({ x, y }) => (
        <h1>La posición del mouse es: {x}, {y}</h1>
      )}
    />
  );
}

Cuando usar Render Props

  • Cuando necesitas máxima flexibilidad en cómo se renderiza la UI
  • Para bibliotecas que exponen funcionalidad a otros desarrolladores
  • Cuando el consumer necesita control total sobre la renderización

Ventajas de Render Props

  • Flexibilidad: El componente que consume la render prop tiene control total sobre cómo se renderiza
  • Claridad: Es explícito sobre qué datos se están compartiendo
  • No hay colisión de props: Los datos se pasan directamente a través de la función

Desventajas de Render Props

  • Callback Hell: Anidar múltiples render props puede crear un código difícil de leer
  • Verbosidad: Requiere más código boilerplate que otros patrones
  • Performance: Crear funciones inline en cada render puede afectar el rendimiento
El diagrama ilustra cómo un componente con Render Props mantiene su estado interno (la posición del mouse), actualiza ese estado basándose en eventos, y luego pasa los datos a través de una función render proporcionada por el componente padre. El padre tiene control total sobre cómo renderizar esos datos.

React Hooks: La solución moderna

Los Hooks te permiten usar estado y otras características de React sin escribir clases. Han cambiado la forma en que escribimos componentes React y, en muchos casos, han reemplazado tanto a los HOC como a las Render Props.

Los Hooks más comunes

React proporciona varios Hooks integrados, pero los más utilizados son:

  • useState: Para manejar estado local en componentes funcionales
  • useEffect: Para manejar efectos secundarios (similar a los métodos de ciclo de vida)
  • useContext: Para acceder al contexto de React
  • useCallback y useMemo: Para optimización de rendimiento

Creando Custom Hooks

Una de las características más poderosas de los Hooks es la capacidad de crear tus propios Hooks personalizados. Esto te permite extraer lógica de componentes en funciones reutilizables:

// Custom Hook para tracking del mouse
function useMousePosition() {
  const [position, setPosition] = useState({ x: 0, y: 0 });
  
  useEffect(() => {
    const handleMouseMove = (event) => {
      setPosition({
        x: event.clientX,
        y: event.clientY
      });
    };
    
    window.addEventListener('mousemove', handleMouseMove);
    
    return () => {
      window.removeEventListener('mousemove', handleMouseMove);
    };
  }, []);
  
  return position;
}

// Uso del Custom Hook
function MouseComponent() {
  const { x, y } = useMousePosition();
  
  return (
    <div>
      <h1>La posición del mouse es: {x}, {y}</h1>
    </div>
  );
}

Cuando usar los Hooks

  • Para nuevos proyectos o componentes (es el enfoque recomendado actualmente)
  • Cuando quieres código más simple y mantenible
  • Para compartir lógica sin afectar la jerarquía de componentes
  • Cuando necesitas manejar estado y efectos en componentes funcionales

Ventajas de los Hooks

  • Simplicidad: El código es más legible y fácil de entender
  • Reutilización: Los Custom Hooks permiten compartir lógica sin cambiar la jerarquía de componentes
  • No más clases: Puedes usar características de React sin escribir componentes de clase
  • Mejor organización: Puedes agrupar código relacionado en lugar de dividirlo por métodos de ciclo de vida
  • Composición: Es fácil componer múltiples Hooks sin crear wrapper hell

Reglas de los Hooks

Para usar Hooks correctamente, debes seguir dos reglas fundamentales:

  1. Solo llama Hooks en el nivel superior: No llames Hooks dentro de loops, condiciones o funciones anidadas
  2. Solo llama Hooks desde funciones de React: Llámalos desde componentes funcionales o desde Custom Hooks

Veamos una comparación visual de un componente de clase vs un componente funcional con Hooks

graph LR
    subgraph "Componente de Clase (Antes de Hooks)"
        A["class MouseTracker extends React.Component"] --> B["constructor(props)"]
        B --> C["this.state = {x: 0, y: 0}"]
        C --> D["componentDidMount()"]
        D --> E["window.addEventListener('mousemove', this.handleMouseMove)"]
        E --> F["handleMouseMove(event)"]
        F --> G["this.setState({x: event.clientX, y: event.clientY})"]
        G --> H["render()"]
        H --> I["return <div>{this.state.x}, {this.state.y}</div>"]
        I --> J["componentWillUnmount()"]
        J --> K["window.removeEventListener('mousemove', this.handleMouseMove)"]
    end
    
    style A fill:#ff6b6b,stroke:#333,stroke-width:2px,color:#fff
    style C fill:#ffd93d,stroke:#333,stroke-width:2px
    style G fill:#ffd93d,stroke:#333,stroke-width:2px,color:#000
    style I fill:#6bcf7f,stroke:#333,stroke-width:2px,color:#000
graph LR
    subgraph "Componente Funcional con Hooks (Moderno)"
        L["function MouseTracker()"] --> M["const [position, setPosition] = useState({x: 0, y: 0})"]
        M --> N["useEffect(() => {...})"]
        N --> O["window.addEventListener('mousemove', handleMouseMove)"]
        O --> P["handleMouseMove actualiza position"]
        P --> Q["return () => window.removeEventListener(...)"]
        Q --> R["return <div>{position.x}, {position.y}</div>"]
    end
    
    style L fill:#61dafb,stroke:#333,stroke-width:2px,color:#000
    style M fill:#98fb98,stroke:#333,stroke-width:2px,color:#000
    style N fill:#98fb98,stroke:#333,stroke-width:2px,color:#000
    style R fill:#6bcf7f,stroke:#333,stroke-width:2px,color:#000

Ejemplo práctico: Implementando los tres patrones

Para ilustrar mejor las diferencias, implementemos la misma funcionalidad (un formulario de autenticación) usando los tres patrones:

Versión con HOC

function withFormValidation(Component) {
  return function ValidatedComponent(props) {
    const [errors, setErrors] = useState({});
    
    const validate = (values) => {
      const newErrors = {};
      if (!values.email) newErrors.email = 'Email requerido';
      if (!values.password) newErrors.password = 'Password requerido';
      setErrors(newErrors);
      return Object.keys(newErrors).length === 0;
    };
    
    return <Component {...props} validate={validate} errors={errors} />;
  };
}

const LoginForm = withFormValidation(({ validate, errors }) => {
  // Implementación del formulario
});

Versión con Render Props

function FormValidation({ children }) {
  const [errors, setErrors] = useState({});
  
  const validate = (values) => {
    const newErrors = {};
    if (!values.email) newErrors.email = 'Email requerido';
    if (!values.password) newErrors.password = 'Password requerido';
    setErrors(newErrors);
    return Object.keys(newErrors).length === 0;
  };
  
  return children({ validate, errors });
}

function LoginForm() {
  return (
    <FormValidation>
      {({ validate, errors }) => (
        // Implementación del formulario
      )}
    </FormValidation>
  );
}

Versión con Hooks

function useFormValidation() {
  const [errors, setErrors] = useState({});
  
  const validate = (values) => {
    const newErrors = {};
    if (!values.email) newErrors.email = 'Email requerido';
    if (!values.password) newErrors.password = 'Password requerido';
    setErrors(newErrors);
    return Object.keys(newErrors).length === 0;
  };
  
  return { validate, errors };
}

function LoginForm() {
  const { validate, errors } = useFormValidation();
  // Implementación del formulario
}

Tabla comparativa: mostrando las características, ventajas y desventajas de cada patrón

CaracterísticaHOC (Higher-Order Components)Render PropsHooks
SintaxisFunción que toma un componente y devuelve un nuevo componenteComponente que usa una función como prop para compartir lógicaFunciones especiales que se llaman dentro de componentes funcionales
Reutilización de lógicaAlta – Envuelve componentes con lógica compartidaAlta – Proporciona lógica a través de una función renderMuy alta – Custom Hooks pueden extraer y compartir lógica fácilmente
Legibilidad del códigoPuede disminuir con múltiples HOC anidados (wrapper hell)Puede volverse complejo con anidación profundaExcelente – Código más limpio y lineal
DebuggingDifícil – Múltiples capas de componentes en DevToolsMedio – Puede ser confuso con callbacks anidadosFácil – Estructura más plana y clara
Colisión de propsPosible – HOC pueden sobrescribir props accidentalmenteMínimo – Control explícito sobre qué se pasaNo existe – Cada Hook maneja su propio estado
Flexibilidad de renderizadoBaja – La UI está predefinida por el HOCMuy alta – Control total sobre cómo se renderizaAlta – Separa lógica de presentación completamente
ComposiciónRequiere anidar múltiples funciones HOCPuede crear «pirámide de la perdición» con múltiples render propsNatural – Múltiples Hooks se usan directamente sin anidación
Tipado (TypeScript)Complejo – Requiere tipos genéricos avanzadosMedio – Necesita tipar correctamente la función renderSimple – Los tipos se infieren fácilmente
RendimientoPuede crear componentes adicionales innecesariosPuede causar re-renders si no se optimiza correctamenteOptimizable con useMemo y useCallback
Curva de aprendizajeAlta – Concepto de programación funcional avanzadoMedia – Requiere entender callbacks y closuresBaja a media – Sintaxis más intuitiva
Soporte oficialPatrón válido pero no recomendado para código nuevoPatrón válido pero menos usado desde HooksEnfoque recomendado oficialmente por React
Casos de uso idealesIntegración con librerías legacy, código existenteLibrerías que necesitan máxima flexibilidad de renderizadoTodo desarrollo nuevo, manejo de estado y efectos
Ventajas principales✅ Bueno para añadir comportamiento a componentes existentes
✅ Patrón bien establecido en ecosistema React
✅ Útil para integración con Redux, Router, etc.
✅ Máxima flexibilidad en la UI
✅ Evita colisiones de props
✅ Útil para librerías públicas
✅ Código más limpio y conciso
✅ Reutilización sin modificar jerarquía
✅ Mejor experiencia de desarrollo
✅ Testing más simple
Desventajas principales❌ Wrapper hell con múltiples HOC
❌ Dificulta debugging
❌ Posibles conflictos de nombres de props
❌ Complejidad adicional
❌ Puede crear código verboso
❌ Pirámide de callbacks anidados
❌ Puede afectar rendimiento sin optimización
❌ Sintaxis menos intuitiva
❌ Reglas estrictas que seguir
❌ No disponible en componentes de clase
❌ Requiere React 16.8+
❌ Curva de aprendizaje inicial para efectos
Ejemplo de usowithAuth(Component)<Mouse render={(pos) => ...}/>;const [state, setState] = useState()
Estado actual🟡 Mantenimiento – Usar solo si es necesario🟡 Nicho – Casos específicos de uso🟢 Recomendado – Estándar actual de React

Errores Comunes que Debes Evitar

1: Usar HOC cuando Hooks es suficiente

Muchos desarrolladores usan HOC por costumbre, cuando un simple custom Hook haría el trabajo mejor.

❌ Mal: Crear un HOC complejo para compartir un estado simple

// HOC complejo para algo simple
const withCounter = (Component) => {
  return class extends React.Component {
    constructor(props) {
      super(props);
      this.state = { count: 0 };
    }
    
    increment = () => {
      this.setState({ count: this.state.count + 1 });
    }
    
    render() {
      return (
        <Component 
          count={this.state.count}
          increment={this.increment}
          {...this.props}
        />
      );
    }
  };
};

// Usar el HOC
const MyComponent = ({ count, increment }) => (
  <div>
    <p>Count: {count}</p>
    <button onClick={increment}>Incrementar</button>
  </div>
);

export default withCounter(MyComponent);

✅ Bien: Crear un custom Hook de 10 líneas

// Custom Hook de 10 líneas
const useCounter = (initialValue = 0) => {
  const [count, setCount] = useState(initialValue);
  
  const increment = useCallback(() => {
    setCount(prev => prev + 1);
  }, []);
  
  return { count, increment };
};

// Usar el Hook directamente
const MyComponent = () => {
  const { count, increment } = useCounter(0);
  
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={increment}>Incrementar</button>
    </div>
  );
};

2: Callback Hell con Render Props

Anidar múltiples render props puede crear código ilegible.

// Múltiples render props anidados = código ilegible
const UserProfile = () => (
  <AuthProvider>
    {(auth) => (
      <ThemeProvider>
        {(theme) => (
          <LanguageProvider>
            {(language) => (
              <DataProvider>
                {(data) => (
                  <div style={{ color: theme.textColor }}>
                    {auth.isAuthenticated ? (
                      <h1>{language.greeting}, {data.userName}</h1>
                    ) : (
                      <p>{language.pleaseLogin}</p>
                    )}
                  </div>
                )}
              </DataProvider>
            )}
          </LanguageProvider>
        )}
      </ThemeProvider>
    )}
  </AuthProvider>
);

export default UserProfile;

Solución: Si necesitas anidar más de 2 render props, considera refactorizar a Hooks.

// Custom Hooks para cada contexto
const useAuth = () => useContext(AuthContext);
const useTheme = () => useContext(ThemeContext);
const useLanguage = () => useContext(LanguageContext);
const useData = () => useContext(DataContext);

// Componente limpio y plano
const UserProfile = () => {
  const { isAuthenticated } = useAuth();
  const { textColor } = useTheme();
  const { greeting, pleaseLogin } = useLanguage();
  const { userName } = useData();
  
  return (
    <div style={{ color: textColor }}>
      {isAuthenticated ? (
        <h1>{greeting}, {userName}</h1>
      ) : (
        <p>{pleaseLogin}</p>
      )}
    </div>
  );
};

export default UserProfile;

3: No memorizar en Hooks

Olvidar usar useCallback y useMemo puede causar re-renders innecesarios.

// Cada render crea nuevas funciones y objetos
const SearchComponent = ({ onSearch }) => {
  const [query, setQuery] = useState('');
  const [filters, setFilters] = useState({ category: '', minPrice: 0 });
  
  // ❌ Nueva función en cada render
  const handleSearch = () => {
    onSearch(query, filters);
  };
  
  // ❌ Nuevo objeto en cada render
  const searchConfig = {
    debounce: 300,
    caseSensitive: false
  };
  
  return (
    <div>
      <input 
        value={query} 
        onChange={(e) => setQuery(e.target.value)} 
      />
      <SearchButton 
        onClick={handleSearch} 
        config={searchConfig} 
      />
    </div>
  );
};

// Este componente se re-renderiza innecesariamente
const SearchButton = React.memo(({ onClick, config }) => {
  console.log('SearchButton renderizado');
  return <button onClick={onClick}>Buscar</button>;
});

Regla: Si pasas una función u objeto como prop, probablemente necesitas memorizarlo.

// Funciones y objetos memorizados
const SearchComponent = ({ onSearch }) => {
  const [query, setQuery] = useState('');
  const [filters, setFilters] = useState({ category: '', minPrice: 0 });
  
  // ✅ Función memoizada con useCallback
  const handleSearch = useCallback(() => {
    onSearch(query, filters);
  }, [query, filters, onSearch]);
  
  // ✅ Objeto memoizado con useMemo
  const searchConfig = useMemo(() => ({
    debounce: 300,
    caseSensitive: false
  }), []); // Dependencias vacías porque no cambia
  
  // ✅ Callback memoizado para el input
  const handleInputChange = useCallback((e) => {
    setQuery(e.target.value);
  }, []);
  
  return (
    <div>
      <input 
        value={query} 
        onChange={handleInputChange} 
      />
      <SearchButton 
        onClick={handleSearch} 
        config={searchConfig} 
      />
    </div>
  );
};

// Ahora solo se re-renderiza cuando realmente cambian las props
const SearchButton = React.memo(({ onClick, config }) => {
  console.log('SearchButton renderizado');
  return <button onClick={onClick}>Buscar</button>;
});

Regla práctica de memoización:

  • useCallback: Para funciones que pasas como props
  • useMemo: Para objetos/arrays que pasas como props o cálculos costosos
  • React.memo: Para componentes que reciben las props memorizadas

⚠️ Advertencia: No memoices todo. Solo hazlo cuando:

  1. El componente hijo usa React.memo
  2. Hay cálculos costosos
  3. Las props causan re-renders innecesarios comprobados con profiler

Mejores prácticas y recomendaciones

Basándonos en la experiencia de la comunidad React y las recomendaciones oficiales, aquí están algunas mejores prácticas:

Para proyectos nuevos

  • Prioriza los Hooks: Son la solución moderna y recomendada por el equipo de React
  • Crea Custom Hooks: Extrae lógica reutilizable en Custom Hooks bien nombrados
  • Mantén los Hooks simples: Un Hook debe hacer una cosa y hacerla bien

Para proyectos existentes

  • No refactorices todo de inmediato: Los HOC y Render Props siguen siendo válidos
  • Migra gradualmente: Convierte componentes a Hooks cuando los modifiques
  • Mantén la consistencia: Usa el mismo patrón en componentes relacionados

Consideraciones de rendimiento

  • Usa useMemo y useCallback para optimizar Hooks cuando sea necesario
  • Evita crear funciones inline en render props cuando el rendimiento sea crítico
  • Considera el impacto en el árbol de componentes al usar HOC

Recursos adicionales para profundizar

Si quieres seguir aprendizando sobre estos patrones, aquí tienes algunos recursos valiosos:

Conclusión: Eligiendo el patrón correcto

Ahora que conoces los tres patrones principales de React, es momento de aplicarlos.

Desafío de hoy:

  1. Abre uno de tus proyectos React actuales
  2. Identifica un componente que repite lógica
  3. Refactorízalo usando el patrón más apropiado

📬 ¿Te gustó este contenido? Suscríbete a mi canal de WhatsApp para más contenido de React, JavaScript y desarrollo web cada semana.


Avatar de darkusphantom

👉 Únete a mi Canal de WhatsApp para más recursos

👉 Sigueme en mis redes sociales para más contenido de programación y productividad


🚀 ¡Hackea tu productividad con IA!

Únete a mi comunidad de +100 artículos y descarga gratis mis 12 prompts maestros para reventar el semestre. Recibe tips de Frontend, Python y recursos que solo comparto por correo.

Prometo no llenar tu bandeja. Solo hacks útiles una vez al mes.

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *