reactjstypescriptchakra-uireact-intersection-observer

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.


Solution

  • 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