React Material & Infinite Scroll to Load MUI Cards

Introduction

Infinite scrolling dynamically loads more content on a page once a user scrolls to the bottom of the page. Most use cases are to display cards with images or videos like YouTube, Amazon Prime Video, Hulu etc.

A downside, it may have a negative impact on SEO. For instance, Google enables JavaScript to load only a part of the content of a given page 

Source Files

GitHub

Libraries Needed

MUI – Material UI v5.4.3

Material UI library for React

npm install @mui/material @mui/icons-material @mui/lab @emotion/react @emotion/styled

react-infinite-scroll library for loading more cards while scrolling down.

npm install react-infinite-scroll-component

MUI Card Component

This component renders a MUI card and uses the onMouseEnter() and onMouseLeave() events to expand and collapse the card.

Below is the source for ‘MUICard.js’.

import React, {  useState, useEffect } from 'react'
import Card from '@mui/material/Card';
import CardHeader from '@mui/material/CardHeader';
import CardMedia from '@mui/material/CardMedia';
import CardContent from '@mui/material/CardContent';
import CardActions from '@mui/material/CardActions';
import Button from '@mui/material/Button';
import Avatar from '@mui/material/Avatar';
import Box from '@mui/material/Box';
import Divider from '@mui/material/Divider';
import Stack from '@mui/material/Stack';
import GroupIcon from '@mui/icons-material/Group';
import FavoriteBorderIcon from '@mui/icons-material/FavoriteBorder';
import Badge from '@mui/material/Badge';
import PrizeEventsIcon from '@mui/icons-material/EmojiEvents';
import IconButton from '@mui/material/IconButton';
import FavoriteIcon from '@mui/icons-material/Favorite';
import Grid from '@mui/material/Grid';
import { red } from '@mui/material/colors';

export default function MUICard(props) {
  
  const [isLiked, setIsLiked] = useState(false)
  const [isEventOpen, setIsEventOpen] = useState(false)  
  const [isExpandCard, setIsExpandCard] = useState(false)
 

  const expandCard = () => {        
    setIsExpandCard(true)
  }
  
  const collapseCard = () => {    
    setIsExpandCard(false)
  }
        
  const  cardRoot = {    
    minWidth: 320,
    maxHeight: 500,
    marginLeft: '20px',
    transition: 'transform 0.5s' 
  }

  const  cardRootExpand = {    
    position: 'absolute',
    minWidth: 340,
    maxHeight: 520,
    marginLeft: '20px',
    transform: 'scale(1.1)',    
    transition: 'transform 0.5s', 
    zIndex: 99,
  }
  
  return (
    <Card style={isExpandCard ? cardRootExpand : cardRoot} 
          onMouseEnter={() => expandCard()}
          onMouseLeave={() => collapseCard()}>
      <CardHeader
        avatar={         
            <Avatar sx={{ bgcolor: red[500] }} aria-label="event">
              R
            </Avatar>                             
        }

        title={props.data.title}
        subheader={
          <b>Sub Header</b>
        }
      >
      </CardHeader>

      <CardMedia  media={'image'}  />

          <Box sx={{display: "flex",  justifyContent: "center"}}>
              <img src={'/contest.svg'}
                width="240"
                height="240"
                layout="intrinsic"/>
          </Box>

      <CardContent>
        
        
        <b>{isEventOpen && 'Vote Now'}</b>          
        
      </CardContent>
      <CardActions disableSpacing>

        <Stack spacing={3}>

          <Stack spacing={3} direction="row">
            <IconButton aria-label="add to favorites">
            {isLiked ?
              <FavoriteIcon color={'error'}/>
            :
                <FavoriteBorderIcon color={'primary'}/>
            }
            </IconButton>
                                        
            <IconButton aria-label="share">
                <Badge badgeContent={4} color="success">
                <GroupIcon />
                </Badge>              
            </IconButton>
            
                        
            
            <Button  variant="outlined" size="small" color="primary">Open</Button>


        </Stack>

  
          {isExpandCard &&
          <>
            <Divider/>                
            <Grid container spacing={1}>
                <Grid item xs={6} align="left">                        
                    <Button variant='outlined'>Watch Later</Button>
                </Grid>

                <Grid item xs={6} align="right">
                    <Button variant='outlined' color="warning">Add to Queue</Button>
                </Grid>
            </Grid>                        
          </>
          }
        </Stack>        
      </CardActions>
    </Card>    
  );
}

