javascript - React: How do you lazyload image from API response? - Stack Overflow

My website is too heavy because it downloads 200-400 images after fetching data from the server (Google

My website is too heavy because it downloads 200-400 images after fetching data from the server (Google's Firebase Firestore).

I came up with two solutions and I hope somebody answers one of them:

  • I want to set each img to have a loading state and enable visitors to see the placeholder image until it is loaded. As I don't know how many images I get until fetching data from the server, I find it hard to initialize image loading statuses by useState. Is this possible? Then, how?
  • How can I Lazy load images? Images are initialized with a placeholder. When a scroll es near an image, the image starts to download replacing the placeholder.
function sample() {}{
  const [items, setItems] = useState([])
  const [imgLoading, setImgLoading] = useState(true)  // imgLoading might have to be boolean[]
  useEffect(() => {
    axios.get(url).
    .then(response => setItems(response.data))
  }, [])
  return (
    items.map(item => <img src={item.imageUrl} onLoad={setImgLoading(false)} />)
  )
}

My website is too heavy because it downloads 200-400 images after fetching data from the server (Google's Firebase Firestore).

I came up with two solutions and I hope somebody answers one of them:

  • I want to set each img to have a loading state and enable visitors to see the placeholder image until it is loaded. As I don't know how many images I get until fetching data from the server, I find it hard to initialize image loading statuses by useState. Is this possible? Then, how?
  • How can I Lazy load images? Images are initialized with a placeholder. When a scroll es near an image, the image starts to download replacing the placeholder.
function sample() {}{
  const [items, setItems] = useState([])
  const [imgLoading, setImgLoading] = useState(true)  // imgLoading might have to be boolean[]
  useEffect(() => {
    axios.get(url).
    .then(response => setItems(response.data))
  }, [])
  return (
    items.map(item => <img src={item.imageUrl} onLoad={setImgLoading(false)} />)
  )
}
Share Improve this question edited Aug 14, 2020 at 2:17 Wt.N asked Aug 14, 2020 at 0:41 Wt.NWt.N 1,6562 gold badges22 silver badges42 bronze badges 6
  • You may also want to associate some loading state with each image versus just a single overall loading state, or is that what you are actually asking about? – Drew Reese Commented Aug 14, 2020 at 0:46
  • I want to make imgLoading[] whose length is the array length in the response from the server, but I don't know the length until I get server response. – Wt.N Commented Aug 14, 2020 at 1:01
  • Answer to your 2nd bullet is I think this library npmjs./package/react-lazy-load – Jacob Commented Aug 14, 2020 at 1:45
  • You fetch all your image urls, and your mapping each of them to display in image? – bertdida Commented Aug 14, 2020 at 2:00
  • 1 Something like this? – bertdida Commented Aug 14, 2020 at 2:18
 |  Show 1 more ment

3 Answers 3

Reset to default 3

I would create an Image ponent that would handle it's own relevant states. Then inside this ponent, 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 ponent, 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 ponents.

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>;
}

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./536a52d9ecad042e1fb1a778?width=1100&format=jpeg&auto=webp'/>
    <LazyImage src='https://www.denofgeek./wp-content/uploads/2019/12/power-rangers-beast-morphers-season-2-scaled.jpg?fit=2560%2C1440'/>
    <LazyImage src='https://i1.wp./www.theilluminerdi./wp-content/uploads/2020/02/mighty-morphin-power-rangers-reunion.jpg?resize=1200%2C640&ssl=1'/>
    <LazyImage src='https://m.media-amazon./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./ajax/libs/react/16.13.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare./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.

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))
        )
      }
    />
  ));
}

发布者:admin,转转请注明出处:http://www.yc00.com/questions/1744263704a4565759.html

相关推荐

发表回复

评论列表(0条)

  • 暂无评论

联系我们

400-800-8888

在线咨询: QQ交谈

邮件:admin@example.com

工作时间:周一至周五,9:30-18:30,节假日休息

关注微信