Render props y Children como función

Lectura
25 min~7 min lectura
CONCEPTO CLAVE: El patrón Children as a Function (Children como función) y Render Props son técnicas avanzadas de composición en React que permiten compartir lógica entre componentes de manera flexible y reutilizable, delegando el control del renderizado al componente que los consume.

¿Qué Problema Resuelven estos Patrones?

Imaginemos que tenemos un componente que maneja datos de estado o efectos secundarios, y queremos que diferentes partes de nuestra aplicación utilicen esa lógica de formas completamente distintas. Los patrones de Render Props y Children como Función nos permiten achieved esto sin necesidad de crear múltiples componentes o usar herramientas externas como Context o HOCs complicados.

Estos patrones son especialmente útiles cuando:

  • Necesitamos compartir lógica de estado entre componentes
  • Queremos mantener el control del renderizado en el componente hijo
  • Buscamos una alternativa más flexible a los High Order Components (HOCs)

Children como Función (Children as a Function)

Este patrón consiste en pasar una función como children de un componente. El componente padre entonces invoca esa función, pasándole los datos o métodos que necesita compartir. El componente hijo decide qué hacer con esos datos.

Sintaxis Básica

function Contador({ children }) {
  const [contador, setContador] = useState(0);
  
  return children({
    contador,
    incrementar: () => setContador(c => c + 1),
    decrementar: () => setContador(c => c - 1)
  });
}

// Uso
function App() {
  return (
    <Contador>
      {({ contador, incrementar, decrementar }) => (
        <div>
          <h2>Contador: {contador}</h2>
          <button onClick={incrementar}>+</button>
          <button onClick={decrementar}>-</button>
        </div>
      )}
    </Contador>
  );
}
💡 Nota importante: El componente Contador no renderiza nada por sí mismo. En su lugar, llama a children como una función, pasándole el estado y las funciones. Esto le da total flexibilidad al consumidor sobre cómo renderizar la interfaz.

Render Props

El patrón de Render Props es muy similar, pero en lugar de usar children, usamos una prop específica (generalmente llamada render o component) que recibe una función.

Sintaxis con Render Prop

function Contador({ render }) {
  const [contador, setContador] = useState(0);
  
  return render({
    contador,
    incrementar: () => setContador(c => c + 1),
    decrementar: () => setContador(c => c - 1)
  });
}

// Uso con prop render
function App() {
  return (
    <Contador
      render={({ contador, incrementar, decrementar }) => (
        <div className="tarjeta"
          <p>Valor: {contador}</p>
          <button onClick={incrementar}>+</button>
          <button onClick={decrementar}>-</button>
        </div>
      )}
    />
  );
}
📌 Diferencia clave: Mientras que Children como Función usa la prop especial children, el patrón Render Props usa una prop nombrada. Ambos logran el mismo resultado, pero la elección depende de la legibilidad y convenciones de tu equipo.

Ejemplo Práctico: Componente de Datos

Veamos un ejemplo más completo donde cremos un componente que maneja la solicitud de datos y permite renderizarlos de diferentes formas:

function DataFetcher({ url, children }) {
  const [datos, setDatos] = useState(null);
  const [cargando, setCargando] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    setCargando(true);
    fetch(url)
      .then(res => {
        if (!res.ok) throw new Error('Error en la solicitud');
        return res.json();
      })
      .then(data => {
        setDatos(data);
        setError(null);
      })
      .catch(err => {
        setError(err.message);
        setDatos(null);
      })
      .finally(() => setCargando(false));
  }, [url]);

  return children({ datos, cargando, error });
}

// Ahora podemos usarlo de múltiples formas
function App() {
  return (
    <DataFetcher url="https://api.ejemplo.com/usuarios">
      {({ datos, cargando, error }) => {
        if (cargando) return <p>Cargando...</p>;
        if (error) return <p>Error: {error}</p>;
        return (
          <ul>
            {datos.map(usuario => (
              <li key={usuario.id}>{usuario.nombre}</li>
            ))}
          </ul>
        );
      }}
    </DataFetcher>
  );
}

Casos de Uso Comunes

PatrónUso IdealEjemplo
Children como FunciónComposición flexible, APIs declarativasComponentes de UI con estado interno
Render PropsCuando necesitas múltiples render propsComponentes de animación, datos

Componente de Animación con Render Props

function Animator({ estado, children }) {
  const [visible, setVisible] = useState(false);
  
  useEffect(() => {
    if (estado === 'entrada') {
      setVisible(true);
    } else if (estado === 'salida') {
      setVisible(false);
    }
  }, [estado]);

  return children({ visible });
}

// Uso
function BotonAnimado() {
  return (
    <Animator estado="entrada">
      {({ visible }) => (
        <div className={`fade ${visible ? 'visible' : 'hidden'}`}>
          Contenido animado
        </div>
      )}
    </Animator>
  );
}

