Infinite Scroll using Algolia on React - reactjs

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

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!)

Related

react movie app breaking when navigating movie detail page

i am creating a react movie app that uses an movie app API from rapid API. on home page everything is working perfectly and I am able to render all movies in the form of cards. i have set the app in such a way that when user clicks on movie card it should direct to Moviedetail page. for this i am using react-router-dom and everything is set up right. on clicking movie card it does navigate to Moviedetail page but nothing is being rendered and getting this error in browser console.
(Uncaught TypeError: Cannot read properties of undefined (reading 'length')
at Details (Details.js:18:1));
but if i comment Detail and MovieTrailers component in MovieDetails page and uncomment it then everything on MovieDetails page is rendered perfectly. again if i refresh the page it breaks again and i get the same browser console error.
import React, {useState, useEffect} from "react";
import {Box} from "#mui/material";
import {useParams} from "react-router-dom";
import MovieTrailers from "../components/MovieTrailers";
import Details from "../components/Details";
import {fetchData, options} from "../utils/fetchData";
function MovieDetails() {
const [movieDetailData, setMovieDetailData] = useState({});
const [movieTrailerData, setMovieTrailerData] = useState([]);
const {id} = useParams();
// console.log(id);
console.log(movieDetailData);
console.log(movieTrailerData);
useEffect(()=>{
const fetchMovieData = async ()=> {
const detailData = await fetchData(`https://movies-app1.p.rapidapi.com/api/movie/${id}`, options);
setMovieDetailData(detailData.result);
const trailerData = await fetchData(`https://movies-app1.p.rapidapi.com/api/trailers/${id}`, options);
setMovieTrailerData(trailerData.result);
}
fetchMovieData();
},[id]);
return(
<Box>
<Details
movieDetailData={movieDetailData}
/>
<MovieTrailers
movieTrailerData={movieTrailerData}
/>
</Box>)
}
export default MovieDetails;
////here is Details component
import React from "react";
import {Box, Stack, Typography, Chip} from "#mui/material";
function Details(props) {
const{image, titleOriginal, countries, genres, rating, release, description} = props.movieDetailData;
return <Stack direction="row" width="100%" sx={{backgroundColor:"#fff"}}>
<img className="movie-poster" src={image} alt="movie-poster"/>
<Stack p="50px" sx={{width:"60%"}}>
<Typography variant="h2" fontWeight="700">
{titleOriginal}
</Typography>
<Box pt="30px" sx={{display:"flex", gap:"30px"}}>
<Chip label={<Typography variant="h6" fontWeight="700">{`rating : ${rating}`}</Typography>} />
<Chip label={<Typography variant="h6" fontWeight="700">{`release: ${release}`}</Typography>} />
</Box>
<Typography variant="h6" pt="30px">
{description.length>600 ? description.substring(0, 601) : description}
</Typography>
<Box pt="30px" sx={{display:"flex", gap:"30px"}}>
<Chip label={<Typography variant="h6" fontWeight="700">{`genre : ${genres[0].name}`}</Typography>} />
<Chip label={<Typography variant="h6" fontWeight="700">{`origin : ${countries[0].name}`}</Typography>} />
</Box>
</Stack>
</Stack>
}
export default Details;
////here is MovieTrailer component
import React from "react";
import {Box, Stack, Typography} from "#mui/material";
import TrailerCard from "./TrailerCard";
import {nanoid} from "nanoid";
function MovieTrailers({movieTrailerData}) {
console.log(movieTrailerData);
return <Box p="30px">
<Typography variant="h4" pt="40px" pb="50px" fontWeight="700" m="auto">{`Watch ${movieTrailerData[0].title}`}
</Typography>
<Stack direction="row" justifyContent="space-between">
{(movieTrailerData.slice(0, 3)).map((trailer)=>(
<TrailerCard trailer={trailer}/>
))}
</Stack>
</Box>
}
export default MovieTrailers;
////here is TrailerCard component
import React from "react";
import {Box, Stack, Typography, Chip} from "#mui/material";
function TrailerCard({trailer}) {
const{thumbnail, ago, author, views, title, url, description} = trailer;
return <Box className="trailer-card" p="10px" backgroundColor="#fff" width="25%">
<Stack>
<a
className="movie-trailer-link"
href={url}
target="_blank"
rel="noreferrer">
<img className="movie-trailer-thumbnail" src={thumbnail} alt="movie-trailer"/>
<Typography pt="10px" variant="body1" fontWeight="700" >
{title}
</Typography>
<Stack paddingBlock="10px" direction="row" justifyContent="space-between" gap="10px" flexWrap="wrap">
<Chip label={`views: ${views}`} />
<Chip label={`ago: ${ago}`}/>
</Stack>
<Typography variant="subtitle2">{`YT: ${author.name}`}</Typography>
<Typography variant="body2" pb="20px">
{description}
</Typography>
</a>
</Stack>
</Box>
}
export default TrailerCard;
In your movie detail component, add a loading state initially set to true. Upon resolution of the API call in the useEffect, set the loading variable to false.
In the jsx, render the page data if loading is false else show a loader or something else
movieDetailData is an empty object before you fetch your data from backend. That's why you get an error.
First check if your props has suitable keys.
import React from "react";
import {Box, Stack, Typography, Chip} from "#mui/material";
function Details(props) {
if (Object.keys(props.movieDetailData).length == 0)
return null;
const{image, titleOriginal, countries, genres, rating, release, description} = props.movieDetailData;
// rest of your code
}

