Implementacja nieskończonego przewijania i leniwego ładowania obrazu w React

Opublikowany: 2022-03-10
Szybkie podsumowanie ↬ W tym samouczku nauczymy się używać interfejsu API HTML Intersection Observer do implementacji nieskończonego przewijania i leniwego ładowania obrazu w funkcjonalnym komponencie React. W trakcie tego dowiemy się, jak korzystać z niektórych hooków Reacta i jak tworzyć niestandardowe hooki.

Jeśli szukałeś alternatywy dla stronicowania, nieskończone przewijanie jest dobrym pomysłem. W tym artykule zbadamy niektóre przypadki użycia interfejsu API Intersection Observer w kontekście funkcjonalnego komponentu React. Czytelnik powinien posiadać praktyczną wiedzę na temat funkcjonalnych komponentów React. Pewna znajomość haczyków React będzie korzystna, ale nie wymagana, ponieważ przyjrzymy się kilku.

Naszym celem jest, aby na końcu tego artykułu zaimplementować nieskończone przewijanie i leniwe ładowanie obrazu przy użyciu natywnego interfejsu API HTML. Dowiedzielibyśmy się też kilku innych rzeczy o hakach reakcji. Dzięki temu możesz w razie potrzeby zaimplementować nieskończone przewijanie i leniwe ładowanie obrazu w swojej aplikacji React.

Zacznijmy.

Tworzenie map z React i ulotką

Pobieranie informacji z pliku CSV lub JSON jest nie tylko skomplikowane, ale także nużące. Przedstawienie tych samych danych w formie pomocy wizualnej jest prostsze. Shajia Abidi wyjaśnia, jak potężne jest narzędzie Ulotka i jak można stworzyć wiele różnych rodzajów map. Przeczytaj powiązany artykuł →

Więcej po skoku! Kontynuuj czytanie poniżej ↓

Interfejs API obserwatora skrzyżowań

Zgodnie z dokumentacją MDN „Intersection Observer API zapewnia sposób na asynchroniczną obserwację zmian w przecięciu elementu docelowego z elementem przodka lub widokiem dokumentu najwyższego poziomu”.

Ten interfejs API pozwala nam zaimplementować fajne funkcje, takie jak nieskończone przewijanie i leniwe ładowanie obrazu. Obserwator przecięcia jest tworzony przez wywołanie jego konstruktora i przekazanie mu wywołania zwrotnego i obiektu opcji. Wywołanie zwrotne jest wywoływane za każdym razem, gdy jeden element, zwany target , przecina widok urządzenia lub określony element, zwany root . Możemy określić niestandardowy korzeń w argumencie options lub użyć wartości domyślnej.

 let observer = new IntersectionObserver(callback, options);

