5

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.

Soham Dasgupta
  • 5,061
  • 24
  • 79
  • 125
  • 4
    You'll need to slim down your code to a minimal verifiable reproducible snippet. – Mordechai Mar 08 '21 at 14:12
  • 2
    This is a large piece of code. Can you try to trim the sections that are unrelated to the issue? For example try commenting first some of the parts that you think are unrelated, and if the issue persist remove them entirely. Linking a code-sandbox (or similar) with a minimal repro case would be ideal. – Anthony Garcia-Labiad Mar 08 '21 at 14:21
  • @Mordechai please see my updated question with the live code link. – Soham Dasgupta Mar 08 '21 at 18:13
  • 1
    something like https://github.com/bvaughn/react-virtualized ? – diedu Mar 09 '21 at 22:22
  • 1
    Take a look at [react-window](https://github.com/bvaughn/react-window). It allows you to render efficiently large lists and tabular data. – Ruslan Gilmutdinov Mar 11 '21 at 12:56

2 Answers2

2

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

Linda Paiste
  • 38,446
  • 6
  • 64
  • 102
2

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

lissettdm
  • 12,267
  • 1
  • 18
  • 39