ResizeObserver API: responsywność w jeszcze wydajniejszej odsłonie
Czy zastanawialiście się kiedyś w jaki sposób można dynamicznie dopasowywać elementy oraz reagować na zmiany ich rozmiarów w aplikacji, tak żeby jak najlepiej sprostać oczekiwaniom jej użytkowników? Do tej pory mieliśmy tylko kilka opcji, np. rozwiązujące podstawowe problemy z responsywnością elementów mediaQueries
czy też globalne eventy onresize lub ich odpowiednik addEventListener resize
. Stosowanie tych rozwiązań w przypadku bardziej zawiłych funkcjonalność często wymagało ‘hakowania’ – nie wspominając o utracie wydajności aplikacji.
Na szczęście na horyzoncie pojawiło się ResizeObserver API – nowe API, które jest uważane za bardziej wydajne i zdecydowanie łatwiejsze w zastosowaniu niż eventy onresize
. Sprawdźmy, co możemy osiągnąć stosując to rozwiązanie.
Piotr Michalczuk. JavaScript Developer w Merixstudio. Frontend developer specjalizujący się w Angularze i Reacie. Kodowanie rozpoczął trzy lata temu od kursu CodersLab (React Bootcamp, polecam!), a teraz doskonali wiedzę podczas pracy w Merixstudio, jak również w ramach lokalnych meetupów oraz realizując swoje własne projekty developerskie. Jeśli aktualnie nie siedzi przy kompie to zapewne jest na szlaku – uwielbia trekking górski.
Spis treści
Kilka słów wstępu
ResizeObserver API pozwala na bardzo dokładne i bezpośrednie monitorowanie wymiarów pojedynczego elementu. Początkowo wsparcie przeglądarek dla tego API było ograniczone jedynie do Chrome (wersja 64) i Firefox (wersja 69), dlatego zastosowanie go na produkcji nie wchodziło w grę. Na szczęście w 2020 roku wsparcie tej funkcjonalności wśród przeglądarek poprawiło się na tyle, że możemy ze spokojem zacząć używać ResizeObserver API częściej. Oczywiście zawsze jest jakieś „ale”. W tym przypadku mamy do czynienia z odwiecznym problemem niewspierania starszych przeglądarek. Rozwiązaniem może być użycie łatki – choć musimy pamiętać, że przy jej zastosowaniu możemy nie mieć dostępu do wszystkich opcji.
Źródło: caniuse.com
Na co komu ResizeObserver aPI
Po tym krótkim wprowadzeniu pora zastanowić się gdzie i kiedy możemy użyć ResizeObserver API. Jak już wspomniałem, to API zostało wprowadzone, aby pokonać dotychczasowe słabości CSS’owych mediaQueries oraz eventów onresize. W związku z tym, ResizeObserver API radzi sobie świetnie w następujących przypadkach:
- zmiana rozmiaru fontu w zależności od wysokości lub szerokości elementu, w którym znajduje się dany tekst,
- aktualizacja wymiarów wykresów,
- dodawanie lub usuwanie elementów w zależności od wymiaru elementu bez konieczności odwoływania się do całego okna,
- zwiększenie responsywności tabeli,
- idealne umiejscowienie strzałki slidera z dynamiczną zawartością w oparciu o wysokość pojedynczego elementu.
Myślę, że po zapoznaniu się z kodem i przyjrzeniu się “z czym to się je”, sami będziecie w stanie podać jeszcze kilka przykładów. Bierzmy się więc do pracy!
Z kodu wzięte: przykład 1
Jeśli na bieżąco śledzicie bloga Just Join IT, na pewno wiecie już co nieco o składni obserwatorów. Sam w jednym z poprzednich artykułów wyjaśniałem jak działa IntersectionObserver, a na blogu Merixstudio pisałem o MutationObserver. Nie martwcie się jednak jeżeli nie czytaliście żadnej z tych publikacji, bo zaraz wyjaśnię Wam działanie obserwatorów krok po kroku.
Interfejs
Aby stworzyć ResizeObserver, używamy konstruktora ResizeObserver()
. Następnie musimy zdefiniować funkcję zwrotną, która zostanie wywołana jedynie w przypadku gdy zmienią się wymiary obserwowanego przez nas elementu.
const resizeObserver = new ResizeObserver((entries, observer) => { entries.forEach(entry => console.log(entry)) });
Tym sposobem stworzyliśmy bardzo prosty obiekt ResizeObserver
i przekazaliśmy do jego konstruktora funkcję zwrotną. Ta ostatnia wymaga zresztą własnych parametrów: entries
, czyli wpisów oraz samego obserwatora. Wpisy są niczym innym jak tablicą zawierającą obiekty ResizeObserverEntry
– do tego co dokładnie się w nich znajduje przejdziemy za chwilę. Drugi parametr jest natomiast odnośnikiem do obserwatora. Możemy go użyć później, np. do zaprzestania obserwowania danego elementu w przypadku gdy jakiś szczególny warunek zostanie spełniony.
Wszystko fajnie, ale pewnie zauważyliście już, że czegoś nam tutaj jednak brakuje. I macie całkowitą rację – musimy przecież zacząć obserwować jakiś element!
Obserwacja elementu
Ten etap jest banalnie prosty: musimy jedynie użyć metody observe()
na utworzonej przez nas wcześniej instancji ResizeObserver
. Metoda ta, podobnie jak poprzednie, przyjmuje dwa argumenty. W pierwszym możemy przekazać zarówno jeden jak i kilka elementów, które chcemy obserwować, natomiast w drugim przekazujemy parametr opcji.
Co ciekawe, te ostatnie są w przypadku ResizeObserver API nie tylko nieskomplikowane, ale też opcjonalne. W momencie pisania tego artykułu jedyną dostępną opcją jest box, dzięki której możemy określić box model obserwowanego elementu. Domyślnie wybrany jest content-box
, ale w razie potrzeby możemy zmienić go na border-box
lub device-content-box
. Pamiętajcie jednak żeby zawsze sprawdzić, którego modelu w danym momencie potrzebujecie – doświadczenie nauczyło mnie, że zły wybór może prowadzić do nerwowego i żmudnego debugowania.
Sam kod jest bardzo prosty:
resizeObserver.observe(elements, options)
Jeśli chcemy przestać obserwować dany element, używamy resizeObserver.unobserve(element)
. W przypadku większej liczby obserwowanych elementów i chęci zatrzymania wszystkich procesów, możemy użyć metody resizeObserver.diconnect()
.
Informacje od obserwatora
Zgodnie z W3C Working Draft, powiadomienie o zmianie wymiarów elementu zostanie wywołane tylko w przypadku spełnienia jednego z kilku warunków. Spójrzmy na kilka przykładowych sytuacji:
- Obserwacja nastąpi kiedy obserwowany element zostanie umieszczony w lub usunięty z drzewa DOM.
- Obserwacja nastąpi kiedy właściwość
display
obserwowanego elementu zostanie ustawiona nanone
. - Obserwacja nie nastąpi w przypadku elementów
non-replaced inline
. - Obserwacje nie będą wywoływane przez CSS’owe transformacje.
- Obserwacja nastąpi w gdy rozpocznie się renderowanie elementu, a jego wielkość będzie inna niż 0,0.
Możemy teraz stworzyć przykładowy element, aby zobaczyć jak wygląda powiadomienie otrzymywane z naszego obserwatora i czego możemy się z niego dowiedzieć. Zaczynamy od elementu HTML, który przekazujemy do naszego ResizeObservera
używając metody observe()
:
<p class="observed">Hello World!</p> resizeObserver.observe(element);
Patrząc na kod naszego resizeObserver
możemy zauważyć, że używamy console.log()
dla każdego pojedynczego elementu wejściowego. Tak wygląda to w konsoli:
Całkiem nieźle, prawda? W contentRect
możemy znaleźć całą masę przydatnych informacji na temat wymiarów danego elementu.
To chyba wszystko jeśli chodzi o podstawowy sposób wykorzystania ResizeObserver API. Teraz zamiast console.log()
wystarczy dodać własną funkcję i gotowe!
Z kodu wzięte: przykład 2
Podzielę się z Wami jeszcze jednym z życia wziętym przykładem zastosowania ResizeObserver API. Tym razem będę się opierać na React oraz Hookach. Dobrze byście mieli już wiedzę i pierwsze doświadczenia z tymi rozwiązaniami – dzięki temu po prostu lepiej zrozumiecie kolejne akapity.
Problem
W projekcie, przy którym niedawno pracowałem, napotkaliśmy ciekawy problem: złożone designy przedstawiające dwurzędową siatkę z galerią, która w przy danej szerokości elementu miała przekształcać się w karuzelę. Szerokość ta była bardzo łatwo osiągana przy zmianie widoku z pionowego na poziomy.
Pomyślałem, że dobrym sposobem na podejście do tego problemu może być użycie ResizeObserver API. Chciałem stworzyć rozwiązanie, które będzie można zastosować nie tylko w przypadku tego konkretnego komponentu, ale również w różnych innych miejscach w projekcie, w których będzie ono odpowiednie. Aby osiągnąć mój cel, zacząłem pracę nad stworzeniem własnego Hooka.
Rozwiązanie
Zacznijmy od utworzenia prostej funkcji przyjmującej dwa argumenty. Pierwszy argument to element, który chcemy obserwować – w tym przypadku nazwiemy go ref
. Drugi to wielkość graniczna, którą będziemy obserwować w contentRect
danego elementu.
export function useResizeObserver(ref, size) { };
Następnie dodajemy useState Hook aby okiełznać stan w oraz zwrócić wynik naszej funkcji. Nazwijmy zmienną stanu isDesktop
i niech będzie to zmienna typu boolean
. Ustawiamy domyślny stan jako false
:
Import { useState } from ‘react’; export function useResizeObserver(ref, size) { const [isDesktop, setIsDesktop] = useState(false); return isDesktop; };
Kolejny krok to poradzenie sobie z logiką ResizeObserver poprzez dodanie useEffect
Hook.
import { useEffect, useState } from ‘react’; export function useResizeObserver(ref, size) { const [isDesktop, setIsDesktop] = useState(false); useEffect(() => { const observeTarget = ref.current; const resizeObserver = new ResizeObserver(entries => ( entries[0].contentRect.width < size ? setIsDesktop(false) : setIsDesktop(true) )); resizeObserver.observe(observeTarget); }, [ref, size ]); return isDesktop; };
W React używamy useRef
Hook by odwołać się do wirtualnego elementu DOM – tak zrobimy również w naszym przypadku. Tworzymy nową zmienną observeTarget
i przypisujemy do niej właściwość .current
z naszego ref’a. Jest to element, który będziemy obserwować.
Następnie definiujemy naszego resizeObservera i jego funkcję zwrotną. W funkcji zwrotnej sprawdzamy tylko pierwszy element wejściowy. Obserwując jeden element, możemy sprawdzić czy wielkość naszego elementu osiągnęła podaną przez nas wartość stosując proste if else
. Jeśli warunek zostanie spełniony, aktualizujemy nasz stan za pomocą setIsDesktop
.
W ten sposób stworzyliśmy Hooka, którego będziemy mogli użyć wielokrotnie w naszej aplikacji w ten sposób:
const isDesktop = useResizeObserver(galleryRef, 1140);
Stosować ResizeObserver API czy nie, oto jest pytanie
ResizeObserver API można z powodzeniem uznać za kolejny obserwator gotowy do użycia na produkcji. Dzięki niemu, tworzenie responsywnych elementów stało się dużo łatwiejsze i wydajniejsze. Jeśli potrzebujecie twardych danych na ten temat, udało mi się dotrzeć do stwierdzenia, że “wydajność resizeObserver może być nawet o 10 razy lepsza niż rozwiązania takie jak onresize
eventy”. Nie ma co się zastanawiać, czas zacząć stosować to rozwiązanie!
Zdjęcie główne artykułu pochodzi z unsplash.com.