Breakdown of the MUICard.js Component

  const [isExpandCard, setIsExpandCard] = useState(false)
 
  const expandCard = () => {        
    setIsExpandCard(true)
  }
  
  const collapseCard = () => {    
    setIsExpandCard(false)
  }

CSS and JS to Animate the Card

Define the CSS for initial render and hover.

const cardRoot = {    
    minWidth: 320,
    maxHeight: 500,
    marginLeft: '20px',
    transition: 'transform 0.5s' 
}

const cardRootExpand = {    
    position: 'absolute',
    minWidth: 340,
    maxHeight: 520,
    marginLeft: '20px',
    transform: 'scale(1.1)',    
    transition: 'transform 0.5s', 
    zIndex: 99
}

Using the onMouseEnter() and onMouseLeave() events to to change the CSS defined above.

<Card style={isExpandCard ? cardRootExpand : cardRoot} 
          onMouseEnter={() => expandCard()}
          onMouseLeave={() => closeCard()}>
    

App.js Root Component

Below is the source for ‘app.js’. This component uses the <Grid/> component to render cards from JSON data with 16 records with titles. The MUICards are optimized for a 16:9 monitor.

import * as React from 'react';
import Box from '@mui/material/Box';
import Grid from '@mui/material/Grid';
import MUICard from './components/MUICard';
import InfiniteScroll from 'react-infinite-scroll-component';
import Skeleton from '@mui/material/Skeleton';
import Stack from '@mui/material/Stack';
import Container from '@mui/material/Container';

function App() {
  const [items, setItems] = React.useState(data)
  const [length, setLength] = React.useState(11)
  const maxRecordsReturned = 4;
  const skeletonItems = [0, 1, 2, 3]

  React.useEffect(() => {
    var tmpArr = []
    data.map((elem, i) => {
        if(i <= 11) {
            tmpArr.push(elem);
        }        
    });
    setItems(data.slice(0,12))
    setLength(12)
  }, [])
    
  const fetchMoreData = () => {
      setTimeout(() => {        
          setItems(data.slice(0,(length + maxRecordsReturned)))
          setLength(length + maxRecordsReturned)        
      }, 1000);
  };
 
  const renderLoading = () => {
    return (
      <>
      <Grid container spacing={3} sx={{mt: 1, ml: '1px'}}>
      {skeletonItems.map((item, i) => (
        <Grid item xs={12} sm={12} md={4} lg={4} xl={3}>

          <Stack spacing={1} key={i}>
            <Stack spacing={1} direction="row" key={i}>
              <Skeleton variant="circular" width={50} height={50} key={i}/>
              <Skeleton variant="text" width={260} height={40} key={i}/>
            </Stack>
            
            
            <Skeleton sx={{ height: 360 }} animation="wave" variant="rectangular" key={i}/>

          </Stack>
        </Grid>
      ))}
      </Grid>
    </>
    )
   }  
  return (
    <>        
      <Box sx={{mt:5}}>
        <Container maxWidth="xl" >
          <InfiniteScroll
              dataLength={items.length-1}
              // style={{ display: 'flex', flexDirection: 'column-reverse' }}
              next={fetchMoreData}           
              hasMore={items.length < 28? true : false}
              loader={renderLoading()}
              >
              <Grid container spacing={2}>
                    {items.map((elem, index) => (
                        <Grid item xs={12} sm={12} md={4} lg={4} xl={3}>
                          {/* <MUIVideoCard data={elem} key={index}/> */}
                          <MUICard data={elem} key={index}/>
                        </Grid>
                    ))}
              </Grid>
          </InfiniteScroll>
        </Container>
      </Box>
    
    </>
  );
}



export default App;



