Używamy useEffect z intersection observerem oraz piszemy custom hook. Kontynuacja lekcji poprzednich. Do dzieła!

Ok, App.js:

import Box from './Box.js';

export default function App() {
  return (
    <>
      <LongSection />
      <Box />
      <LongSection />
      <Box />
      <LongSection />
    </>
  );
}

Jak widać mamy tutaj komponent Box, importowany z tego samego folderu, oraz komponent LongSection, który musi być zdefiniowany gdzieś w tym samym pliku.

Najpierw zobaczmy LongSection:

function LongSection() {
  const items = [];
  for (let i = 0; i < 50; i++) {
    items.push(<li key={i}>Item #{i} (keep scrolling)</li>);
  }
  return <ul>{items}</ul>
}

Ok, pierwszy rzut oka na Box.js:

import { useRef, useEffect } from 'react';

export default function Box() {
  const ref = useRef(null);

  //(...)

  return (
    <div ref={ref} style={{
      margin: 20,
      height: 100,
      width: 100,
      border: '2px solid black',
      backgroundColor: 'blue'
    }} />
  );
}

Jak widać mamy tu ref, dzięki któremu możemy mieć dostęp do elementu div, oraz typowe dla Reacta zarządzanie stylami CSS, czyli:

  • atrybut style z interpolacją
  • wewnątrz interpolacji obiekt reprezentujący style
  • nazwy properties jak w CSS, ale tam, gdzie mamy myślniki (w CSS) mamy wielką literę (w JS)
  • atrybuty liczbowe jako int, React jakoś domyśla się, że chodzi o piksele
  • każdy inny atrybut, który zawiera cokolwiek innego niż liczba reprezentująca piksele musi być w cudzysłowie
  • nawet tego diva zamykać nie musimy, skoro nie mamy w nim dzieci, swoją drogą to mnie zaskoczyło, myślałem, że tylko z reactowymi komponentami możemy się bawić w ten sposób

Ok, to może poznajmy nieco intersection observer. W czystym JS tak się go robi:

const options = {
  root: document.querySelector("#scrollArea"),
  rootMargin: "0px",
  threshold: 1.0,
};

const observer = new IntersectionObserver(callback, options);

Options mają trzy argumenty:

  • root – element, jak null to viewport jest rootem
  • rootMargin – margines, domyślnie zero, można podawać jak w CSS, to jest ten margines który będzie się zaliczał, że już jest intersekcja
  • threshold – próg, np. 1.0 oznacza, że intersekcji nie ma, dopóki każdy piksel nie jest widoczny, 0.5 by oznaczało, że wystarczy połowa, aby uznać, że jest intersekcja
    • domyślny próg wynosi 0, co oznacza, że choćby jeden piksel był widoczny, mamy intersekcję, nie mamy obiekcji ile % elementu musi być widoczne
    • można mieć wiele progów i wtedy podajemy je jako tablica

Callback przyjmuje IntersectionObserverEntries oraz samego observera (o ile nam to do czegokolwiek potrzebne), przykład callbacku:

const intersectionCallback = (entries) => {
  entries.forEach((entry) => {
    if (entry.isIntersecting) {
      let elem = entry.target;

      if (entry.intersectionRatio >= 0.75) {
        intersectionCounter++;
      }
    }
  });
};

Ważna uwaga:

  • Nie wystarczy utworzyć intersection observer, trzeba jeszcze wywołać na nim metodę observe
  • Mamy też do dyspozycji metodę disconnect, która przestaje obserwować

Ok, to teraz zobaczymy sobie nasz intersection observer w useEffect:

import { useRef, useEffect } from 'react';

export default function Box() {
  const ref = useRef(null);

  useEffect(() => {
    const div = ref.current;
    const observer = new IntersectionObserver(entries => {
      const entry = entries[0];
      if (entry.isIntersecting) {
        document.body.style.backgroundColor = 'black';
        document.body.style.color = 'white';
      } else {
        document.body.style.backgroundColor = 'white';
        document.body.style.color = 'black';
      }
    }, {
       threshold: 1.0
    });
    observer.observe(div);
    return () => {
      observer.disconnect();
    }
  }, []);

  return (
    <div ref={ref} style={{
      margin: 20,
      height: 100,
      width: 100,
      border: '2px solid black',
      backgroundColor: 'blue'
    }} />
  );
}

Czyli tak:

  • ref posiada aktualny element div naszego boxa
  • observer ma próg 1.0, czyli każdy pixel musi być widoczny, aby można było mówić o intersekcji
  • observer nie ma marginu, czyli margin 0
  • observer nie ma roota, rootem jest viewport zatem
  • skoro jest jeden próg, to będzie jedno entry pod indeksem 0
  • jak jest intersekcja zmieniamy kolory dokumentu
  • jak jej nie ma zmieniamy na powrót
  • jako setup useEffecta ustawiamy observe
  • jako cleanup useEffecta ustawiamy disconnect

Wszystko jasne, ale możemy z tego zrobić custom hook:

import { useState, useEffect } from 'react';

export function useIntersectionObserver(ref) {
  const [isIntersecting, setIsIntersecting] = useState(false);

  useEffect(() => {
    const div = ref.current;
    const observer = new IntersectionObserver(entries => {
      const entry = entries[0];
      setIsIntersecting(entry.isIntersecting);
    }, {
       threshold: 1.0
    });
    observer.observe(div);
    return () => {
      observer.disconnect();
    }
  }, [ref]);

  return isIntersecting;
}

Te custom hooki tak działają, że:

  • nazwa zaczyna się od use
  • muszą być dobre importy oraz export funkcji-hooka
  • to, co przekazujemy do funkcji jako argument w nawiasach, musi znaleźć się w tablicy zależności

Znając te trzy kroki możemy w sumie niemal każdą rzecz, którą zechcemy, wywalić do custom hooka.

A jak wygląda użycie? Bo tutaj mamy zwracane isIntersecting i część związaną z intersection observerem i to isIntersecting to jest zmienna reaktywna, której ustawianie jest po prostu automatyczne, nie musimy się tym bawić.

Ale nadal musimy, za pomocą useEffect, obsłużyć co ma się stać, gdy się isIntersecting zmieni:

import { useRef, useEffect } from 'react';
import { useIntersectionObserver } from './useIntersectionObserver.js';

export default function Box() {
  const ref = useRef(null);
  const isIntersecting = useIntersectionObserver(ref);

  useEffect(() => {
   if (isIntersecting) {
      document.body.style.backgroundColor = 'black';
      document.body.style.color = 'white';
    } else {
      document.body.style.backgroundColor = 'white';
      document.body.style.color = 'black';
    }
  }, [isIntersecting]);

  return (
    <div ref={ref} style={{
      margin: 20,
      height: 100,
      width: 100,
      border: '2px solid black',
      backgroundColor: 'blue'
    }} />
  );
}

Ok, może i trudne, ale będziemy musieli iść w tym kierunku. Więcej custom hooks niedługo.