🖼️ Reutilizando todas las props de <img> en un componente LazyImage
Si quisiéramos usar las mismas propiedades del <img> nativo (como onClick, alt, className, etc.) desde nuestra página en un componente LazyImage, el enfoque ingenuo sería tipar una por una dentro del componente 😵💫.
❌ Enfoque no escalable
type Props = {
image: string;
onClick: () => void;
};
Esto funciona… hasta que necesitas más props 😬
Imagina agregar alt, loading, decoding, sizes, srcSet, onMouseEnter, etc.
👉 Sería insostenible.
🧩 Uso del componente desde la página
{images.map(({ id, url }) => (
<div className="p-4" key={id}>
<LazyImage
image={url}
onClick={() => {
console.log("hey");
}}
/>
</div>
))}
Y el componente:
export function LazyImage({ image, onClick }: Props): JSX.Element {
return (
<img
ref={node}
width={320}
height="auto"
src={src}
className="rounded bg-gray-300"
onClick={onClick}
/>
);
}
⚠️ Problema: cada nueva prop del <img> debe declararse manualmente.
✅ Solución correcta: extender las props nativas del <img>
React ya nos da los tipos correctos para los elementos del DOM 🎉
En este caso: ImgHTMLAttributes<HTMLImageElement>.
🧠 Idea clave
👉 Combinamos nuestras props personalizadas con las props nativas del <img>
✨ Implementación final (escalable y limpia)
import { useRef, useEffect, useState } from "react";
import type { ImgHTMLAttributes } from "react";
type LazyImageProps = {
src: string;
};
type Props = ImgHTMLAttributes<HTMLImageElement> & LazyImageProps;
🧪 Componente LazyImage
export function LazyImage({ src, ...imgProps }: Props): JSX.Element {
const node = useRef<HTMLImageElement>(null);
const [currentSrc, setCurrentSrc] = useState(
"data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzIwIiBoZWlnaHQ9IjMyMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB2ZXJzaW9uPSIxLjEiLz4="
);
useEffect(() => {
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (!entry.isIntersecting || !node.current) return;
setCurrentSrc(src);
});
});
if (node.current) observer.observe(node.current);
return () => observer.disconnect();
}, [src]);
return <img ref={node} src={currentSrc} {...imgProps} />;
}
🧠 ¿Qué logramos con esto?
✅ Beneficios clave
- 🔁 Reutilización total de las props del
<img>
- 🧩 No necesitamos tipar cada atributo manualmente
- 🧼 Código más limpio gracias al spread operator
- 🛡️ Tipado fuerte con TypeScript
- 🚀 Escalable y fácil de mantener
📐 Buenas prácticas de diseño de componentes
🔑 ¿Por qué usar src y no image?
Usar src en lugar de inventar una prop nueva es parte de buenas convenciones de diseño de APIs:
- 🧠 Menos curva de aprendizaje
- 👥 Tus compañeros ya conocen la API
- 🧩 El componente se comporta como un
<img> nativo
- 🧪 Menos documentación extra
📌 Regla de oro:Cuanto más se parezca tu componente a un elemento nativo del DOM, más fácil será usarlo y adoptarlo.
🏁 Conclusión
Con este enfoque creamos un componente que:
- Está por encima del
<img>
- Añade Lazy Loading
- Mantiene toda la compatibilidad nativa
💡 Un ejemplo perfecto de composición + buenas prácticas en React y TypeScript ✨
⚙️ Aclaraciones importantes (TypeScript ≠ Runtime)
🧩 ImgHTMLAttributes<HTMLImageElement> NO es pesado
Una duda común es pensar que al extender:
ImgHTMLAttributes<HTMLImageElement>
estamos “trayendo” todas las props del <img> al componente y que eso puede afectar el rendimiento.
👉 Esto NO sucede.
- 🧠
ImgHTMLAttributes existe solo en TypeScript
- ❌ No se transpila a JavaScript
- ❌ No agrega código al bundle
- ❌ No afecta el HTML final
📌 Costo en runtime: 0
TypeScript usa este tipo solo para validar y dar autocompletado, luego desaparece por completo.
🌀 El spread operator (...imgProps) NO inventa props
Otro miedo frecuente es pensar que:
<img {...imgProps} />
va a generar un <img> enorme con atributos como title, sizes, decoding, etc., aunque no los usemos.
👉 Esto tampoco ocurre ❌
El spread operator:
- ✅ Copia únicamente las props que existen
- ❌ No crea props nuevas
- ❌ No rellena valores por defecto
- ❌ No pasa atributos que nunca fueron enviados
🔍 Ejemplo real
Uso del componente:
<LazyImage
src={url}
onClick={() => console.log("click")}
/>
Dentro del componente:
imgProps = {
onClick: () => console.log("click")
}
📌 Eso es todo lo que se pasa al <img>.
HTML resultante:
<img src="fox.jpg">
(Sin alt, sin title, sin atributos extra).
🧠 Regla mental importante
Los tipos describen lo que ES POSIBLE, no lo que EXISTE
- TypeScript define qué props pueden usarse
- React renderiza solo las props que realmente pasas
- El spread operator solo propaga lo que ya está en el objeto
🧪 Analogía rápida
ImgHTMLAttributes es como un menú 📜:
- El menú tiene muchas opciones
- Pero solo consumes lo que pides
- Nadie te sirve todo automáticamente 😄
🏁 Cierre final
Este patrón:
- ✅ Es el estándar en React moderno
- ✅ Lo usan librerías profesionales (MUI, Radix, Chakra, Next)
- ✅ No afecta performance
- ✅ Mantiene el HTML limpio
- ✅ Mejora enormemente la DX
💡 Usar ImgHTMLAttributes<HTMLImageElement> + spread operator es seguro, escalable y recomendado cuando construimos componentes que envuelven elementos del DOM.