Implementarea derulării infinite și a încărcării leneșe a imaginii în React

Publicat: 2022-03-10
Rezumat rapid ↬ În acest tutorial, vom învăța cum să folosim API-ul HTML Intersection Observer pentru a implementa derularea infinită și încărcarea leneră a imaginilor într-o componentă funcțională React. În acest proces, vom învăța cum să folosim unele dintre cârligele lui React și cum să creăm cârlige personalizate.

Dacă ați căutat o alternativă la paginare, defilarea infinită este o bună luare în considerare. În acest articol, vom explora câteva cazuri de utilizare pentru API-ul Intersection Observer în contextul unei componente funcționale React. Cititorul ar trebui să posede cunoștințe de lucru despre componentele funcționale React. O oarecare familiaritate cu cârligele React va fi benefică, dar nu necesară, deoarece vom arunca o privire la câteva.

Scopul nostru este ca la sfârșitul acestui articol, vom fi implementat derulare infinită și încărcare leneră a imaginii folosind un API HTML nativ. De asemenea, am mai fi învățat câteva lucruri despre React Hooks. Cu aceasta, puteți implementa derularea infinită și încărcarea leneșă a imaginii în aplicația dvs. React, acolo unde este necesar.

Să începem.

Crearea de hărți cu React și Leaflet

Înțelegerea informațiilor dintr-un fișier CSV sau JSON nu este doar complicată, dar este și plictisitoare. Reprezentarea acelorași date sub formă de ajutor vizual este mai simplă. Shajia Abidi explică cât de puternic este un instrument Leaflet și cât de multe tipuri diferite de hărți pot fi create. Citiți un articol înrudit →

Mai multe după săritură! Continuați să citiți mai jos ↓

API-ul Intersection Observer

Conform documentelor MDN, „Intersection Observer API oferă o modalitate de a observa în mod asincron schimbările în intersecția unui element țintă cu un element strămoș sau cu vizualizarea unui document de nivel superior”.

Acest API ne permite să implementăm funcții interesante, cum ar fi derularea infinită și încărcarea leneră a imaginilor. Observatorul de intersecție este creat prin apelarea constructorului său și prin transmiterea unui callback și a unui obiect opțiuni. Callback-ul este invocat ori de câte ori un element, numit target , intersectează fie fereastra de vizualizare a dispozitivului, fie un element specificat, numit root . Putem specifica o rădăcină personalizată în argumentul opțiuni sau folosim valoarea implicită.

 let observer = new IntersectionObserver(callback, options);

API-ul este ușor de utilizat. Un exemplu tipic arată astfel:

 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 este o listă de obiecte IntersectionObserverEntry . Obiectul IntersectionObserverEntry descrie o schimbare de intersecție pentru un element țintă observat. Rețineți că apelul invers nu ar trebui să gestioneze nicio sarcină care necesită timp, deoarece rulează pe firul principal.

API-ul Intersection Observer se bucură în prezent de suport larg pentru browser, așa cum se arată pe caniuse.

Suport pentru browser Intersection Observer. (Previzualizare mare)

Puteți citi mai multe despre API în linkurile furnizate în secțiunea de resurse.

Să vedem acum cum să folosim acest API într-o aplicație React reală. Versiunea finală a aplicației noastre va fi o pagină de imagini care se derulează la infinit și va avea fiecare imagine încărcată leneș.

Efectuarea de apeluri API cu useEffect Hook

Pentru a începe, clonează proiectul de pornire de la această adresă URL. Are o configurație minimă și câteva stiluri definite. De asemenea, am adăugat un link către CSS-ul Bootstrap în fișierul public/index.html , deoarece voi folosi clasele sale pentru stil.

Simțiți-vă liber să creați un nou proiect dacă doriți. Asigurați-vă că aveți instalat managerul de pachete de yarn dacă doriți să urmați repo-ul. Puteți găsi instrucțiunile de instalare pentru sistemul dvs. de operare specific aici.

Pentru acest tutorial, vom prelua imagini dintr-un API public și le vom afișa pe pagină. Vom folosi API-urile Lorem Picsum.

Pentru acest tutorial, vom folosi punctul final, https://picsum.photos/v2/list?page=0&limit=10 , care returnează o serie de obiecte imagine. Pentru a obține următoarele zece imagini, schimbăm valoarea paginii la 1, apoi la 2 și așa mai departe.

