reactjs - Intersection Observer API går in i en oändlig renderingsloop


Jag försöker använda intersection observer API för att villkorligt visa objekt i ett CSS-grid när användaren börjar scrolla, men det verkar gå in i en oändlig renderingloop. Här är min kod.

Här är länken till min livekod på StackBlitz

Det jag också försöker uppnå är att inte rendera för många objekt på skärmen när jag kan undvika det. Jag är inte säker på om display: none faktiskt gör att webbläsaren arbetar mindre. Om detta inte är rätt sätt, vänligen låt mig veta.

Tack för att du läste min fråga. All hjälp uppskattas mycket.

Svar #1

Problem: Samma referens på 1000 element

Du har 1000 GridItem-komponenter som alla får samma callback-ref setRefs. De tar alla emot samma värde för inView även om vi vet att vissa vid en given tidpunkt är i vy och andra inte är det. Det som händer är att varje item skriver över den tidigare inställda refen så att alla 1000 items får en boolean inView som representerar om det sista itemet i listan är i vy - inte om det själv är i vy.

Lösning: useInView för varje element

För att veta om varje enskild komponent är synlig eller inte behöver vi använda useInView-hooken separat för varje element i listan. Vi kan flytta koden för varje objekt till sin egen komponent. Vi behöver skicka detta komponent dess nummer ix och alternativen för useInView-hooken (vi skulle också kunna bara skicka ner root-ref och skapa options-objektet här).

import { Box, Flex, Text, useColorModeValue as mode, Divider, Grid, GridItem, Center } from '@chakra-ui/react';
import { useInView, IntersectionOptions } from 'react-intersection-observer';
import React, { useRef } from 'react';

interface ItemProps {
  ix: number;
  inViewOptions: IntersectionOptions;
}

export const ListItem = ({ix, inViewOptions}: ItemProps) => {
  const {ref, inView}= useInView(inViewOptions);

  return (
    <GridItem bg={inView?"red.100":"blue.100"} ref={ref} _last={{ mb: 4 }} key={ix}>
      <Center  border={1} borderColor="gray.100" borderStyle="solid" h={16} w="100%">
        Item {ix}
      </Center>
    </GridItem>
  )
}


export type PortfolioListProps = {
  title: string;
};

export const PortfolioList = ({ 
  title,
}: PortfolioListProps) => {
  const parRef = useRef(null);

  return (
    <Box
      w="100%"
      mx="auto"
      rounded={{ md: `lg` }}
      bg={mode(`white`, `gray.700`)}
      shadow="md"
      overflow="hidden"
    >
      <Flex align="center" justify="space-between" px="6" py="4">
        <Text as="h3" fontWeight="bold" fontSize="xl">
          {title}
        </Text>
      </Flex>
      <Divider />
      <Grid
        p={4}
        gap={4}
        templateColumns="1fr 1fr 1fr 1fr"
        templateRows="min-content"
        maxH="500px"
        minH="500px"
        overflowY="auto"
        id="list"
        ref={parRef}
      >
        {[...Array(1000)].map((pt,ix) => (
          <ListItem ix={ix} key={ix} inViewOptions={{
            threshold: 1,
            rootMargin: '0px',
            root: parRef?.current,
          }}/>
        ))}
      </Grid>
    </Box>
  );
};

StackBlitz-länk

Svar #2

Komponenten fungerar inte på det sätt du förväntar dig eftersom du använder samma referens för alla objekt. Du kan använda ref för att lagra en array med referenser eller skapa en komponent med logiken för listobjekten.

Om du inte vill rendera alla objekt samtidigt kan du rendera en del (100), och varje gång scroll når slutet, rendera 100 till och så vidare. Jag rekommenderar att du använder React.memo för att undvika att rendera objektet varje gång tillståndet uppdateras:

PortfolioItem.js

const PortfolioItem = React.memo(({ ix }) => {
  const ref = useRef();
    const [inViewRef, inView] = useInView({
    threshold: 1,
    rootMargin: '0px',
  });
    const setRefs = useCallback(
    (node) => {
      ref.current = node;
      inViewRef(node);
    },
    [], //--> empty dependencies
  );

  return ( <GridItem bg={inView?"red.100":"blue.100"} ref={setRefs} _last={{ mb: 4 }} >
              <Center  border={1} borderColor="gray.100" borderStyle="solid" h={16} w="100%">
                Item {ix}
              </Center>
            </GridItem>)
});

PortfolioLista.js

export const PortfolioList = ({ 
  title,
  count = 100
}: PortfolioListProps) => {
  const ref = useRef(null);
  const items = [...Array(1000)];
  const [index, setIndex] = useState(count);
  
  useEffect(()=> {
    const grid = ref.current;
    function onScroll(){
      if(grid.offsetHeight + grid.scrollTop >= grid.scrollHeight) {
           setIndex(prev => prev+count);
      }
    }
    grid.addEventListener("scroll", onScroll);
    return ()=> {
       grid.removeEventListener("scroll", onScroll);
    }
  }, []);

  return (
    <Box
      w="100%"
      mx="auto"
      rounded={{ md: `lg` }}
      bg={mode(`white`, `gray.700`)}
      shadow="md"
      overflow="hidden"
    >
      <Flex align="center" justify="space-between" px="6" py="4">
        <Text as="h3" fontWeight="bold" fontSize="xl">
          {title}
        </Text>
      </Flex>
      <Divider />
      <Grid
        p={4}
        gap={4}
        templateColumns="1fr 1fr 1fr 1fr"
        templateRows="min-content"
        maxH="500px"
        minH="500px"
        overflowY="auto"
        id="list"
        ref={ref}
      >
        {items.slice(0,index).map((pt,ix) => (
            <PortfolioItem ix={ix} key={`Postfolio__item-${ix}`}/>
          ))
         }
      </Grid>
    </Box>
  );
};

Arbetande exempel

Relaterade artiklar