Unable to navigate to another web page using useNavigate in my ReactJS app

I am trying to navigate to a different web page using 'useNavigate'. I am getting the error message below and I don't quite understand what I am doing incorrectly. Please can someone advise?
import React from 'react';
import AppBar from '#mui/material/AppBar';
import Box from '#mui/material/Box';
import Toolbar from '#mui/material/Toolbar';
import Typography from '#mui/material/Typography';
import Button from '#mui/material/Button';
import IconButton from '#mui/material/IconButton';
import MenuIcon from '#mui/icons-material/Menu';
import { useNavigate } from 'react-router-dom';
import SendIcon from '#mui/icons-material/Send';
const Header = () => {
const SendMessage = () => {
console.log('Send message!');
const navigate = useNavigate();
navigate('/writeMessage');
}
return (
<Box sx={{ flexGrow: 1 }}>
<AppBar position="static">
<Toolbar>
<IconButton
size="large"
edge="start"
color="inherit"
aria-label="menu"
sx={{ mr: 2 }}
>
<MenuIcon />
</IconButton>
<Typography variant="h6" component="div" sx={{ flexGrow: 1 }}>
MyApp
</Typography>
<SendIcon sx={{ marginRight: "20px" }} onClick={SendMessage}>
</ SendIcon>
<Button variant="contained" >Connect Wallet</Button>
</Toolbar>
</AppBar>
</Box>
);
}
export default Header;
Console:
Uncaught Error: Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:
1. You might have mismatching versions of React and the renderer (such as React DOM)
2. You might be breaking the Rules of Hooks
3. You might have more than one copy of React in the same app
See https://reactjs.org/link/invalid-hook-call for tips about how to debug and fix this problem.
You are calling the useNavigate hook in a nested function. This breaks one of the Rules of Hooks, "Don’t call Hooks inside loops, conditions, or nested functions."
Move it to the component scope.
const Header = () => {
const navigate = useNavigate();
const SendMessage = () => {
console.log('Send message!');
navigate('/writeMessage');
};
...

How do I set the onClick in the navbar of material-ui?

Below is the code for creating the navigation bar/header for an application. I have used material-ui.
import React, { Component } from "react";
import PropTypes from "prop-types";
import { withStyles } from "#material-ui/styles";
import AppBar from "#material-ui/core/AppBar";
import Toolbar from "#material-ui/core/Toolbar";
import Typography from "#material-ui/core/Typography";
import IconButton from "#material-ui/core/IconButton";
import MenuIcon from "#material-ui/icons/Menu";
import AccountCircle from "#material-ui/icons/AccountCircle";
import ShoppingCartOutlinedIcon from "#material-ui/icons/ShoppingCartOutlined";
const styles = (theme) => ({
root: {
width: "100%",
},
flex: {
flex: 1,
},
menuButton: {
marginLeft: -12,
marginRight: 20,
},
});
class Nav extends Component {
render() {
const { classes } = this.props;
return (
<AppBar position="static" elevation={0}>
<Toolbar>
<IconButton
className={classes.menuButton}
color="contrast"
onClick={this.props.toggleDrawer}
>
<MenuIcon />
</IconButton>
<Typography className={classes.flex} type="title" color="inherit">
Pizza Shop
</Typography>
<div>
<IconButton color="contrast" onClick={this.props.cart}>
<ShoppingCartOutlinedIcon />
</IconButton>
<IconButton color="contrast" onClick={this.props.login}>
<AccountCircle />
</IconButton>
</div>
</Toolbar>
</AppBar>
);
}
}
Nav.propTypes = {
classes: PropTypes.object.isRequired,
};
export default withStyles(styles)(Nav);
I'm new to 'props' and I'm not sure what this.props.cart or this.props.login will do. I want to create a functionality where when I click on the above two icons, I'm directed to some another component, but I'm not sure how to do it. Please help me to understand it.
Props are just parameters that the parent component sent to the children component. In your example this.props.cart and this.props.login are functions ( I am not sure about the cart, but it is used as a function ). From your example, when you click on the icons, you will call cart and login functions sent from the parent.
The answer to your question "How do you set onClick" is you already doing it on the login function/method. So you need to look at the parent component implementation.
Below I wrote a much more readable example, so feel free to look
import React from 'react'
class ChildrenComponent extends React.Component {
render() {
// This is doing the same thing, just we call the function / method in little different way
// return <div onClick={() => { this.props.onShoppingSubmit() }}>Aaa</div>
return <div onClick={this.props.onShoppingSubmit}>Aaa</div>
}
}
class ParentComponent extends React.Component {
handleShoppingSubmit = () => {
// Do what every that function needs to do
console.log('Hi from parent')
}
render() {
return (
<div>
<ChildrenComponent onShoppingSubmit={this.handleShoppingSubmit} />
</div>
)
}
}

Changed style onClick applies to all the ListItems instead of single ListItems

I am developing a menu item the code of is provided below. I am trying to change the style of the menu item when it is clicked. I have applied a function clickHandler to toggle between selected and not selected states. Based on the state of the menuItem, I want to change its style. In current scenario, if I clicked on any ListItem, the changed style is applied to all the ListItems instead of one. How can I achieve it?
import React, {Fragment, useCallback, useState} from 'react';
import {makeStyles, withStyles} from '#material-ui/core/styles';
import AppBar from '#material-ui/core/AppBar';
import {Link} from "react-router-dom";
import ListItemText from "#material-ui/core/ListItemText";
import ListItem from "#material-ui/core/ListItem";
import Typography from "#material-ui/core/Typography";
import Toolbar from "#material-ui/core/Toolbar";
import Box from "#material-ui/core/Box";
import List from "#material-ui/core/List";
import clsx from "clsx";
const useStyles = makeStyles((theme) => ({
root: {
width: '100%',
backgroundColor: theme.palette.background.paper,
},
appBar: {
shadowColor: '#206a5d',
margin: 0
},
active: {
color: '#206a5d',
borderBottom: '2px solid blue'
},
}));
export function Submenu({items, header}) {
const classes = useStyles();
const [active, setActive] = useState(false)
const clickHandler = () => {
setActive((prev) => !prev);
};
return (
<Fragment>
<div className={classes.root}>
<AppBar position="sticky"
color="inherit"
className={classes.appBar}
>
<Typography style={{marginLeft: '1.5rem'}}>
<h1 style={{color:'#1f4068'}}>{header}</h1>
</Typography>
<div style={{display: 'flex',}}
>
{items.map(item =>
<ListItem button
key={item.title}
onClick={clickHandler}
component={Link}
className={clsx(null, {[classes.active]: !active})}
to={item.to || '/404'}
style={{padding:'0.75rem', width: 'auto'}}
>
{item.title}
</ListItem>
)}
</div>
</AppBar>
</div>
</Fragment>
);
}
If you want to set the style of a single ListItem, you need to provide an identifier as a parameter to clickHandler so that you can identify which one is clicked and according to that you can change style.
the issue here is that your state from the useState for active is a global state for all the list item.
So even if you click any list item, it is changing the state active which is given to all the list items.
So solve this, you can make the state for setting active styles and clickHandler in the ListItem component.
That would solve your issue
That is happening, because your onClick function is triggering a re-render due to the change in state, so all of your ListItems get re-rendered with the new "active" state.
Instead of holding one single value in the state, you should create an array that holds the active state for all of your ListItems, like
[
{itemID: "item1", active: false},
{itemID: "item2", active: false},
{itemID: "item3", active: true}
]
That is the default initial state of the list items, so if you want all of them to be inactive, set all active to false.
And when you are rendering with the .map, you just check wether the current item that is being rendered has its active set to true or false and change the class for only that item.
That would also require you to pass additional parameters to your onClick function, you do that by creating an anonymous function and not calling it:
onClick={() => { ClickHandler(idOfCurrentListItem, param2, param3...) }}
So it would look something like this (mind you im doing this from the top of my head, dont have an editor infront of me :P):
allItems.map(item => {
return <ListItem onClick={() => { ClickHandler(item.itemID) }}></ListItem>
});
And in the ClickHandler:
const ClickHandler = (id) => {
setItemsState(prevItems => {
// looping through all items from the initial state, and when we reach the item that matches the
// id passed to the click handler, we change only its state to true
// otherwise we return the same item without change
return prevItems.map(item => {
if (item.id === id) return { ...item, item.active: true }
return item;
});
});
}
Another approach would be to move the onClick inside the ListItem component itself, so that each ListItem handles its own state, and then you can do it the simpler way, like you've been trying to do it. That owuld also probably be the better option as that would not trigger a re-render of the whole menu, but just of that particular ListItem.
TLDR
At that situation, You can use string or object for indicate what is selected
Answer
Simply you can use activeKey and setActiveKey. And you should indicate with item.title
Code
import React, {Fragment, useCallback, useState} from 'react';
import {makeStyles, withStyles} from '#material-ui/core/styles';
import AppBar from '#material-ui/core/AppBar';
import {Link} from "react-router-dom";
import ListItemText from "#material-ui/core/ListItemText";
import ListItem from "#material-ui/core/ListItem";
import Typography from "#material-ui/core/Typography";
import Toolbar from "#material-ui/core/Toolbar";
import Box from "#material-ui/core/Box";
import List from "#material-ui/core/List";
import clsx from "clsx";
const useStyles = makeStyles((theme) => ({
root: {
width: '100%',
backgroundColor: theme.palette.background.paper,
},
appBar: {
shadowColor: '#206a5d',
margin: 0
},
active: {
color: '#206a5d',
borderBottom: '2px solid blue'
},
}));
export function Submenu({items, header}) {
const classes = useStyles();
const [activeItem, setActiveItem] = useState("")
const clickHandler = useCallback((itemKey) => () => {
setActiveItem((itemKey));
},[]);
return (
<Fragment>
<div className={classes.root}>
<AppBar position="sticky"
color="inherit"
className={classes.appBar}
>
<Typography style={{marginLeft: '1.5rem'}}>
<h1 style={{color:'#1f4068'}}>{header}</h1>
</Typography>
<div style={{display: 'flex',}}
>
{items.map(item =>
<ListItem button
key={item.title}
onClick={clickHandler(item.title)}
component={Link}
className={clsx(null, {[classes.active]: item.title === activeKey})}
to={item.to || '/404'}
style={{padding:'0.75rem', width: 'auto'}}
>
{item.title}
</ListItem>
)}
</div>
</AppBar>
</div>
</Fragment>
);
}
ETC
But more efficiently It is good to use object like other people say. But If you are newbie in react I think it is good to use either string type and object !
And you can sometime realize that what is more efficient to use

Passing styling options using JSS/Material-UI

I've written a small wrapper component for the Paper Material-UI Component:
import React from 'react';
import Paper from '#material-ui/core/Paper';
import {withStyles} from '#material-ui/core/styles';
const styles = theme => ({
root: {
...theme.mixins.gutters(),
paddingTop: theme.spacing.unit * 2,
paddingBottom: theme.spacing.unit * 2,
},
});
const PaddedPaper = (props) => {
const {classes, children} = props;
return (
<Paper className={classes.root}>
{children}
</Paper>
);
};
export default withStyles(styles)(PaddedPaper);
Which, as you may have guessed, is used like this:
<PaddedPaper>
<p>Some content.</p>
</PaddedPaper>
With JSS, is it possible to pass padding into PaddedPaper as a prop?
<PaddedPaper padding={20}>
<p>Some content.</p>
</PaddedPaper>
Since styles is defined outside of the PaddedPaper class, and doesn't have access to props, how can I pull this off? Or am I thinking about this entire process incorrectly?
When you're using withStyles, you have access to the theme, but not props.
this is still an ongoing issue : https://github.com/mui-org/material-ui/issues/7633
easiest way to use props in your styles is using inline styles (for now)
like this:
function PaperSheet(props) {
return (
<div>
<PaddedPaper {...props} size={10}>
<Typography variant="headline" component="h3">
This is a sheet of paper.
</Typography>
<Typography component="p">
Paper can be used to build surface or other elements for your
application.
</Typography>
</PaddedPaper>
</div>
);
}
const PaddedPaper = props => {
const { children, size } = props;
console.log(size);
return <Paper style={{ padding: size }}>{children}</Paper>;
};
here is a working example: https://codesandbox.io/s/yl4671wxz

Resources