React Material & Infinite Scroll to Load MUI Cards

Table of Contents
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
GitHubLibraries 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>
</>
);
}
You must be logged in to post a comment.