Sådan bruges en IntersectionObserver i en reaktionskrog

Foto af chuttersnap på Unsplash

(Jeg vil gerne give kredit, hvor jeg mener, at kredit skyldes. Kodeeksemplerne i dette indlæg blev skrevet af mig, men blev meget informeret om kode skrevet af min tidligere medarbejder, Jared tuxsudo på twitter.)

En af de sværeste ting at gøre på nettet er at finde ud af, om et element er synligt, eller hvor et element er i forhold til dets overordnede element. Historisk betød dette at køre beregninger udløst af en rullebegivenhed, som hurtigt kan blive et ydeevneansvar for din app.

Heldigvis er der introduceret en bedre og meget mere udførende måde at gøre dette på: Intersection Observer. Intersection Observer API tillader asynkron kontrol af forholdet mellem skæringspunktet mellem et element og et synspunkt og udløser kun et tilbagekald, når den eller de foruddefinerede tærskler er opfyldt. Dette har åbnet mange brugeroplevelser, som var vanskelige at implementere på en udøvende måde, såsom uendelig rulning, doven belastningsbilleder eller forsinkelse af animationer, indtil de er synlige.

For nylig ønskede jeg at undersøge, hvordan man ville arbejde på at implementere dette i en reaktionskrog. Jeg stødte på mange gotchas, men heldigvis sendte Dan Abramov for nylig en meget hjælpsom guide til at bruge Effect over på hans blog, Overreacted, hvilket hjalp mig enormt med at forstå disse gotchas og hvad jeg var nødt til at gøre for at ordne dem. Så jeg troede, at jeg ville sammenfatte, hvad jeg lærte for forhåbentlig at hjælpe dig med at undgå de samme fejl, som jeg stødte på.

Hvordan fungerer API'en til skæringsobserver?

For at få en fuldstændig forståelse af Intersection Observer API, vil jeg anbefale, at du tjekker dokumentationen, der findes på MDN. Kort sagt skal du oprette en observatør, der 'observerer' en DOM-knude og kalder et tilbagekald, når en eller flere af tærsklerne er opfyldt. En tærskel kan være ethvert forhold fra 0 til 1, hvor 1 betyder, at elementet er 100% i udsigten og 0 er 100% ude af udsigten. Som standard er tærsklen indstillet til 0. Her er et eksempel på, hvordan man opretter en observatør, som jeg lånte fra MDN:

const callback = (poster, observatør) => {
  entry.forEach (entry => {
    // Hver post beskriver en krydsændring for en observeret
    // målelement:
    // entry.boundingClientRect
    // entry.intersectionRatio
    // entry.intersectionRect
    // entry.isKryds
    // entry.rootBounds
    // entry.target
    // entry.time
  });
};
const observatør = ny IntersectionObserver (callBack);

Valgfrit kan du videregive et objekt som en anden parameter til IntersectionObersver-konstruktøren. Dette objekt giver dig mulighed for at konfigurere observatøren. Du kan konfigurere 3 mulige egenskaber: root, rootMargin og tærskel.

Tærsklen egenskab kan enten være en enkelt ration, som 0,2, eller en række tærskler, ligesom [0,01, 0,02, 0,03, ....]. RootProperty er det element, der skal bruges som visningsport ved beregning af krydsforholdet. Root-egenskaben skal være en stamfar til det element, der observeres, og er som standard browservisporten. Endelig kan du indstille egenskaben rootMargin ved hjælp af CSS-margin-syntaks til at specificere et usynligt felt rundt om roden, som tærsklen beregnes med.

Så ovenstående eksempel kunne omskrives på denne måde:

const optioner = {
  root: domNode,
  rootMargin: '0px',
  tærskelværdi: [0,98, 0,99, 1]
}
const observatør = ny IntersectionObserver (callBack, indstillinger);

Vi har observatøren, men den observerer endnu ikke noget. For at starte den med at observere, skal du videregive en dom-knude til observationsmetoden. Den kan observere et vilkårligt antal knudepunkter, men du kan kun passere ad gangen. Når du ikke længere ønsker, at den skal observere en knude, kalder du unobserve-metoden og videregiver den den node, du gerne vil have, at den skal stoppe med at se, eller du kan ringe til afbrydelsesmetoden for at forhindre den i at observere en hvilken som helst knude, som denne:

observer.observer (nodeOne); // kun observere nodeOne
observer.observer (nodeTwo); // observerer både nodeOne og nodeTwo
observer.unobserve (nodeOne); // kun overvågning af nodeTo
observer.disconnect (); // ikke observerer nogen knude

Der er flere ting, men dette dækker de mest typiske anvendelsestilfælde af IntersectionObserver.

Hvordan bruger du det i en krog?

Først og fremmest skal vi være i stand til at angive den post, som IntersectionObserver returnerer fra tilbagekaldet. For at gøre dette bruger vi useState-krogen. Vi vil antage, at vi kun vil observere en knude ad gangen, så vi vil ødelægge posterne i den første post i matrixen og gemme den for at angive sådan:

Der er allerede en stor gotcha med dette. Hver gang komponenten genudgiver, kaldes useIntersect, hvilket betyder, at observatøren bliver instantieret hver gang med en ny IntersectionObserver. Dette er ikke den tilsigtede opførsel.

Hvad vi vil bruge er useRef-krog. UseRef-krog bruges ofte til at holde styr på en DOM-knude, så du kan gøre vigtige ting med det senere (f.eks. Give det fokus), men useRef kan bruges til at holde enhver værdi på tværs af tilbageleveringer. Vi får adgang til værdien af ​​en ref gennem den aktuelle ejendom på selve ref. Selve ref er mutatabel, og den aktuelle værdi kan tildeles igen når som helst, men vi får altid tilbage det samme ref-objekt med dets seneste værdi på hver tilbagelever.

Man kan spørge, hvad er forskellen mellem useRef og useState, da begge returnerer den aktuelle værdi. Den største forskel er, hvordan du opdaterer værdien, og hvad det betyder for resten af ​​komponenten, der bruger den. Du kan kun opdatere staten ved hjælp af den anden værdi, der er returneret fra useState, hvor ref's værdi kan opdateres når som helst ved at tildele en ny værdi til den aktuelle egenskab. Opdatering af værdien af ​​en ref signaliserer heller ikke en tilbagelever, hvor opdatering af staten vil.

Lad os opdatere vores krog til at bruge useRef:

Vores krog kommer sammen, men vi mangler stadig den vigtigste del: observationen. For vi har brug for to ting: en nodereference, og vi er nødt til at begynde at observere den ved hjælp af brugskontakten. Vi kan prøve at implementere det på denne måde:

Dette har allerede mange gotchas. Den første gotcha var ikke åbenbar for mig, før jeg læste mere om, hvordan brugenEffect-krogen virkede. Den funktion, du returnerer fra useEffect-krogen, køres, når komponenten er afmonteret, på den måde kan du rydde op i ting som at afbryde observatøren, hvilket er, hvad vi gør. Gotcha er, at da strøm kan muteres, er det ikke sikkert at bare få adgang til den fra den aktuelle egenskab. Hvis jeg af en eller anden grund i fremtiden besluttede at omdele den nuværende ejendom til noget andet, som null eller en anden observatør, kan oprydningsfunktionen muligvis ikke rydde op, som jeg forventer. Den sikre ting at gøre er at tildele den aktuelle egenskab til en variabel i hooken useEffect og derefter bruge variablen i stedet for den aktuelle egenskab direkte. Sådan her:

Den anden gotcha er, at denne effekt vil løbe igen og igen, fordi når observatøren ringer tilbagekaldet, vil den opdatere tilstanden, hvilket vil forårsage en tilbagelevering, hvilket vil bevirke, at useEffect-krogen køres igen. Den første tanke ville være at videregive knudepunkt i afhængighedsarray til brug af Effekt. Problemet er, at videregive dommeren har sin egen 'livscyklus' og ikke kan være pålidelig til at fortælle useEffect-krogen at springe over at køre denne gengivelse.

For at løse dette er vi nødt til at skifte fra at bruge useRef-krog til useState-krogen på denne måde:

Ved at videregive setNode-funktionen bruger vi callback ref-mønsteret i stedet for det nye ref-mønster (for at lære mere om de forskellige måder at håndtere refs på, kan du læse min begynderguide til refs i react post). Dette overfører noden til det tilbagekald, vi leverer, som i vores tilfælde vil opdatere staten til at være den nye node. Dette betyder, at ved første passering er noden nul, og derfor er vi nødt til at kontrollere for at sikre, at noden har en værdi, før vi forsøger at 'observere' den.

Ting ser godt ud, men der er endnu to ting, som vi skal overveje. Den første er, hvad sker der, hvis komponenten, der bruger krogen, ændrer den knude, som observatøren 'observerer'? Det udløser en tilstandsændring, da setRef kaldes på den nye knude. Da noden er ændret, kører useEffect igen, og den begynder at observere den nye node. Lyder godt, ikke? Hvad med den gamle knude? Din ret, vi stoppede aldrig med at observere den. Dette betyder, at der vil være mere end en post i tilbagekaldet, og vi gemmer muligvis den forkerte post til staten, for ikke at nævne det faktum, at vi observerer knudepunkter, som vi ikke længere er interesseret i.

For at løse dette er vi nødt til at frakoble observatøren, hver gang brug af Effekt kaldes sådan:

Dette vil sikre, at observatøren kun ser på den knude, vi holder af. Den sidste ting, vores krog har brug for, er at være i stand til at tilpasse vores observatør. Vi kan sende config-objektet ind i krogen. Vi kan give nogle standardværdier til indstillingerne, så vi giver en vis sikkerhed for den måde, vores krog kører på. Her er den endelige version af vores krog:

Nu har vi vores fuldt funktionsdygtige tilpassede krog, lad os se den i aktion. Jeg oprettede en kodesandkasse ved hjælp af vores nye useIntersect-krog. Du kan kigge igennem koden og se, hvordan den fungerer, men dybest set har jeg flere bokse, hver anden vil enten falme eller vokse, baseret på dens skæringsforhold. Du kan skifte hver boks enten i en fade- eller vokse-boks, og krogen står korrekt for nodens ændringer.

Hooks tilbyder en fantastisk måde at komponere funktionalitet, som en Intersection Observer, til en komponent. Når du har forstået, hvordan fungerer, kan du gøre nogle fantastiske ting.

Jeg har foretaget en opdatering baseret på kommentarer fra Porfírio Ribeiro. Der var yderligere to krydderier, som jeg ikke fandt før. Den første gotcha er, at krogen ikke opdaterer observatøren, hvis nogen af ​​konfigurationsværdierne ændres. De oprindelige værdier er de eneste, der respekteres på den første gengivelse og ignoreres derefter effektivt hver anden gengivelse.

Den anden gotcha er relateret til den første, hvilket er, at useRef vil bruge det, der er sendt som den første værdi, første gang det kaldes, men ignorerer det hver anden gengivelse. Hvis det var en simpel primitiv værdi, er det ikke noget, men vi konstruerer et nyt IntersectionObserver-objekt hver gengivelse, selvom det ignoreres på alle efterfølgende gengivelser.

Heldigvis kan vi løse begge problemer på én gang ved at flytte konstruktionen af ​​IntersectionObserver til funktionen useEffect, som denne:

Som du kan se, nu hvor observatøren kunne være null på den første gengivelse, så vi kobler kun fra, hvis den aktuelle værdi ikke er null. Vi konstruerer derefter IntersectionObserver og tildeler den til strøm. Derefter fungerer krogen nøjagtigt det samme. Dette giver os mulighed for derefter at tilføje konfigurationsindstillingerne som afhængigheder.

Det er vigtigt at bemærke, at da tærsklen kan være en matrix, er det vigtigt, at den array, der sendes ind, er den nøjagtige række hver gang. Det er fordi arrays passerer ved reference og useEffect kun vil kontrollere, om referencen ændres for at afgøre, om det er det, der skal køre useEffect callback igen. I koden sandkasse bruger jeg useMemo uden afhængighed for at sikre, at den matrix, der er sendt ind, er den samme matrix hver gang.