You don't have access to this class

Keep learning! Join and start boosting your career

Aprovecha el precio especial y haz tu profesión a prueba de IA

Antes: $249

Currency
$209
Suscríbete

Termina en:

0 Días
19 Hrs
50 Min
15 Seg

Reto: sigamos extendiendo el DOM

12/16
Resources

What are new component properties in React?

When it comes to developing components in React, extending and customizing functionality often plays a crucial role. This article explores the scenario where we introduce a new property in our component, called onLazyLoad, which adds a function ready to run when an image is loaded into the viewport. This concept brings exciting challenges and learning opportunities in JavaScript and TypeScript.

How is the onLazyLoad functionality implemented?

This challenge is to incorporate specific functionality into our component. OnLazyLoad is a function that is triggered when an image is loaded, giving us the power to execute code only when the image is visible and loaded in the browser.

  • Callback: OnLazyLoad must be written as a callback without return (void function in TypeScript).
  • Single argument: This callback receives as a single argument the node of the loaded image.

This allows our teams to interact with the image once it is fully visible and operational, optimizing performance and user experience.

interface LazyImageProps { onLazyLoad?: (imgNode: HTMLImageElement) => void;}

How to ensure that onLazyLoad is executed only once in React?

The second challenge stems from making sure that onLazyLoad is only executed once. In React, it can be common for certain callbacks to be triggered multiple times for various reasons, such as multiple renders. However, we want to avoid repeated execution that can lead to unexpected behavior.

  • Control single execution: It is essential, through logic in React, to ensure that onLazyLoad is not executed multiple times. This can be controlled using side-effects(useEffect) with the appropriate dependency.
import React, { useState, useEffect } from 'react';
const LazyImage = ({ onLazyLoad }) => { const [isLoaded, setIsLoaded] = useState(false);
 useEffect(() => { if (isLoaded && onLazyLoad) { onLazyLoad(); } } }, [isLoaded, onLazyLoad]);
 return <img onLoad={() => setIsLoaded(true)}  src="path/to/image"/>;};

What are the advantages of this approach?

Implementing these functionalities allows us to:

  1. Improve efficiency: By limiting execution to a single execution, memory usage and browser performance is optimized.
  2. Understand React more deeply: Doing research on why React sometimes allows functions to execute multiple times can provide a more detailed understanding of its internal processes.
  3. Implement best practices with TypeScript: Forcing explicit types ensures fewer data type-related errors when compiling code, contributing to safer and more robust development.

These steps, along with additional resources and future classes on advanced TypeScript configurations, can really help elevate your application development skills using React and TypeScript. Keep exploring and learning, challenges become opportunities when you dare to investigate them thoroughly.

Contributions 15

Questions 3

Sort by:

Want to see more contributions, questions and answers from the community?

Estuvo genial el reto, aprendí mucho de useEffect leyendo la documentación oficial de cómo remover las dependencias de useEffect buscando una solución al bug del segundo reto.
.
En resumen, aprendí que el linter siempre estará pendiente de que agreguemos en el array de dependencias de useEffect todos los valores reactivos que usemos dentro del mismo, como props o estados. Como onLazyLoad es una función dentro de los props del componente LazyImage, React lo trata como un valor reactivo. Sin embargo, no queremos agregar esta función en las dependencias de useEffect, porque lo único que nos interesa de onLazyLoad es obtener sus datos, más no ejecutar el callback de useEffect cada vez que onLazyLoad cambie.
.
La mejor solución en la documentación es utilizar el hook (en fase experimental a la fecha de escribir este comentario 15/03/23) useEffectEvent. Este hook le indica a useEffect que vamos a utilizar un valor reactivo dentro del mismo, pero no queremos que ‘reaccione’ a sus cambios, sino que solamente lo use para lectura. Este hook al estar en fase experimental, preferí no usarlo pero está bastante interesante para cuando salga.
.
Al final, opté por la opción que React no recomienda, que es desactivar el linter. Como en todo, cada cosa a su momento, y en este caso desactivar el linter es la opción que mejor se adapta a mi código porque solamente quiero leer lo que me arroja onLazyLoad, más no reaccionar a sus cambios. Es de esos momentos cuando la solución menos recomendada resulta ser la más óptima para un caso específica, o por lo menos la más sencilla según yo

interface LazyImageProps extends ImgHTMLAttributes<HTMLImageElement> {
  src: string
  alt: string,
	// especificamos que onLazyLoad es un valor opcional de tipo void que aceptará un argumento de tipo HTMLImageElement
  onLazyLoad?: (node: HTMLImageElement) => void
}