Buenas Prácticas

⚠️ Advertencia: Evita anidar muchos niveles de Render Props o Children como Función. Esto puede generar el llamado "Callback Hell" inverso, donde el código se vuelve difícil de leer y mantener. Si necesitas mucha composición, considera usar Context o un sistema de state management.
  1. Mantén la simplicidad: No uses estos patrones cuando un componente simple sea suficiente.
  2. Documenta tus APIs: Asegúrate de que otros desarrolladores entiendan qué datos recibe la función de render.
  3. Evita recrear funciones en cada render: Usa useCallback cuando sea necesario para optimizar el rendimiento.
  4. Considera alternativas modernas: En muchos casos, hooks personalizados como useState, useReducer o Context pueden ser más simples.

Comparación con otras alternativas

Los patrones de Render Props y Children como Función fueron ampliamente usados antes de la introducción de los Hooks en React 16.8. Aunque los Hooks han reducido la necesidad de estos patrones para muchos casos, siguen siendo herramientas válidas para ciertos escenarios de composición.

vs High Order Components (HOCs)

// HOC - Wrappea el componente
const withContador = (Componente) => {
  return function ComponenteEnvuelto(props) {
    const [contador, setContador] = useState(0);
    return (
      <Componente 
        contador={contador} 
        incrementar={() => setContador(c => c + 1)}
        {...props}
      />
    );
  };
};

// Children como Función - Envuelve la lógica
function Contador({ children }) {
  const [contador, setContador] = useState(0);
  return children({ contador, incrementar: () => setContador(c => c + 1) });
}

// Children como Función es más fácil de leer y depurar
💡 Recomendación: Los patrones de Children como Función suelen ser más fáciles de entender que los HOCs porque mantienen el flujo de datos explícito y evitan problemas de collide de props.
Ver más: Ejemplo avanzado con múltiples render props
function ModalManager({ children, renderHeader, renderFooter }) {
  const [isOpen, setIsOpen] = useState(false);

  const abrir = () => setIsOpen(true);
  const cerrar = () => setIsOpen(false);

  if (!isOpen) {
    return <button onClick={abrir}>Abrir Modal</button>;
  }

  return (
    <div className="modal-overlay">
      <div className="modal">
        {renderHeader({ cerrar })}
        <div className="modal-content">
          {children}
        </div>
        {renderFooter({ cerrar })}
      </div>
    </div>
  );
}

// Uso
function App() {
  return (
    <ModalManager
      renderHeader={({ cerrar }) => (
        <div className="header">
          <h2>Título</h2>
          <button onClick={cerrar}>×</button>
        </div>
      )}
      renderFooter={({ cerrar }) => (
        <div className="footer">
          <button onClick={cerrar}>Cancelar</button>
          <button onClick={cerrar}>Aceptar</button>
        </div>
      )}
    >
      <p>Contenido del modal</p>
    </ModalManager>
  );
}

Consideraciones de Rendimiento

Es importante entender que cada vez que el componente padre se re-renderiza, la función de render o children se ejecuta nuevamente. Esto puede causar problemas de rendimiento si la función crea nuevas referencias en cada render.

// ❌ Problema: Nueva función en cada render
function App() {
  return (
    <Contador>
      {({ contador }) => <MostrarContador valor={contador} />}
    </Contador>
  );
}

// ✅ Solución: Usar React.memo o useMemo
const MostrarContador = React.memo(({ valor }) => {
  console.log('Renderizando');
  return <span>{valor}</span>;
});
📌 Recuerda: En la mayoría de los casos, el impacto en el rendimiento será mínimo. Solo optimiza cuando profiler confirme que hay un problema real.

Conclusión

Los patrones de Render Props y Children como Función son herramientas poderosas para compartir lógica y composición en React. Aunque los Hooks han proporcionado alternativas más directas para muchos casos de uso, estos patrones siguen siendo relevantes cuando necesitas:

  • Compartir lógica de estado entre componentes de manera flexible
  • Mantener el control del renderizado fuera del componente que maneja la lógica
  • Crear APIs declarativas y fáciles de usar

Dominar estos patrones te dará una comprensión más profunda de cómo React maneja la composición y te preparará para crear componentes más flexibles y reutilizables.

🧠 Quiz

¿Cuál es la diferencia principal entre el patrón Render Props y Children como Función?

  • A) No hay diferencia, son exactamente lo mismo
  • B) Render Props usa una prop nombrada, mientras que Children como Función usa la prop especial children
  • C) Children como Función solo funciona con componentes funcionales
  • D) Render Props es más rápido que Children como Función
✅ Respuesta correcta: B) La diferencia principal es que Render Props típicamente usa una prop específica (como render o component), mientras que Children como Función utiliza la prop especial children que todo componente React tiene.