Soham Dasgupta
Soham Dasgupta

Reputation: 5199

Intersection Observer API going into infinite rendering loop

I am trying to use the intersection observer API to conditionally display items in a CSS grid when the user starts scrolling, but it seems to go into an infinite rendering loop. Here is my code.

Here is the link to my live code on StackBlitz

Also what I'm trying to achieve is not render too many items on the screen when I can avoid it. I'm not sure if display: none actually makes the browser work less. If this is not the correct way, please let me know.

Thanks for reading my question. Any help is highly appreciated.

Upvotes: 4

Views: 4110

Answers (2)

lissettdm
lissettdm

Reputation: 13078

The component is not working the way you expect because you are using the same reference for all the items. You can use ref to store an array of reference or create a component with the list item logic.

If you don't want to render all the items at the same time, you can render a portion (100), and every time the scroll reaches the end, render 100 more and so on. I recommend you to use React.memo to avoid render the item every time the state updates:

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>)
});

PortfolioList.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>
  );
};

Working example

Upvotes: 2

Linda Paiste
Linda Paiste

Reputation: 42228

Problem: Same Ref on 1000 Elements

You have 1000 GridItem components which are all getting the same callback ref setRefs. They all receive the same value of inView even though we know that at any given time some are in view and others are not. What ends up happening is that each items overwrites the previously set ref such that all 1000 items receive a boolean inView that represents whether the last item in the list is in view -- not whether it is itself in view.

Solution: useInView for Each Element

In order to know whether each individual component is in view or not, we need to use the useInView hook separately for each element in the list. We can move the code for each item into its own component. We need to pass this component its number ix and the options for the useInView hook (we could also just pass down the root ref and create the options object here).

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 Link

Upvotes: 2

Related Questions