Interfejs API jest prosty w użyciu. Typowy przykład wygląda tak:

 var intObserver = new IntersectionObserver(entries => { entries.forEach(entry => { console.log(entry) console.log(entry.isIntersecting) // returns true if the target intersects the root element }) }, { // default options } ); let target = document.querySelector('#targetId'); intObserver.observe(target); // start observation

entries to lista obiektów IntersectionObserverEntry . Obiekt IntersectionObserverEntry opisuje zmianę przecięcia dla jednego obserwowanego elementu docelowego. Zauważ, że wywołanie zwrotne nie powinno obsługiwać żadnego czasochłonnego zadania, ponieważ działa w głównym wątku.

Interfejs API Intersection Observer obecnie cieszy się szeroką obsługą przeglądarek, jak pokazano na caniuse.

Obsługa przeglądarki obserwatora skrzyżowań. (duży podgląd)

Więcej o API można przeczytać w linkach w sekcji zasobów.

Przyjrzyjmy się teraz, jak wykorzystać to API w prawdziwej aplikacji React. Ostateczna wersja naszej aplikacji będzie stroną ze zdjęciami, która przewija się w nieskończoność, a każdy obraz będzie ładowany leniwie.

Wykonywanie wywołań API za pomocą useEffect

Aby rozpocząć, sklonuj projekt startowy z tego adresu URL. Ma minimalną konfigurację i kilka zdefiniowanych stylów. Dodałem również link do CSS Bootstrap w pliku public/index.html , ponieważ będę używał jego klas do stylizacji.

Zapraszam do stworzenia nowego projektu, jeśli chcesz. Upewnij się, że masz zainstalowany menedżer pakietów yarn , jeśli chcesz śledzić repozytorium. Instrukcje instalacji dla konkretnego systemu operacyjnego można znaleźć tutaj.

W tym samouczku będziemy pobierać zdjęcia z publicznego interfejsu API i wyświetlać je na stronie. Będziemy używać interfejsów API Lorem Picsum.

W tym samouczku użyjemy punktu końcowego https://picsum.photos/v2/list?page=0&limit=10 , który zwraca tablicę obiektów obrazu. Aby uzyskać kolejne dziesięć zdjęć, zmieniamy wartość strony na 1, potem 2 i tak dalej.

Teraz zbudujemy komponent App kawałek po kawałku.

Otwórz src/App.js i wprowadź następujący kod.

 import React, { useEffect, useReducer } from 'react'; import './index.css'; function App() { const imgReducer = (state, action) => { switch (action.type) { case 'STACK_IMAGES': return { ...state, images: state.images.concat(action.images) } case 'FETCHING_IMAGES': return { ...state, fetching: action.fetching } default: return state; } } const [imgData, imgDispatch] = useReducer(imgReducer,{ images:[], fetching: true}) // next code block goes here }

Najpierw definiujemy funkcję redukującą, imgReducer . Ten reduktor obsługuje dwie akcje.

  1. Akcja STACK_IMAGES łączy tablicę images .
  2. Akcja FETCHING_IMAGES przełącza wartość fetching zmiennej między true a false .

Następnym krokiem jest podłączenie tego reduktora do haka useReducer . Gdy to zrobimy, otrzymujemy dwie rzeczy:

  1. imgData , która zawiera dwie zmienne: images to tablica obiektów obrazu. fetching to wartość logiczna, która mówi nam, czy wywołanie API jest w toku, czy nie.
  2. imgDispatch , który jest funkcją do aktualizacji obiektu reduktora.

Możesz dowiedzieć się więcej o haczyku useReducer w dokumentacji Reacta.

Następna część kodu to miejsce, w którym wykonujemy wywołanie API. Wklej następujący kod poniżej poprzedniego bloku kodu w App.js .

 // make API calls useEffect(() => { imgDispatch({ type: 'FETCHING_IMAGES', fetching: true }) fetch('https://picsum.photos/v2/list?page=0&limit=10') .then(data => data.json()) .then(images => { imgDispatch({ type: 'STACK_IMAGES', images }) imgDispatch({ type: 'FETCHING_IMAGES', fetching: false }) }) .catch(e => { // handle error imgDispatch({ type: 'FETCHING_IMAGES', fetching: false }) return e }) }, [ imgDispatch ]) // next code block goes here

Wewnątrz haka useEffect wykonujemy wywołanie do punktu końcowego API za pomocą funkcji fetch API. Następnie aktualizujemy tablicę images o wynik wywołania API, wywołując akcję STACK_IMAGES . Wysyłamy również akcję FETCHING_IMAGES po zakończeniu wywołania API.

Następny blok kodu definiuje wartość zwracaną przez funkcję. Wpisz następujący kod po haczyku useEffect .

 return ( <div className=""> <nav className="navbar bg-light"> <div className="container"> <a className="navbar-brand" href="/#"> <h2>Infinite scroll + image lazy loading</h2> </a> </div> </navv <div id='images' className="container"> <div className="row"> {imgData.images.map((image, index) => { const { author, download_url } = image return ( <div key={index} className="card"> <div className="card-body "> <img alt={author} className="card-img-top" src={download_url} /> </div> <div className="card-footer"> <p className="card-text text-center text-capitalize text-primary">Shot by: {author}</p> </div> </div> ) })} </div> </div> </div> );

Aby wyświetlić obrazy, mapujemy tablicę images w obiekcie imgData .

Teraz uruchom aplikację i wyświetl stronę w przeglądarce. Powinieneś zobaczyć ładnie wyświetlane obrazy w responsywnej siatce.

Ostatnim bitem jest wyeksportowanie komponentu App.

 export default App;
Zdjęcia w responsywnej siatce. (duży podgląd)

Odpowiednia gałąź w tym momencie to 01-make-api-calls.

Rozszerzmy to teraz, wyświetlając więcej zdjęć podczas przewijania strony.

Implementacja nieskończonego przewijania

Staramy się prezentować więcej zdjęć w miarę przewijania strony. Z adresu URL punktu końcowego API, https://picsum.photos/v2/list?page=0&limit=10 , wiemy, że aby uzyskać nowy zestaw zdjęć, wystarczy zwiększyć wartość page . Musimy to również zrobić, gdy zabraknie nam zdjęć do pokazania. Dla naszego celu będziemy wiedzieć, że skończyły nam się obrazy, gdy trafimy na dół strony. Czas zobaczyć, jak interfejs API Intersection Observer pomaga nam to osiągnąć.

Otwórz src/App.js i utwórz nowy reduktor, pageReducer , poniżej imgReducer .

 // App.js const imgReducer = (state, action) => { ... } const pageReducer = (state, action) => { switch (action.type) { case 'ADVANCE_PAGE': return { ...state, page: state.page + 1 } default: return state; } } const [ pager, pagerDispatch ] = useReducer(pageReducer, { page: 0 })

Definiujemy tylko jeden typ akcji. Za każdym razem, gdy zostanie wywołana akcja ADVANCE_PAGE , wartość page jest zwiększana o 1.

Zaktualizuj adres URL w funkcji fetch , aby dynamicznie akceptować numery stron, jak pokazano poniżej.

 fetch(`https://picsum.photos/v2/list?page=${pager.page}&limit=10`)

Dodaj pager.page do tablicy zależności obok imgData . Dzięki temu wywołanie API będzie uruchamiane przy każdej zmianie pager.page .

 useEffect(() => { ... }, [ imgDispatch, pager.page ])

Po haczyku useEffect dla wywołania API wprowadź poniższy kod. Zaktualizuj również linię importu.

 // App.js import React, { useEffect, useReducer, useCallback, useRef } from 'react'; useEffect(() => { ... }, [ imgDispatch, pager.page ]) // implement infinite scrolling with intersection observer let bottomBoundaryRef = useRef(null); const scrollObserver = useCallback( node => { new IntersectionObserver(entries => { entries.forEach(en => { if (en.intersectionRatio > 0) { pagerDispatch({ type: 'ADVANCE_PAGE' }); } }); }).observe(node); }, [pagerDispatch] ); useEffect(() => { if (bottomBoundaryRef.current) { scrollObserver(bottomBoundaryRef.current); } }, [scrollObserver, bottomBoundaryRef]);

Definiujemy zmienną bottomBoundaryRef i ustawiamy jej wartość na useRef(null) . useRef pozwala zmiennym zachowywać swoje wartości podczas renderowania komponentu, tj. bieżąca wartość zmiennej jest zachowywana podczas ponownego renderowania komponentu zawierającego. Jedynym sposobem zmiany jego wartości jest ponowne przypisanie właściwości .current tej zmiennej.

W naszym przypadku bottomBoundaryRef.current zaczyna się od wartości null . W miarę postępu cyklu renderowania strony ustawiamy jego bieżącą właściwość jako węzeł <div id='page-bottom-boundary'> .

Używamy instrukcji przypisania ref={bottomBoundaryRef} , aby powiedzieć Reactowi, aby ustawił bottomBoundaryRef.current jako div, w którym zadeklarowane jest to przypisanie.

Zatem,

 bottomBoundaryRef.current = null

pod koniec cyklu renderowania staje się:

 bottomBoundaryRef.current = <div></div>

Zobaczymy, gdzie to zadanie zostanie wykonane za minutę.

Następnie definiujemy funkcję scrollObserver , w której ustawiamy obserwatora. Ta funkcja akceptuje do obserwacji węzeł DOM . Należy zwrócić uwagę, że za każdym razem, gdy trafimy na obserwowane skrzyżowanie, wywołujemy akcję ADVANCE_PAGE . Efektem jest zwiększenie wartości pager.page o 1. Gdy to nastąpi, zostanie ponownie uruchomiony hak useEffect , który ma go jako zależność. To ponowne uruchomienie z kolei wywołuje wywołanie pobierania z nowym numerem strony.

Procesja eventowa wygląda tak.

Trafienie na przecięcie pod obserwacją → wywołanie akcji ADVANCE_PAGE → zwiększenie wartości pager.page o 1 → useEffect hook dla uruchomień wywołań fetch → zwrócone obrazy są łączone z tablicą images .

scrollObserver w haku useEffect , aby funkcja działała tylko wtedy, gdy zmieni się którakolwiek z zależności haka. Gdybyśmy nie wywołali funkcji wewnątrz useEffect , funkcja byłaby uruchamiana przy każdym renderowaniu strony.

Przypomnij sobie, że bottomBoundaryRef.current odnosi się do <div id="page-bottom-boundary" style="border: 1px solid red;"></div> . Sprawdzamy, czy jego wartość nie jest null przed przekazaniem jej do scrollObserver . W przeciwnym razie konstruktor IntersectionObserver zwróci błąd.

Ponieważ użyliśmy scrollObserver w haczyku useEffect , musimy zawinąć go w hak useCallback , aby zapobiec niekończącemu się ponownemu renderowaniu komponentów. Możesz dowiedzieć się więcej o useCallback w dokumentacji React.

Wpisz poniższy kod po <div id='images'> .

 // App.js <div id='image'> ... </div> {imgData.fetching && ( <div className="text-center bg-secondary m-auto p-3"> <p className="m-0 text-white">Getting images</p> </div> )} <div id='page-bottom-boundary' style={{ border: '1px solid red' }} ref={bottomBoundaryRef}></div>

Po uruchomieniu wywołania API ustawiamy fetching na true , a tekst Getting images staje się widoczny. Po zakończeniu ustawiamy fetching na false , a tekst zostaje ukryty. Moglibyśmy również wyzwolić wywołanie API przed dokładnym osiągnięciem granicy, ustawiając inny threshold w obiekcie opcji konstruktora. Czerwona linia na końcu pozwala nam dokładnie zobaczyć, kiedy zbliżamy się do granicy strony.

Odpowiednia gałąź w tym momencie to 02-infinite-scroll.

Zaimplementujemy teraz leniwe ładowanie obrazu.

Wdrażanie leniwego ładowania obrazu

Jeśli przyjrzysz się karcie sieci podczas przewijania w dół, zobaczysz, że gdy tylko trafisz na czerwoną linię (dolna granica), nastąpi wywołanie API i wszystkie obrazy zaczną się ładować, nawet jeśli nie masz jeszcze do ich przeglądania ich. Istnieje wiele powodów, dla których może to być niepożądane zachowanie. Możemy chcieć zapisywać połączenia sieciowe, dopóki użytkownik nie zechce zobaczyć obrazu. W takim przypadku moglibyśmy zdecydować się na leniwe wczytywanie obrazów , tzn. nie załadujemy obrazu, dopóki nie przewinie się do widoku.

Otwórz src/App.js . Tuż pod nieskończonymi funkcjami przewijania wprowadź następujący kod.

 // App.js // lazy loads images with intersection observer // only swap out the image source if the new url exists const imagesRef = useRef(null); const imgObserver = useCallback(node => { const intObs = new IntersectionObserver(entries => { entries.forEach(en => { if (en.intersectionRatio > 0) { const currentImg = en.target; const newImgSrc = currentImg.dataset.src; // only swap out the image source if the new url exists if (!newImgSrc) { console.error('Image source is invalid'); } else { currentImg.src = newImgSrc; } intObs.unobserve(node); // detach the observer when done } }); }) intObs.observe(node); }, []); useEffect(() => { imagesRef.current = document.querySelectorAll('.card-img-top'); if (imagesRef.current) { imagesRef.current.forEach(img => imgObserver(img)); } }, [imgObserver, imagesRef, imgData.images]);

Podobnie jak w przypadku scrollObserver , definiujemy funkcję imgObserver , która akceptuje węzeł do obserwowania. Kiedy strona trafia na przecięcie, co określa en.intersectionRatio > 0 , zamieniamy źródło obrazu na elemencie. Zauważ, że najpierw sprawdzamy, czy nowe źródło obrazu istnieje przed dokonaniem zamiany. Podobnie jak w przypadku funkcji scrollObserver , zawijamy imgObserver w hook useCallback , aby zapobiec niekończącemu się ponownemu renderowaniu komponentu.

Zwróć też uwagę, że przestajemy obserwować element img po zakończeniu zastępowania. Robimy to metodą unobserve .

W poniższym haczyku useEffect pobieramy wszystkie obrazy z klasą .card-img-top na stronie z document.querySelectorAll . Następnie iterujemy nad każdym obrazem i ustawiamy na nim obserwatora.

Zauważ, że dodaliśmy imgData.images jako zależność useEffect . Gdy to się zmieni, wyzwala zaczep useEffect , a z kolei imgObserver wywoływany z każdym elementem <img className='card-img-top'> .

Zaktualizuj element <img className='card-img-top'/> , jak pokazano poniżej.

 <img alt={author} data-src={download_url} className="card-img-top" src={'https://picsum.photos/id/870/300/300?grayscale&blur=2'} />

Ustawiamy domyślne źródło dla każdego elementu <img className='card-img-top'/> i przechowujemy obraz, który chcemy pokazać we właściwości data-src . Domyślny obraz ma zwykle mały rozmiar, więc pobieramy jak najmniej. Gdy pojawi się element <img/> , wartość właściwości data-src zastępuje obraz domyślny.

Na poniższym obrazku widzimy domyślny obraz latarni nadal wyświetlany w niektórych przestrzeniach.

Obrazy są leniwie ładowane. (duży podgląd)

Odpowiednia gałąź w tym momencie to 03-leniwe ładowanie.

Zobaczmy teraz, jak możemy wyabstrahować wszystkie te funkcje, aby można je było ponownie wykorzystać.

Abstrakcyjne pobieranie, nieskończone przewijanie i leniwe ładowanie do niestandardowych haków

Pomyślnie wdrożyliśmy pobieranie, nieskończone przewijanie i leniwe ładowanie obrazu. Możemy mieć inny komponent w naszej aplikacji, który potrzebuje podobnej funkcjonalności. W takim przypadku moglibyśmy wyabstrahować i ponownie wykorzystać te funkcje. Wystarczy przenieść je do osobnego pliku i zaimportować tam, gdzie ich potrzebujemy. Chcemy zmienić je w niestandardowe hooki.

Dokumentacja Reacta definiuje niestandardowy hook jako funkcję JavaScript, której nazwa zaczyna się od "use" i która może wywoływać inne hooki. W naszym przypadku chcemy stworzyć trzy hooki, useFetch , useInfiniteScroll , useLazyLoading .

Utwórz plik w folderze src/ . Nazwij go customHooks.js i wklej poniższy kod w środku.

 // customHooks.js import { useEffect, useCallback, useRef } from 'react'; // make API calls and pass the returned data via dispatch export const useFetch = (data, dispatch) => { useEffect(() => { dispatch({ type: 'FETCHING_IMAGES', fetching: true }); fetch(`https://picsum.photos/v2/list?page=${data.page}&limit=10`) .then(data => data.json()) .then(images => { dispatch({ type: 'STACK_IMAGES', images }); dispatch({ type: 'FETCHING_IMAGES', fetching: false }); }) .catch(e => { dispatch({ type: 'FETCHING_IMAGES', fetching: false }); return e; }) }, [dispatch, data.page]) } // next code block here

useFetch akceptuje funkcję wysyłania i obiekt danych. Funkcja dispatch przekazuje dane z wywołania API do komponentu App , podczas gdy obiekt data pozwala nam zaktualizować URL punktu końcowego API.

 // infinite scrolling with intersection observer export const useInfiniteScroll = (scrollRef, dispatch) => { const scrollObserver = useCallback( node => { new IntersectionObserver(entries => { entries.forEach(en => { if (en.intersectionRatio > 0) { dispatch({ type: 'ADVANCE_PAGE' }); } }); }).observe(node); }, [dispatch] ); useEffect(() => { if (scrollRef.current) { scrollObserver(scrollRef.current); } }, [scrollObserver, scrollRef]); } // next code block here

Hak useInfiniteScroll akceptuje scrollRef i funkcję dispatch . scrollRef pomaga nam skonfigurować obserwatora, jak już omówiono w sekcji, w której go zaimplementowaliśmy. Funkcja dispatch umożliwia wyzwolenie akcji, która aktualizuje numer strony w adresie URL punktu końcowego API.

 // lazy load images with intersection observer export const useLazyLoading = (imgSelector, items) => { const imgObserver = useCallback(node => { const intObs = new IntersectionObserver(entries => { entries.forEach(en => { if (en.intersectionRatio > 0) { const currentImg = en.target; const newImgSrc = currentImg.dataset.src; // only swap out the image source if the new url exists if (!newImgSrc) { console.error('Image source is invalid'); } else { currentImg.src = newImgSrc; } intObs.unobserve(node); // detach the observer when done } }); }) intObs.observe(node); }, []); const imagesRef = useRef(null); useEffect(() => { imagesRef.current = document.querySelectorAll(imgSelector); if (imagesRef.current) { imagesRef.current.forEach(img => imgObserver(img)); } }, [imgObserver, imagesRef, imgSelector, items]) }

Hak useLazyLoading otrzymuje selektor i tablicę. Selektor służy do wyszukiwania obrazów. Każda zmiana w tablicy wyzwala zaczep useEffect , który ustawia obserwatora na każdym obrazie.

Widzimy, że są to te same funkcje, które mamy w src/App.js , które wyodrębniliśmy do nowego pliku. Dobrą rzeczą jest to, że teraz możemy przekazywać argumenty dynamicznie. Użyjmy teraz tych niestandardowych haków w komponencie App.

Otwórz src/App.js . Zaimportuj niestandardowe haki i usuń zdefiniowane przez nas funkcje pobierania danych, nieskończonego przewijania i opóźnionego ładowania obrazu. Pozostaw reduktory i sekcje, w których korzystamy z useReducer . Wklej poniższy kod.

 // App.js // import custom hooks import { useFetch, useInfiniteScroll, useLazyLoading } from './customHooks' const imgReducer = (state, action) => { ... } // retain this const pageReducer = (state, action) => { ... } // retain this const [pager, pagerDispatch] = useReducer(pageReducer, { page: 0 }) // retain this const [imgData, imgDispatch] = useReducer(imgReducer,{ images:[], fetching: true }) // retain this let bottomBoundaryRef = useRef(null); useFetch(pager, imgDispatch); useLazyLoading('.card-img-top', imgData.images) useInfiniteScroll(bottomBoundaryRef, pagerDispatch); // retain the return block return ( ... )

Mówiliśmy już o bottomBoundaryRef w sekcji o nieskończonym przewijaniu. Przekazujemy obiekt pager i funkcję imgDispatch do useFetch . useLazyLoading akceptuje nazwę klasy .card-img-top . Zwróć uwagę na . zawarte w nazwie klasy. Robiąc to, nie musimy tego określać document.querySelectorAll . useInfiniteScroll akceptuje zarówno ref, jak i funkcję wysyłania do zwiększania wartości page .

Odpowiednia gałąź w tym momencie to 04-haki niestandardowe.

Wniosek

HTML jest coraz lepszy w dostarczaniu ładnych interfejsów API do implementowania fajnych funkcji. W tym poście widzieliśmy, jak łatwo jest używać obserwatora skrzyżowania w komponencie funkcjonalnym React. W trakcie tego nauczyliśmy się, jak korzystać z niektórych hooków Reacta i jak pisać własne hooki.

Zasoby

  • „Nieskończony zwój + leniwe ładowanie obrazu”, Orji Chidi Matthew, GitHub
  • Przyciski „Nieskończone przewijanie, paginacja czy „Załaduj więcej”? Ustalenia użyteczności w handlu elektronicznym”, Christian Holst, Smashing Magazine
  • „Lorem Picsum”, David Marby i Nijiko Yonskai
  • „IntersectionObserver's Coming In View”, Surma, Web Fundamentals
  • Czy mogę użyć… IntersectionObserver
  • „Intersection Observer API”, dokumentacja internetowa MDN
  • „Komponenty i rekwizyty”, React
  • useCallback ”, React
  • useReducer ”, React