React: Как вы лениво загружаете изображение из ответа API?

#javascript #reactjs #react-hooks

#javascript #reactjs #реагирующие хуки

Вопрос:

Мой веб-сайт слишком тяжелый, потому что он загружает 200-400 изображений после получения данных с сервера (Firebase Firestore от Google Firebase).

Я придумал два решения, и я надеюсь, что кто-нибудь ответит на одно из них:

  • Я хочу установить для каждого изображения состояние загрузки и позволить посетителям видеть изображение-заполнитель до тех пор, пока оно не будет загружено. Поскольку я не знаю, сколько изображений я получу до получения данных с сервера, мне сложно инициализировать статусы загрузки изображений с помощью useState. Возможно ли это? Тогда как?
  • Как я могу лениво загружать изображения? Изображения инициализируются с помощью заполнителя. Когда прокрутка приближается к изображению, изображение начинает загружаться, заменяя заполнитель.
 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)} />)
  )
}
  

Комментарии:

1. Возможно, вы также захотите связать некоторое состояние загрузки с каждым изображением, а не только с одним общим состоянием загрузки, или это то, о чем вы на самом деле спрашиваете?

2. Я хочу создать imgLoading [], длина которого равна длине массива в ответе от сервера, но я не знаю длину, пока не получу ответ сервера.

3. Ответ на ваш 2-й пункт, я думаю, что эта библиотека npmjs.com/package/react-lazy-load

4. Вы извлекаете все URL-адреса вашего изображения и сопоставляете каждый из них для отображения в изображении?

5. Что-то вроде этого ?

Ответ №1:

Я бы создал Image компонент, который обрабатывал бы свои собственные соответствующие состояния. Затем внутри этого компонента я бы использовал IntersectionObserver API, чтобы определить, виден ли контейнер изображения в браузере пользователя или нет.

У меня были бы состояния isLoading и isInview , isLoading которые будут всегда true до isInview обновления true .

И пока isLoading есть true , я бы использовал null as src для изображения и отобразил заполнитель.

Загружайте только то, src когда контейнер виден в браузере пользователя.

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

У меня также были бы стили CSS, которые будут переключать заполнитель и display свойство изображения.

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

Теперь мой родительский компонент будет выполнять запросы для всех URL-адресов изображений. У него также было бы свое собственное isLoading состояние, которое при установке true отображало бы свой собственный заполнитель. Когда запрос URL-адреса изображения разрешится, я бы затем сопоставил каждый URL-адрес для рендеринга моих Image компонентов.

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

Редактировать flamboyant-kare-zz6qq

Ответ №2:

Для этого есть библиотеки, но если вы хотите создать свою собственную, вы можете использовать IntersectionObserver что-то вроде этого:

 const { useState, useRef, useEffect } = React;

const LazyImage = (imageProps) => {
  const [shouldLoad, setShouldLoad] = useState(false);
  const placeholderRef = useRef(null);

  useEffect(() => {
    if (!shouldLoad amp;amp; 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=1100amp;format=jpegamp;auto=webp'/>
    <LazyImage src='https://www.denofgeek.com/wp-content/uploads/2019/12/power-rangers-beast-morphers-season-2-scaled.jpg?fit=2560,1440'/>
    <LazyImage src='https://i1.wp.com/www.theilluminerdi.com/wp-content/uploads/2020/02/mighty-morphin-power-rangers-reunion.jpg?resize=1200,640amp;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>  

В этом коде они загружаются, как только заполнитель становится видимым на экране, но если вам нужен больший предел обнаружения, вы можете настроить rootMargin параметр IntersectionObserver , чтобы он начинал загружаться, все еще находясь немного за пределами экрана.

Комментарии:

1. observer.observe(placeholderRef.current); выдает ошибку ввода, * Аргумент типа ‘null’ не может быть присвоен параметру типа ‘Element’.ts(2345) *. Вы знаете, как это решить?

2. Если вы используете TypeScript, вы захотите сделать const placeholderRef = useRef<Element | null>(null);

Ответ №3:

Сопоставьте данные ответа с массивом логических значений «isLoading» и обновите обратный вызов, чтобы получить индекс и обновить конкретное логическое значение «isLoading».

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