const data = [
  {id: 1, title: 'Magnificent Visions'},
  {id: 2, title: 'The Last Snake'},
  {id: 3, title: 'Blade of Year'},
  {id: 4, title: 'The Ways Captive'},
  {id: 5, title: 'Silk in the Abyss'},
  {id: 6, title: 'Growing Star'},
  {id: 7, title: 'The Last Snake'},
  {id: 8, title: 'The Cracked Willow'},
  {id: 9, title: 'Heart of Servant'},
  {id: 10, title: 'The Scent of the Door'},
  {id: 11, title: 'Flames in the End'},
  {id: 12, title: 'Professional Serpents'},
  {id: 13, title: 'Hunter of Storm'},
  {id: 14, title: 'Storm Illusion'},
  {id: 15, title: 'The Abyss'},
  {id: 16, title: 'Sucking Name'},
  {id: 17, title: 'Moon in the Truth'},
  {id: 18, title: 'Silent Rings'},
  {id: 19, title: 'Roses of Vision'},
  {id: 20, title: 'The Crying Room'},
  {id: 21, title: 'Magnificent Visions'},
  {id: 22, title: 'The Last Snake'},
  {id: 23, title: 'Blade of Year'},
  {id: 24, title: 'The Ways Captive'},
  {id: 25, title: 'Silk in the Abyss'},
  {id: 26, title: 'Growing Star'},
  {id: 27, title: 'The Last Snake'},
  {id: 28, title: 'The Cracked Willow'},    
]

Display Skeletons While Loading

Render four MUI Skeletons in the list while scrolling.

Below is the code for the MUI Skeletons. Using Skeleton and Stack from MUI render a circle and small rectangle over a large rectangle similar to the MUI Card. The delay to render is set to 1 second in the fetchMoreData() function.

import * as React from 'react';
import Skeleton from '@mui/material/Skeleton';

function App() {

  const skeletonItems = [0, 1, 2, 3]

  const fetchMoreData = () => {
      setTimeout(() => {        
          setItems(data.slice(0,(length + maxRecordsReturned)))
          setLength(length + maxRecordsReturned)        
      }, 1000);
  };

  const renderLoading = () => {
    return (
      <>
      <Grid container spacing={3} sx={{mt: 1, ml: '1px'}}>
      {skeletonItems.map((item, i) => (
        <Grid item xs={12} sm={12} md={4} lg={4} xl={3}>

          <Stack spacing={1} key={i}>
            <Stack spacing={1} direction="row" key={i}>
              <Skeleton variant="circular" width={50} height={50} key={i}/>
              <Skeleton variant="text" width={260} height={40} key={i}/>
            </Stack>
            
            
            <Skeleton sx={{ height: 360 }} animation="wave" variant="rectangular" key={i}/>

          </Stack>
        </Grid>
      ))}
      </Grid>
    </>
    )
 }  

Infinite Scroll

Create a list of the first twelve elements using the JavaScript slice() operator. The function fetchMoreData() gets an additional four records (defined in constant maxRecordsReturned) each time it is called from <InfiniteScroll>. <InfiniteScroll> wraps a MUI Grid.

import InfiniteScroll from 'react-infinite-scroll-component';

function App() {
  const [items, setItems] = React.useState(data)
  const [length, setLength] = React.useState(11)
  const maxRecordsReturned = 4;
  const skeletonItems = [0, 1, 2, 3]

  React.useEffect(() => {
    var tmpArr = []
    data.map((elem, i) => {
        if(i <= 11) {
            tmpArr.push(elem);
        }        
    });

    setItems(data.slice(0,12))
    setLength(12)
  }, [])
    
  const fetchMoreData = () => {
      setTimeout(() => {        
          setItems(data.slice(0,(length + maxRecordsReturned)))
          setLength(length + maxRecordsReturned)        
      }, 1000);
  };
 
  return (
    <>        
      <Box sx={{mt:5}}>
        <Container maxWidth="xl" >
          <InfiniteScroll
              dataLength={items.length-1}
              next={fetchMoreData}           
              hasMore={items.length < 28? true : false}
              loader={renderLoading()}
              >
              <Grid container spacing={2}>
                    {items.map((elem, index) => (
                        <Grid item xs={12} sm={12} md={4} lg={4} xl={3}>
                          {/* <MUIVideoCard data={elem} key={index}/> */}
                          <MUICard data={elem} key={index}/>
                        </Grid>
                    ))}
              </Grid>
          </InfiniteScroll>
        </Container>
      </Box>    
    </>
  );
}