score:2

Accepted answer

there are libraries for this, but if you want to roll your own, you can use an intersectionobserver, something like this:

const { usestate, useref, useeffect } = react;

const lazyimage = (imageprops) => {
  const [shouldload, setshouldload] = usestate(false);
  const placeholderref = useref(null);

  useeffect(() => {
    if (!shouldload && placeholderref.current) {
      const observer = new intersectionobserver(([{ intersectionratio }]) => {
        if (intersectionratio > 0) {
          setshouldload(true);
        }
      });
      observer.observe(placeholderref.current);
      return () => observer.disconnect();
    }
  }, [shouldload, placeholderref]);

  return (shouldload 
    ? <img {...imageprops}/> 
    : <div classname="img-placeholder" ref={placeholderref}/>
  );
};

reactdom.render(
  <div classname="scroll-list">
    <lazyimage src='https://i.insider.com/536a52d9ecad042e1fb1a778?width=1100&format=jpeg&auto=webp'/>
    <lazyimage src='https://www.denofgeek.com/wp-content/uploads/2019/12/power-rangers-beast-morphers-season-2-scaled.jpg?fit=2560%2c1440'/>
    <lazyimage src='https://i1.wp.com/www.theilluminerdi.com/wp-content/uploads/2020/02/mighty-morphin-power-rangers-reunion.jpg?resize=1200%2c640&ssl=1'/>
    <lazyimage src='https://m.media-amazon.com/images/m/mv5bntfiody1nditodc1zi00mje2ltk0mzqtnjexy2i1ntu3mzdixkeyxkfqcgdeqxvynzu1nze3ntg@._v1_cr0,45,480,270_al_ux477_cr0,0,477,268_al_.jpg'/>
  </div>,
  document.getelementbyid('app')
);
.scroll-list > * {
  margin-top: 400px;
}

.img-placeholder {
  content: 'placeholder!';
  width: 400px;
  height: 300px;
  border: 1px solid black;
  background-color: silver;
}
<div id="app"></div>

<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.1/umd/react-dom.production.min.js"></script>

this code is having them load as soon as the placeholder is visible on the screen, but if you want a larger detection margin, you can tweak the rootmargin option of the intersectionobserver so it starts loading while still slightly off screen.

score:0

map the response data to an array of "isloading" booleans, and update the callback to take the index and update the specific "isloading" boolean.

function sample() {
  const [items, setitems] = usestate([]);
  const [imgloading, setimgloading] = usestate([]);

  useeffect(() => {
    axios.get(url).then((response) => {
      const { data } = response;
      setitems(data);
      setimgloading(data.map(() => true));
    });
  }, []);

  return items.map((item, index) => (
    <img
      src={item.imageurl}
      onload={() =>
        setimgloading((loading) =>
          loading.map((el, i) => (i === index ? false : el))
        )
      }
    />
  ));
}

score:3

i would create an image component that would handle it's own relevant states. then inside this component, i would use intersectionobserver api to tell if the image's container is visible on user's browser or not.

i would have isloading and isinview states, isloading will be always true until isinview updates to true.

and while isloading is true, i would use null as src for the image and will display the placeholder.

load only the src when container is visible on user's browser.

function image({ src }) {
  const [isloading, setisloading] = usestate(true);
  const [isinview, setisinview] = usestate(false);
  const root = useref(); // the container

  useeffect(() => {
    // sets `isinview` to true until root is visible on users browser

    const observer = new intersectionobserver(onintersection, { threshold: 0 });
    observer.observe(root.current);

    function onintersection(entries) {
      const { isintersecting } = entries[0];

      if (isintersecting) { // is in view
        observer.disconnect();
      }

      setisinview(isintersecting);
    }
  }, []);

  function onload() {
    setisloading((prev) => !prev);
  }

  return (
    <div
      ref={root}
      classname={`imgwrapper` + (isloading ? " imgwrapper--isloading" : "")}
    >
      <div classname="imgloader" />
      <img classname="img" src={isinview ? src : null} alt="" onload={onload} />
    </div>
  );
}

i would also have css styles that will toggle the placeholder and image's display property.

.app {
  --image-height: 150px;
  --image-width: var(--image-height);
}

.imgwrapper {
  margin-bottom: 10px;
}

.img {
  height: var(--image-height);
  width: var(--image-width);
}

.imgloader {
  height: 150px;
  width: 150px;
  background-color: red;
}

/* container is loading, hide the img */
.imgwrapper--isloading .img {
  display: none;
}

/* container not loading, display img */
.imgwrapper:not(.imgwrapper--isloading) .img {
  display: block;
}

/* container not loading, hide placeholder */
.imgwrapper:not(.imgwrapper--isloading) .imgloader {
  display: none;
}

now my parent component, will do the requests for all the image urls. it would also have its own isloading state that when set true would display its own placeholder. when the image url's request resolves, i would then map on each url to render my image components.

export default function app() {
  const [imageurls, setimageurls] = usestate([]);
  const [isloading, setisloading] = usestate(true);

  useeffect(() => {
    fetchimages().then((response) => {
      setimageurls(response);
      setisloading((prev) => !prev);
    });
  }, []);

  const images = imageurls.map((url, index) => <image key={index} src={url} />);

  return <div classname="app">{isloading ? "please wait..." : images}</div>;
}

edit flamboyant-kare-zz6qq


Related Query

More Query from same tag