Acum vom construi componenta aplicației bucată cu bucată.

Deschideți src/App.js și introduceți următorul cod.

 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 }

În primul rând, definim o funcție de reducere, imgReducer . Acest reductor se ocupă de două acțiuni.

  1. Acțiunea STACK_IMAGES concatenează matricea de images .
  2. Acțiunea FETCHING_IMAGES comută valoarea variabilei de fetching între true și false .

Următorul pas este să conectați acest reductor la un cârlig useReducer . Odată ce s-a terminat, revenim două lucruri:

  1. imgData , care conține două variabile: images este matricea de obiecte imagine. fetching este un boolean care ne spune dacă apelul API este în desfășurare sau nu.
  2. imgDispatch , care este o funcție pentru actualizarea obiectului reductor.

Puteți afla mai multe despre cârligul useReducer în documentația React.

Următoarea parte a codului este locul în care facem apelul API. Lipiți următorul cod sub blocul de cod anterior în 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

În interiorul cârligului useEffect , facem un apel către punctul final API cu API-ul de fetch . Apoi actualizăm matricea de imagini cu rezultatul apelului API prin trimiterea acțiunii STACK_IMAGES . De asemenea, trimitem acțiunea FETCHING_IMAGES după finalizarea apelului API.

Următorul bloc de cod definește valoarea returnată a funcției. Introduceți următorul cod după cârligul 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> );

Pentru a afișa imaginile, mapăm peste matricea de imagini din obiectul imgData .

Acum porniți aplicația și vizualizați pagina în browser. Ar trebui să vedeți imaginile bine afișate într-o grilă receptivă.

Ultimul bit este să exportați componenta App.

 export default App;
Imagini în grila receptivă. (Previzualizare mare)

Ramura corespunzătoare în acest moment este 01-make-api-calls.

Acum să extindem acest lucru afișând mai multe imagini pe măsură ce pagina derulează.

Implementarea Infinite Scroll

Ne propunem să prezentăm mai multe imagini pe măsură ce pagina derulează. Din adresa URL a punctului final API, https://picsum.photos/v2/list?page=0&limit=10 , știm că pentru a obține un nou set de fotografii, trebuie doar să creștem valoarea page . Trebuie să facem acest lucru și atunci când nu avem imagini de afișat. Pentru scopul nostru aici, vom ști că am rămas fără imagini când vom ajunge în partea de jos a paginii. Este timpul să vedem cum API-ul Intersection Observer ne ajută să realizăm acest lucru.

Deschideți src/App.js și creați un nou reductor, pageReducer , sub 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 })

Definim un singur tip de acțiune. De fiecare dată când este declanșată acțiunea ADVANCE_PAGE , valoarea page este incrementată cu 1.

Actualizați adresa URL în funcția de fetch pentru a accepta numerele de pagină în mod dinamic, așa cum se arată mai jos.

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

Adăugați pager.page la matricea de dependențe alături de imgData . Acest lucru asigură că apelul API va rula ori de câte ori pager.page se modifică.

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

După cârligul useEffect pentru apelul API, introduceți codul de mai jos. Actualizați-vă și linia de import.

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

Definim o variabilă bottomBoundaryRef și setăm valoarea acesteia la useRef(null) . useRef permite variabilelor să-și păstreze valorile în randurile componentelor, adică valoarea curentă a variabilei persistă atunci când componenta care le conține se redă din nou. Singura modalitate de a-i schimba valoarea este prin reatribuirea proprietății .current pe acea variabilă.

În cazul nostru, bottomBoundaryRef.current începe cu o valoare null . Pe măsură ce ciclul de redare a paginii continuă, setăm proprietatea sa curentă să fie nodul <div id='page-bottom-boundary'> .

Folosim instrucțiunea de atribuire ref={bottomBoundaryRef} pentru a-i spune lui React să seteze bottomBoundaryRef.current să fie div-ul în care este declarată această atribuire.

Prin urmare,

 bottomBoundaryRef.current = null

la sfârșitul ciclului de randare, devine:

 bottomBoundaryRef.current = <div></div>

Vom vedea unde se face această sarcină într-un minut.

În continuare, definim o funcție scrollObserver , în care să setăm observatorul. Această funcție acceptă un nod DOM de observat. Principalul punct de remarcat aici este că de fiecare dată când atingem intersecția sub observație, trimitem acțiunea ADVANCE_PAGE . Efectul este de a crește valoarea pager.page cu 1. Odată ce se întâmplă acest lucru, cârligul useEffect care îl are ca dependență este reluat. Această reluare, la rândul său, invocă apelul de preluare cu noul număr de pagină.

Procesiunea evenimentului arată așa.

Atingeți intersecția sub observație → apelați acțiunea ADVANCE_PAGE → incrementați valoarea pager.page cu 1 → useEffect hook pentru rulări apel de fetch → apelul de preluare este rulat → imaginile returnate sunt concatenate la matricea de images .

scrollObserver într-un hook useEffect , astfel încât funcția să ruleze numai atunci când oricare dintre dependențele hook-ului se schimbă. Dacă nu am apela funcția în interiorul unui hook useEffect , funcția ar rula pe fiecare pagină de randare.

Amintiți-vă că bottomBoundaryRef.current se referă la <div id="page-bottom-boundary" style="border: 1px solid red;"></div> . Verificăm dacă valoarea sa nu este nulă înainte de a o trece la scrollObserver . În caz contrar, constructorul IntersectionObserver ar returna o eroare.

Deoarece am folosit scrollObserver într-un cârlig useEffect , trebuie să-l înfășurăm într-un cârlig useCallback pentru a preveni redarea fără sfârșit a componentelor. Puteți afla mai multe despre utilizarea Callback în documentele React.

Introdu codul de mai jos după <div id='images'> div.

 // 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>

Când începe apelul API, setăm fetching la true , iar textul Obținerea imaginilor devine vizibil. De îndată ce se termină, setăm fetching la false , iar textul este ascuns. De asemenea, am putea declanșa apelul API înainte de a atinge limita exact prin setarea unui threshold diferit în obiectul opțiunilor constructorului. Linia roșie de la sfârșit ne permite să vedem exact când atingem limita paginii.

Ramura corespunzătoare în acest punct este 02-infinite-scroll.

Acum vom implementa încărcarea leneră a imaginii.

Implementarea încărcării lenere a imaginii

Dacă inspectați fila de rețea în timp ce derulați în jos, veți vedea că de îndată ce atingeți linia roșie (limita de jos), are loc apelul API și toate imaginile încep să se încarce chiar și atunci când nu ați ajuns la vizualizare. lor. Există o varietate de motive pentru care acest comportament ar putea să nu fie de dorit. Este posibil să dorim să salvăm apelurile de rețea până când utilizatorul dorește să vadă o imagine. Într-un astfel de caz, am putea opta pentru încărcarea leneș a imaginilor, adică nu vom încărca o imagine până nu derulează în vedere.

Deschideți src/App.js . Chiar sub funcțiile de defilare infinită, introduceți următorul cod.

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

Ca și în cazul scrollObserver , definim o funcție, imgObserver , care acceptă un nod de observat. Când pagina atinge o intersecție, așa cum este determinat de en.intersectionRatio > 0 , schimbăm sursa imaginii pe element. Observați că mai întâi verificăm dacă noua sursă de imagine există înainte de a face schimbul. Ca și în cazul funcției scrollObserver , împachetăm imgObserver într-un cârlig useCallback pentru a preveni redarea fără sfârșit a componentei.

De asemenea, rețineți că nu mai observăm un element img odată ce terminăm cu înlocuirea. Facem asta cu metoda unobserve .

În următorul cârlig useEffect , luăm toate imaginile cu o clasă de .card-img-top pe pagina cu document.querySelectorAll . Apoi repetăm ​​fiecare imagine și setăm un observator pe ea.

Rețineți că am adăugat imgData.images ca o dependență a cârligului useEffect . Când aceasta se schimbă, declanșează cârligul useEffect și, la rândul său, imgObserver este apelat cu fiecare <img className='card-img-top'> .

Actualizați elementul <img className='card-img-top'/> după cum se arată mai jos.

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

Setăm o sursă implicită pentru fiecare <img className='card-img-top'/> și stocăm imaginea pe care dorim să o arătăm pe proprietatea data-src . Imaginea implicită are de obicei o dimensiune mică, astfel încât să descarcăm cât mai puțin posibil. Când elementul <img/> apare la vedere, valoarea proprietății data-src înlocuiește imaginea implicită.

În imaginea de mai jos, vedem imaginea implicită a farului încă afișată în unele dintre spații.

Imaginile fiind încărcate alene. (Previzualizare mare)

Ramura corespunzătoare în acest moment este 03-lazy-loading.

Să vedem acum cum putem abstractiza toate aceste funcții, astfel încât să fie reutilizabile.

Preluare abstractă, derulare infinită și încărcare leneșă în cârlige personalizate

Am implementat cu succes preluarea, derularea infinită și încărcarea leneră a imaginilor. S-ar putea să avem o altă componentă în aplicația noastră care are nevoie de funcționalități similare. În acest caz, am putea abstra și reutiliza aceste funcții. Tot ce trebuie să facem este să le mutăm într-un fișier separat și să le importam acolo unde avem nevoie de ele. Vrem să le transformăm în cârlige personalizate.

Documentația React definește un Hook personalizat ca o funcție JavaScript al cărei nume începe cu "use" și care poate apela alte cârlige. În cazul nostru, dorim să creăm trei cârlige, useFetch , useInfiniteScroll , useLazyLoading .

Creați un fișier în folderul src/ . Numiți-i customHooks.js și inserați codul de mai jos în interior.

 // 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

Cârligul useFetch acceptă o funcție de expediere și un obiect de date. Funcția de expediere transmite datele de la apelul API către componenta App , în timp ce obiectul de date ne permite să actualizăm adresa URL a punctului final 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

Cârligul useInfiniteScroll acceptă un scrollRef și o funcție de dispatch . scrollRef ne ajută să setăm observatorul, așa cum sa discutat deja în secțiunea în care l-am implementat. Funcția de expediere oferă o modalitate de a declanșa o acțiune care actualizează numărul paginii din adresa URL a punctului final 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]) }

Cârligul useLazyLoading primește un selector și o matrice. Selectorul este folosit pentru a găsi imaginile. Orice modificare a matricei declanșează cârligul useEffect care setează observatorul pe fiecare imagine.

Putem vedea că sunt aceleași funcții pe care le avem în src/App.js pe care le-am extras într-un fișier nou. Lucrul bun acum este că putem transmite argumente dinamic. Să folosim acum aceste cârlige personalizate în componenta App.

Deschideți src/App.js . Importați cârlige personalizate și ștergeți funcțiile pe care le-am definit pentru preluarea datelor, derulare infinită și încărcare leneră a imaginilor. Lăsați reductoarele și secțiunile în care folosim useReducer . Lipiți codul de mai jos.

 // 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 ( ... )

Am vorbit deja despre bottomBoundaryRef în secțiunea de defilare infinită. Trecem obiectul pager și funcția imgDispatch la useFetch . useLazyLoading acceptă numele clasei .card-img-top . Rețineți că . incluse în numele clasei. Făcând acest lucru, nu trebuie să-l specificăm document.querySelectorAll . useInfiniteScroll acceptă atât o funcție de referință, cât și o funcție de expediere pentru creșterea valorii page .

Ramura corespunzătoare în acest moment este 04-custom-hooks.

Concluzie

HTML devine din ce în ce mai bun în ceea ce privește furnizarea de API-uri frumoase pentru implementarea funcțiilor interesante. În această postare, am văzut cât de ușor este să utilizați observatorul de intersecție într-o componentă funcțională React. În acest proces, am învățat cum să folosim unele dintre cârligele lui React și cum să ne scriem propriile cârlige.

Resurse

  • „Defilare infinită + încărcare leneră a imaginii”, Orji Chidi Matthew, GitHub
  • Butoane „Defilare infinită, paginare sau „Încărcați mai multe”? Constatări de utilizare în comerțul electronic”, Christian Holst, Smashing Magazine
  • „Lorem Picsum”, David Marby și Nijiko Yonskai
  • „IntersectionObserver’s Coming Into View”, Surma, Fundamentele web
  • Pot folosi... IntersectionObserver
  • „Intersection Observer API”, documente web MDN
  • „Componente și elemente de recuzită”, React
  • useCallback ”, React
  • useReducer ”, React