Jak uruchomić akcję w momencie, gdy element znajdzie się w widoku użytkownika?

Opublikowane przez Jakub, 17.06.2024

Lawa wypływająca z wulkanu - pixel art

Jak wykryć, że element znajduje się w polu widzenia użytkownika?

Jest to dość częsty scenariusz - chcemy aby dana akcja została uruchomiona dopiero wtedy, gdy element znajdzie się w polu widzenia użytkownika. Zazwyczaj jest to przydatne, gdy strona jest tzw. landing page bez żadnych dodatkowych zakładek - cała zawartość znajduje się na jednej, długiej stronie, którą użytkownik scrolluje w dół. W momencie, gdy dana sekcja się pojawia - chcemy uruchomić animację lub podjąć inne działanie,

Dawniej nie było żadnego wygodnego rozwiązania, jednak od pewnego czasu do standardu ECMA Script wprowadzono klasę IntersectionObserver. Tutaj możesz sprawdzić kompatybilność API IntersectionObserver z nowoczesnymi przeglądarkami. Jeśli nie zadowala Cię ona, wypróbuj dostępne polyfille, np. ten: intersection-observer-polyfill

Jak działa IntersectionObserver?

Jest to klasa, której obiekt przyjmuje w konstruktorze callback. Callback ten przyjmuje argument entries. Są to wszystkie elementy, które obserwuje IntersectionObserver. Uruchamiany jest za każdym razem, gdy dowolny z elementów zmieni swoją widoczność, a więc zniknie z pola widzenia lub się w nim pojawi.

Dokładna dokumentacja IntersectionObservera dostępna jest na mdn web docs.

const observer = new IntersectionObserver((entries) => { const element = entries.find((element) => element.id === "myElement"); console.info(element.isIntersecting); }); const myElement = document.getElementById("myElement"); observer.observe(myElement);

Powyższy kod zaloguje true do konsoli kiedy tylko element o id myElement pojawi się w polu widzenia użytkownika lub false, gdy ten element zniknie.

Jak to przekuć na hooka?

W React najwygodniej korzysta się z hooków, dlatego zbudujmy hook który obsłuży IntersectionObserver

function useInViewport<T>(el?: MutableRefObject<T>): boolean { const [visible, setVisible] = useState(false); useEffect(() => { const observer = new IntersectionObserver((entries) => { setVisible(entries[0].isIntersecting); }); el?.current && observer.observe(el.current); return () => { el?.current && observer.unobserver(el.current); }; }, [el]); return visible; }

Przyjmuje on ref do elementu, który chcemy obserwować. Zwraca stan true/false w zależności od stanu widoczności. Każda zmiana triggeruje rerender komponentu, dlatego zmiany możemy zastosować na żywo. Dzięki funkcji sprzątającej zwracanej z hooka useEffect, element przestanie być obserwowany gdy komponent zostanie odmontowany.

Hook można łatwo przerobić tak, aby przyjmował id elementu, a nie jego ref:

function useInViewport(id: string, remainVisible?: boolean): boolean { const [visible, setVisible] = useState(false); useEffect(() => { const element = document.getElementById(id); const observer = new IntersectionObserver((entries) => { setVisible(entries[0].isIntersecting); }); element && observer.observe(element); return () => { element && observer.unobserver(element); }; }, [el]); return visible; }

Czy potrzebujemy tylu IntersectionObserverów, co elementów?

Jak pewnie zauważyłeś, callback IntersectionObservera przyjmuje argument entries, który jest listą elementów. Możemy stworzyć klasę pomocniczą, która zadba o to, aby istniał tylko jeden IntersectionObserver, który obsłuży wszystkie elementy na raz.

interface Listener { el: Element; listener: (isIntersecting: boolean) => void; } class IntersectionObserverRunner { private static listeners: Listener[] = []; private static observer = new IntersectionObserver((entries) => { entries.forEach((entry) => { const listener = this.listeners.find( (currentListener) => currentListener.el === entry.target, ); listener?.listener(entry.isIntersecting); }); }); static addListener = ( element: Element, listener: (isIntersecting: boolean) => void, ) => { this.observer?.observe(element); this.listeners.push({ el: element, listener }); }; static removeListener = (element: Element) => { this.observer?.unobserve(element); this.listeners.splice( this.listeners.findIndex( (currentListener) => currentListener.el === element, ), 1, ); }; }

No dobra, co tu się właściwie dzieje?

Klasa posiada metody i pola statyczne, co oznacza, że istnieje ich tylko jedna instancja na cały program. Dzięki temu będzie istniał tylko jeden observer zapisany w klasie IntersectionObserverRunner

Posiada on pole listeners które jest listą obiektów typu Listener. Każdy z nich posiada element drzewa DOM oraz listener, czyli funkcja, która zostanie uruchomiona gdy element znajdzie się w obszarze widzenia lub z niego zniknie,

W polu observer znajduje się zainicjalizowana instancja klasy IntersectionObserver wraz z callbackiem. Callback ten iteruje po wszystkich elementach entries. Dla każdego z nich znajduje własny listener na liście listeners i uruchamia przyporządkowaną mu funkcję.

Dodatkowo, okreslone zostały dwie dodatkowe funkcje:

  • addListener - przyjmuje element drzewa DOM oraz funkcję, która ma być dla niego uruchamiana. Dodaje listner do listy wszystkich listenerów i podpina element do IntersectionObservera
  • removeListener - usuwa listener z listy i odłącza element od IntersectionObservera

Teraz możemy dostosować nasz poprzedni hook do klasy IntersectionObserverRunner:

function useInViewport<T>( el?: MutableRefObject<T>, remainVisible?: boolean, ): boolean { const [visible, setVisible] = useState(false); useEffect(() => { el?.current && IntersectionObserverRunner.addListener(el.current, (isVisible) => setVisible(isVisible || !!remainVisible), ); return () => { el?.current && IntersectionObserverRunner.removeListener(el.current); }; }, [el, remainVisible]); return visible; }

Hook ten działa podobnie do poprzedniego, jednak zamiast tworzyć nowy IntersectionObserver za każdym wykorzystaniem, używa tego już istniejącego. Różnicą jest dodatkowy parametr remainVisible - gdy zostanie przekazany jako true, stan widoczności przestanie się aktualizować po pierwszym pojawieniu się w obszarze widzialnym przez użytkownika - dzięki temu możemy sterować wykonaniem animacji tak, aby uruchomiła się tylko raz.

devFlipCards 2024

Akceptujesz cookies?

Cookies to niewielkie fragmenty danych zapisywane lokalnie na twoim urządzeniu. Pomagają w funkcjonowaniu naszej strony - zapisują Twoje ustawienia strony takie jak motyw czy język. Poprawiają funkcjonowanie reklam i pozwalają nam na przeprowadzanie analityki odwiedzin strony. Korzystając ze strony zgadasz się na ich wykorzystanie.