export default function LazyImage({src, alt, onLazyLoad, ...imgOptionalAttrs}: LazyImageProps) {
  
  const node = useRef<HTMLImageElement>(null)
  
  const [currentSrc, setCurrentSrc] = useState(defaultImg)
  
  useEffect(() => {
    const intersectionObserver = new IntersectionObserver(entries => {
      entries.forEach(entry => {
        if (entry.isIntersecting) {
          setCurrentSrc(src)
					// ya que onLazyLoad es un valor opcional, validamos que sea verdadero y lo ejecutamos pasando el target de la entrada como argumento el cual contiene todos los datos del elemento img
          onLazyLoad && onLazyLoad(entry.target as HTMLImageElement)
					// le digo al intersectionObserver que deje de observar a img luego de entrar al viewport y ejecutar el código de arriba
          intersectionObserver.unobserve(entry.target)
        }
      })
    })
    
    node.current && intersectionObserver.observe(node.current)
    return () => { intersectionObserver.disconnect() }
	// DANGER ZONE: le digo al linter que ignore la línea de abajo
  // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [src])

  return (
    <img ref={node} src={currentSrc} alt={alt} {...imgOptionalAttrs} />
  )
}

Bueno no entendí el reto my bien, lo unico que pude enteder es que tipo de dato era la funcion onLazyLoad la cual logré, más allá de eso no entendí mucho los retos. Aquí dejo la primera parte del primer reto, OJO, NO ESTÁ COMPLETO

Código

type LazyImageProps = {
  src: string;
  onLazyLoad?: ()=> void 
};

Mi solucion

interface Props extends ImgHTMLAttributes<HTMLImageElement> {
    src: string,
    onLazyLoad?: (node: HTMLImageElement) => void
}
useEffect(() => {
        const observer = new IntersectionObserver((entries) => {
            entries.forEach((entry) => {
                if(entry.isIntersecting) {
                    console.log("Hey")
                    setCurrentSrc(src)

                    if(node.current) {
                        onLazyLoad && onLazyLoad(node.current)
                        observer.disconnect()
                    }
                }
            })
        })

        if(node.current) {
            observer.observe(node.current)
        }

        return () => {
            observer.disconnect()
        }
}, [src, onLazyLoad])

Yo lo solucione un poco diferente sin pasar el nodo y usando prevState

Comparto mi solución al reto:

// LazyLoad.tsx

  1. definir y typar onLazyLoad como una función opcional y que recibe un parámetro que es un elemento HTMLImageElement o null y que retorna vacío. (Se añade el tipo null ya que la referencia de la imagen con useRef inicia con un valor inicial de null).

  2. implementar el uso de la nueva prop onLazyLoad. Al ser una propiedad opcional, typescript se quejará si queremos utilizar sin antes validar que realmente sea una función y no un valor undefined. Por lo que se añade una validación

// page.tsx

  1. Definimos la función onLazyLoad y para tiparla, en mi caso decidí reutilizar el tipo que ya definimos en el componente LazyLoad.tsx, por lo que solo lo importo y podemos typar la función con el typo que acabamos de definir y tendremos todas las ventajas que typescript da.

Hay un detalle y es que si en el componente LazyLoad.tsx utilizamos la función onLazyLoad dentro del useEffect. Habrá que añadirla en el arreglo de dependencias, ya que es un valor externo que podría cambiar.
Si hacemos esto en nuestro ejemplo la aplicación funciona correctamente. pero en realidad dentro el useEffect se estará ejecutando cada que haya un rerender del componente (en nuestro caso, cada que añadimos una imagen), ya que la referencia de onLazyLoad cambia en cada rerender. Esto podría ocasionar comportamientos inesperados o loops infinitos dependiendo de si llegásemos a mutar ese useEffect.

Una solución a eso sería que utilizáramos el hook useCallback al definir la función en el page.tsx. Pero esa no sería una buena implementación ya que estaríamos obligando al desarrollador a usar un useCallback cada que quiera utilizar el prop onLazyLoad.

La solución que yo implemente para eso fue usar el hook useRef dentro el LazyImage.tsx para almacenar una referencia de la funcion onLazyLoad. y utilizar esa referencia en la implemetanción dentro del useEffect. como las referencias con useRef no cambian y su valor es persistente en cada rerender. No será necesario listar la funcion en el arreglo de dependencias del useEffect y sería una implementación bonita 😃

Esto último es algo vago, pero nunca esta de más saber a profundidad como funciona React

Sin duda estos Types de html deberían venir más amigables
La tecnología va cambiando, nos vemos en unos años

Comparto mi solución antes de ver la que implementó el profe en el repositorio del curso, en mi caso decidí agregar otro useEffect: ```js import { ImgHTMLAttributes, JSX, useEffect, useRef, useState } from "react"; type LazyImageProps = { src: string; onLazyLoad?: (img: HTMLImageElement) => void; }; type ImageNative = ImgHTMLAttributes<HTMLImageElement>; type Props = LazyImageProps & ImageNative; const transparentImage = "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzIwIiBoZWlnaHQ9IjMyMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB2ZXJzaW9uPSIxLjEiLz4="; export const LazyImage = ({ src, onLazyLoad, ...imgProps }: Props): JSX.Element => { const [currentSrc, setCurrentSrc] = useState(transparentImage); const node = useRef<HTMLImageElement>(null); const [isLazyLoaded, setIsLazyLoaded] = useState(false); useEffect(() => { if (isLazyLoaded) { return; } const observer = new IntersectionObserver((entries) => { entries.forEach((entry) => { if (entry.isIntersecting) { setCurrentSrc(src); observer.disconnect(); setIsLazyLoaded(true); if (typeof onLazyLoad === "function" && node.current) { onLazyLoad(node.current); } } }); }); if (node.current) { observer.observe(node.current); } return () => { observer.disconnect(); }; }, [src, onLazyLoad, isLazyLoaded]); return ; }; ```
Solucion: * Implementacion de onLazyLoad de acuerdo a lo requerido: ![](https://static.platzi.com/media/user_upload/image-b8bff478-d8cf-4dca-9ed3-fec22b167bc4.jpg) * UseState adicional y verficaciones para evitar que se ejecute el onLazyLoad mas de una vez: ![](https://static.platzi.com/media/user_upload/image-096aab9d-cfd3-4e01-8cff-f63523066a31.jpg) ![](https://static.platzi.com/media/user_upload/image-40c1e6fd-79bb-41f4-939f-5a2b284cba7c.jpg) Codigo completo: ![](https://static.platzi.com/media/user_upload/image-b6d18498-e7f8-4f8a-97e9-f3b6b64ddbc5.jpg)
*import* React, {JSX, useEffect, useRef, useState} *from* "react"; *type* LazyImageProps = { src: *string*; onLazyLoad?: (image: HTMLImageElement) => *void* } *type* ImageNativeTypes = React.ImgHTMLAttributes\<HTMLImageElement> *type* Props = LazyImageProps & ImageNativeTypes *export const* LazyImage = ({src, onLazyLoad, ...imgProps}: Props): JSX.Element => { *const* imgRef = useRef\<HTMLImageElement>(*null*) *const* \[currentSrc, setCurrentSrc] = useState<*string* | *undefined*>(*undefined*) *const* \[isImageLoading, setIsImageLoading] = useState(*true*) useEffect(() => { *const* FoxImageObserverCallback: IntersectionObserverCallback = (entries, observer) => { entries.forEach(entry => { *if* (entry.isIntersecting) { setCurrentSrc(src) observer.unobserve(entry.target) } }) } *const* FoxImageObserver: IntersectionObserver = *new* IntersectionObserver(FoxImageObserverCallback) *if* (imgRef.current) { FoxImageObserver.observe(imgRef.current) } *return* () => { FoxImageObserver.disconnect() } }, \[src]); *return* \ { setIsImageLoading(*false*) *if* (imgRef.current && onLazyLoad) { onLazyLoad(imgRef.current) } }} {...imgProps} /> } ```js import React, {JSX, useEffect, useRef, useState} from "react"; type LazyImageProps = { src: string; onLazyLoad?: (image: HTMLImageElement) => void } type ImageNativeTypes = React.ImgHTMLAttributes<HTMLImageElement> type Props = LazyImageProps & ImageNativeTypes export const LazyImage = ({src, onLazyLoad, ...imgProps}: Props): JSX.Element => { const imgRef = useRef<HTMLImageElement>(null) const [currentSrc, setCurrentSrc] = useState<string | undefined>(undefined) const [isImageLoading, setIsImageLoading] = useState(true) useEffect(() => { const FoxImageObserverCallback: IntersectionObserverCallback = (entries, observer) => { entries.forEach(entry => { if (entry.isIntersecting) { setCurrentSrc(src) observer.unobserve(entry.target) } }) } const FoxImageObserver: IntersectionObserver = new IntersectionObserver(FoxImageObserverCallback) if (imgRef.current) { FoxImageObserver.observe(imgRef.current) } return () => { FoxImageObserver.disconnect() } }, [src]); return { setIsImageLoading(false) if (imgRef.current && onLazyLoad) { onLazyLoad(imgRef.current) } }} {...imgProps} /> } ```
Basandome en la solución que ya teníamos usé elo evento onLoad del elemento img el cual se ejecuta cuando la imagen se haya renderizado. Luego con la ayuda de useRef que ya captura nuestra imagen y ina validación de que no sea undefined la función onLazyLoad ni tampoco la referencia a la imagen, ejecutamos dentro del evento onLoad la funcion onLazyLoad. `import ``React, {JSX, useEffect, useRef, useState} ``from ``"react";` `type ``LazyImageProps = {` ` src: ``string``;` ` onLazyLoad?: (image: HTMLImageElement) => ``void` `}` `type ``ImageNativeTypes = React.ImgHTMLAttributes<HTMLImageElement>` `type ``Props = LazyImageProps & ImageNativeTypes` `export const ``LazyImage = ({src, onLazyLoad, ...imgProps}: Props): JSX.Element => {` ` ``const ``imgRef = useRef<HTMLImageElement>(``null``)` ` ``const ``[currentSrc, setCurrentSrc] = useState<``string ``| ``undefined``>(``undefined``)` ` ``const ``[isImageLoading, setIsImageLoading] = useState(``true``)` ` useEffect(() => {` ` ``const ``FoxImageObserverCallback: IntersectionObserverCallback = (entries, observer) => {` ` entries.forEach(entry => {` ` ``if ``(entry.isIntersecting) {` ` setCurrentSrc(src)` ` observer.unobserve(entry.target)` ` }` ` })` ` }` ` ``const ``FoxImageObserver: IntersectionObserver = ``new ``IntersectionObserver(FoxImageObserverCallback)` ` ``if ``(imgRef.current) {` ` FoxImageObserver.observe(imgRef.current)` ` }` ` ``return ``() => {` ` FoxImageObserver.disconnect()` ` }` ` }, [src]);` ` ``return ``<img` ` ref={imgRef}` ` width={320}` ` height="auto"` `` className={`aspect-square object-cover rounded-lg bg-gray-300 min-h-1 transition-all duration-300 ease-in-out ${isImageLoading ? "blur-md" : ""}`}`` ` src={currentSrc}` ` onLoad={() => {` ` setIsImageLoading(``false``)` ` ``if ``(imgRef.current && onLazyLoad) {` ` onLazyLoad(imgRef.current)` ` }` ` }}` ` {...imgProps}` ` />` `}`
```js return ( <main > <h1 className="text-3xl font-bold underline">Hey Platzi 😎! <button onClick={addNewFox}>Add Fox</button> {images.map( ({id, url}, index) => (
{/* <h6>{id}</h6> */} <LazyImage // image={url} src={url} onClick={ () => console.log("Hey baby")} title="RandomFox" width={320} height="auto" alt="" className='rounded bg-gray-300' onLazyLoad={(img) => { console.log(`Image #${index+1} cargaba el nodo`, img) }} />
) )} </main> ) ```       ```js export const LazyImage = ({src, onLazyLoad, ...ImgProps}:Props):JSX.Element => { // Uso de props con destructing //Asignar null aqui es muy importante tenerlo en cuenta const node = useRef<HTMLImageElement>(null) const [loaded, setLoaded] = useState(false); const [currentSrc, setSrc] = useState("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzIwIiBoZWlnaHQ9IjMyMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB2ZXJzaW9uPSIxLjEiLz4="); useEffect(() => { if (loaded) { return } // Nuevo observador const observer = new IntersectionObserver((entries) => { entries.forEach((entry) => { if (entry.isIntersecting){ setSrc(src) if (typeof onLazyLoad === 'function' && node.current){ onLazyLoad(node.current); observer.disconnect(); setLoaded(true); } } }) }); // Observar nodo if (node.current){ observer.observe(node.current); } // Desconectarnos return () => { observer.disconnect() } }, [src, onLazyLoad]) // node.current.src // const image: string = return } ``` Esto si que me ayudo mucho.

Ok, aqui les voy a dejar unos snippets de código con mi solución a los dos retos:

Como se usa:

<div>
  {foxes.map(fox => 
    <LazyImage
      key={fox.id}
      onLazyLoad={imgElement => imgElement.src = fox.url}
      className='mb-5'
      alt="fox"
      width={400}
    />
  )}
</div>

Como esta implementado:

if (entry.isIntersecting) {
  if (imageRef.current && imageRef.current.src === imagePlaceholder) {
    onLazyLoad(imageRef.current)
    observer.unobserve(imageRef.current as Element)
  };
}

El imagePlaceholder es la imagen sin nada que nos paso el paso clases pasadas

Les dejo la solución que implementé, tomé varias ideas que vi en comentarios, fueron de muchísima ayuda, me gustaron mucho estos dos retos!

LazyImage.tsx


import { useState, useEffect, useRef } from "react";
import { ImgHTMLAttributes } from "react";
type NativeType = ImgHTMLAttributes<HTMLImageElement>;
type LazyImageProps = {
  alt: string;
  key: string;
  onLazyLoad?: (node: HTMLImageElement) => void;
};
type Props = LazyImageProps & NativeType;
const LazyImage = ({ src, alt, onLazyLoad, ...props }: Props): JSX.Element => {
  const imageRef = useRef<HTMLImageElement>(null);
  const [currentSource, setCurrentSource] = useState<string | undefined>("");
  useEffect(() => {
    const observer = new window.IntersectionObserver((entries) => {
      entries.forEach((entry) => {
        if (entry.isIntersecting) {
          setCurrentSource(src);
          if (onLazyLoad !== undefined) {
            onLazyLoad(imageRef.current!);
            observer.unobserve(imageRef.current!);
            return;
          }
        }
      });
    });
    if (imageRef.current) {
      observer.observe(imageRef.current);
    }
    return () => observer.disconnect();
  }, [src]);
index.tsx

const handleLazyLoad = (node: any): void => {
    console.log(node);
  };

Comparto mi código

Index

import { LazyImg } from "@/components/LazyImage/LazyImage";
import { useState } from "react";
import type { MouseEventHandler } from "react";
type ImageItem = { id: string; url: string };

export default function Home() {
    const [images, setImages] = useState<Array<ImageItem>>([]);

    const random = () => Math.floor(Math.random() * 123) + 1;
    const generateId = () => Math.random().toString(36).substring(2, 10);

    const handleNew: MouseEventHandler<HTMLButtonElement> = (e) => {
        e.preventDefault();
        const newFox = {
            id: generateId(),
            url: `https://randomfox.ca/images/${random()}.jpg`,
        };
        setImages([...images, newFox]);
    };

    const handleClick = () => {
        console.log("Nice fox huh?");
    };

    const handleLoaded = (node: HTMLImageElement): void => {
        console.log("Cargada" + node);
    };

    return (
        <div>
            <header className="titleFox">
                <h1 className="text-3xl font-bold">Wanna see some foxes?</h1>
                <button onClick={handleNew}>Add new</button>
            </header>
            <main className="gridFoxes">
                {images.map((i) => (
                    <div className="p-4" key={i.id}>
                        <LazyImg
                            src={i.url}
                            onLazyLoad={handleLoaded}
                            className="rounded bg-gray-300 w-80 h-auto"
                            onClick={handleClick}
                        />
                    </div>
                ))}
            </main>
        </div>
    );
}

LazyImage

import { useEffect, useRef, useState } from "react";
import type { ImgHTMLAttributes } from "react";

type LazyImageProps = {
    src: string;
    onLazyLoad: (node: HTMLImageElement) => void;
};
type ImageNative = ImgHTMLAttributes<HTMLImageElement>;
type Props = LazyImageProps & ImageNative;

export const LazyImg = ({
    src,
    onLazyLoad,
    ...imgProps
}: Props): JSX.Element => {
    const node = useRef<HTMLImageElement>(null);

    const [currentSrc, setCurrentSrc] = useState(
        "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzIwIiBoZWlnaHQ9IjMyMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB2ZXJzaW9uPSIxLjEiLz4="
    );
    const [isLoaded, setIsLoaded] = useState(false);

    useEffect(() => {
        if (isLoaded) {
            return;
        }
        const observer = new IntersectionObserver((entries) => {
            entries.forEach((entry) => {
                if (entry.isIntersecting) {
                    setCurrentSrc(src);
                    setIsLoaded(true);

                    if (node.current) onLazyLoad(node.current);
                }
            });
        });

        if (node.current) observer.observe(node.current);

        return () => {
            observer.disconnect();
        };
    }, [src, onLazyLoad, isLoaded]);

    return <img ref={node} src={currentSrc} {...imgProps} />;
};

Siento que mi solución fue “inocente”, pero aquí va:

Paso 1:

Le añadí el evento onload al nodo DOM enviado por parámetro.
😁. Ese es el único paso.
El código está medio ilegible… pero si a alguien le interesa leer: