0

I'm trying to implement infinite scrolling on my react app for search hits from Algolia.

I came across a class component in their documentation. And I use React Hooks so tried to make it work on React Hooks. All I got was so many renders and my app gets hung up when this component mounts.

Here's my code:

import React, { useEffect, useRef } from 'react';
import Card from '@material-ui/core/Card';
import CardActionArea from '@material-ui/core/CardActionArea';
import CardActions from '@material-ui/core/CardActions';
import CardContent from '@material-ui/core/CardContent';
import CardMedia from '@material-ui/core/CardMedia';
import Button from '@material-ui/core/Button';
import IconButton from '@material-ui/core/IconButton';
import Typography from '@material-ui/core/Typography';
import Container from '@material-ui/core/Container';
import Grid from '@material-ui/core/Grid';
import { connectHits, connectInfiniteHits } from 'react-instantsearch-dom';
import noItemImage from './../../assets/img/noItemImage.png'
import { Link } from 'react-router-dom'
import ShareIcon from '@material-ui/icons/Share';
import PropTypes from 'prop-types';

function AlgoliaHits(props) {
    const { hits } = props
    console.log(hits)
    var sentinel = useRef(null)
    
    useEffect(() => {
        function onSentinelIntersection (entries){
            const { hasMore, refine } = props

            entries.forEach(entry => {
                if (entry.isIntersecting && hasMore) {
                    refine()
                }
            })
        }

        var observer = new IntersectionObserver(onSentinelIntersection, {})

        observer.observe(sentinel.current)
        return () => {
            observer.disconnect()
        }
    }, [props])

    return (
        <Container maxWidth="md" style={{ marginBottom: 100 }}>
            <Grid container spacing={2}>
                {
                    hits.map(hit => (
                        <Grid item xs={12} sm={6} md={4} lg={4} xl={3}>
                            <Link to={`/item/${hit.item_id}`} style={{ textDecoration: 'none' }}>
                                <Card maxWidth={210} key={hit.item_id} elevation={0}>
                                    <CardActionArea>
                                        <CardMedia
                                            component="img"
                                            alt="Contemplative Reptile"
                                            height="140"
                                            image={
                                                hit.item_images_url ? 
                                                    hit.item_images_url.length === 0 ?
                                                    noItemImage
                                                    :
                                                    hit.item_images_url[0]
                                                : 
                                                noItemImage
                                            }
                                            title={hit.item_name}
                                        />
                                        <CardContent>
                                            <Typography gutterBottom variant="h5" component="h2"
                                                style={{ whiteSpace: 'nowrap', width: 250, overflow: 'hidden', textOverflow: 'ellipsis' }}>
                                                {hit.item_name}
                                            </Typography>
                                            <Typography variant="body2" color="textSecondary" component="p" 
                                                style={{ whiteSpace: 'nowrap', width: 200, overflow: 'hidden', textOverflow: 'ellipsis'  }}>
                                                {hit.item_description}
                                    </Typography>
                                        </CardContent>
                                    </CardActionArea>
                                    <CardActions>
                                        <Button size="small" color="primary" component={Link} to={`/item/${hit.item_id}`}>
                                            View
                                        </Button>
                                        <IconButton size="small" color="secondary">
                                            <ShareIcon style={{ padding: 4 }}/>
                                        </IconButton>
                                    </CardActions>
                                </Card>
                            </Link>
                        </Grid>                        
                    ))
                }
            </Grid>
            <div id="sentinel" ref={sentinel} />
        </Container>
    );
}

AlgoliaHits.propTypes = {
    hits: PropTypes.arrayOf(PropTypes.object).isRequired,
    hasMore: PropTypes.bool.isRequired,
    refine: PropTypes.func.isRequired,
}

const AlgoliaInfiniteScroll = connectHits(AlgoliaHits)

const ItemCard = connectInfiniteHits(AlgoliaInfiniteScroll)

export default ItemCard

And here's where I used the reference from. What wrong am I doing? And how to solve it?

TIA

Sarath Dev
  • 83
  • 1
  • 1
  • 9

1 Answers1

0

My first thought here is that the issue may be in the useEffect dependency array.

The useEffect hook will trigger whenever one of it's dependencies changes. In your case, you've specified the dependency array as [props], meaning every time a prop changes, it will trigger once more. As props likely changes quite often, for objects such as props.hits, I'd wager this is throwing your code into the infinite loops you mention.

I believe in the original example, the onSentinelIntersection functionality only occurs once, when the component mounts.

I'd start with an empty dependency array: [] at the end of your useEffect. This is a (albeit a little bit hacky) way to replace componentDidMount and only run once, when the component's first render.

If that helps, you may be most of the way there. I would also recommend, however, moving the onSentinelIntersection function out of the useEffect (or even completely out of the component, if possible!) - as it's good practice to keep as much code outside of a useEffect as possible (as it'll be evaluated every time it executes).

So something like:

function AlgoliaHits(props) {
    const { hits } = props
    console.log(hits)
    var sentinel = useRef(null)
    
    function onSentinelIntersection (entries){
        const { hasMore, refine } = props

        entries.forEach(entry => {
            if (entry.isIntersecting && hasMore) {
                refine()
            }
        })
    }

    useEffect(() => {
        var observer = new IntersectionObserver(onSentinelIntersection, {})
        observer.observe(sentinel.current)

        return () => observer.disconnect()
    }, [])

    ....

May be a good start. You may be able to safely add [IntersectionObserver, onSentinelIntersection] to the useEffect dependencies too, as they're technically required for the useEffect to function, and shouldn't cause any further triggers of useEffect in this case.

The Dependencies & Performance section of the React useEffect docs is helpful here- as well as the whole page itself.

For extra resources, I really got a lot of mileage out of The Complete Guide To useEffect by Dan Abramov - Long read, but worth it, I think (when you have the time, that is!)

oheydrew
  • 36
  • 8