React Slider images changing on Click not working (uncaught error too many re-renders) - reactjs

I'm trying to create a React slider for images. But I'm getting an error that says Uncaught Error: Too many re-renders. If anyone can just point me in the right direction I would really appreciate it. I'm certain that the issue lays within the onClick aspect of the sliderDots mapping.
import React, { useEffect, useState } from 'react';
import Sliderdots from '../CarasouelDots/Sliderdots.component';
import './Slider.styles.scss'
import sliderImages from '../../MockImages/mockimages';
const Slider = () => {
const images = sliderImages;
//Iterator
const [img, setImg] = useState(0);
//Getting all shoe images from an object array
const shoes = images.map(i => (i.shoe));
const heading = images.map(i => (i.title));
const content = images.map(i => (i.content))
const numbers = shoes.map((i, index) => (index))
const indexSet = (number) =>{
setImg(number);
}
//problem with onClick here??
const sliderD = images.map((dot, index) => <Sliderdots key={index} onClick={indexSet(index)}/>);
useEffect(() => {
const timer = setTimeout(() => {
img == shoes.length - 1 ? setImg(0) : setImg(img + 1)
}, 4500)
}, [img]);
return (
<div className='slider-container' style={{ backgroundImage: `url(${shoes[img]})` }}>
<div className='overlay'>
<h1 className='introduction'>{heading[img]}</h1>
<p className='content'>{content[img]}</p>
<div className='dot-container'>
{sliderD}
</div>
</div>
</div>
);
};
export default Slider;

The reason why your component is constantly rerendering is because your onClick property is actually a function call in disguise that gets executed every render:
// This line actually calls the `indexSet` function each time!
const sliderD = images.map((dot, index) => <Sliderdots key={index} onClick={indexSet(index)}/>);
And since indexSet updates the state of the React component by calling setImg, the React component will always end up re-rendering when it reaches that line of code, and since that line of code always re-calls the indexSet function, your component will infinitely re-render.
To fix your code, you just need to replace that onClick property with an anonymous function:
const sliderD = images.map((dot, index) => <Sliderdots key={index} onClick={() => indexSet(index)}/>);

Related

How to avoid this message warning "Maximum update depth exceeded..." on NextJs

on NextJs i not understand, how useEffect work. What i need to do, to stop of receiving this warning message
"Maximum update depth exceeded":
The Code bellow is the page, that call a component ListContainer, this page add a item to container.
The page JSX:
import { useState } from "react";
import AppLayout from "../components/AppLayout";
import ListContainer from "../components/ListContainer";
export default function componentCreator(){
const [item,setItem] = useState([])
/* add item to container */
function addItem(){
let newItem = item
newItem.push({
produto: 'Skol 350ml',
preco: '1200,00',
quantidade: 'cx c/ 15 unidades'
})
setItem(newItem)
}
return (
<AppLayout>
<ListContainer items={item} setItems={setItem}/>
<div className="productcardbuttonshow" onClick={() => addItem()}>ADICIONAR</div>
</AppLayout>
)
}
Bellow the component that handle the items, remove or add. But it works, but on console trigger warning messages about update.
Component ListContainer.jsx:
import { useState,useEffect } from "react";
export default function ListContainer(props){
const [html,setHTML] = useState(null)
const [item,setItem] = useState(props.items)
/* refresh html container */
useEffect(() => {
const itemHTML = item.map((itemmap,id) => {
return (
<div id={id} onClick={() => delItem(id)} className="itemProposta">
{itemmap.produto} - {itemmap.quantidade} - R$ {itemmap.preco}
</div>
)
})
setHTML(itemHTML)
})
/* remove item from container */
function delItem(id){
let itemlist = props.items
itemlist.splice(id,1)
props.setItems(itemlist)
}
return (
<>
{html}
</>
)
}
You are getting into an infinite loops of renders. This code is responsible:
useEffect(() => {
const itemHTML = item.map((itemmap,id) => {
return (
<div id={id} onClick={() => delItem(id)} className="itemProposta">
{itemmap.produto} - {itemmap.quantidade} - R$ {itemmap.preco}
</div>
)
})
setHTML(itemHTML)
})
This callback inside useEffect will run after every render, because there is no dependency array. That means after every render, setHTML(itemHTML) is called. And even if the constituent objects of the array itemHTML are same, a new reference of the array is created. A new reference is created because .map() returns a new reference of the array. And although render and update works correctly, infinite rendering is happening.
Consider adding a dependency array to useEffect. For example:
useEffect(() => {
/* function body */
},[props.items]);
Now useEffect callback only runs if props.items reference changes.
Side note (unrelated to your question):
In the below code,
function addItem(){
let newItem = item
newItem.push({
produto: 'Skol 350ml',
preco: '1200,00',
quantidade: 'cx c/ 15 unidades'
})
setItem(newItem)
}
You should do let newItem = [...item], otherwise you are not creating a new reference of item array and setItem(newItem) is basically useless in that case.

React hook error when changing size of rendered array

I get the following error: React Error: "Rendered more hooks than during the previous render", and it is because inside a mapped array that I render are buttons that have their own useState hooks.
So I have an array of projects that I render from a list. Initially, only 3 projects are shown, and clicking a button will load the whole list.
The problem is that inside project can be multiple ProjectButtons, and those ProjectButtons are components because I want to use special hover states using the useState hook.
But when I change the size of the project list being rendered, it throws an error because of the useState hook inside the ProjectButton component.
import { projects } from "../lib/projectList";
const Projects: FC = () => {
// Initially use a portion of the array
const [projectArray, setProjectArray] = useState(projects.slice(0, 3));
// Load the whole array on button click
const loadMoreProjects = () => {
setProjectArray([...projects]);
}
const ProjectButton = (button: { type: string, link: string }) => {
// Removing this useState hook fixes the problem, but I need it for my design
const [hoverColor, setHoverColor] = useState("#0327d8");
const handleMouseEnter = () => {
setHoverColor("white");
}
const handleMouseLeave = () => {
setHoverColor(original);
}
return (
<a href={button.link} rel="noreferrer" target="_blank" key={button.link}>
<button onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave}>
<WebsiteIcon className="projectButtonIcon" fill={hoverColor} />
<p>{button.type}</p>
</button>
</a>
);
}
return projectArray.map(project => (
...
<div className="projectLinks">
{project.buttons.map(button => ProjectButton(button))}
</div>
...
<Button onClick={loadMoreProjects}>Load More</Button>
));
}
You've defined ProjectButton within your Projects component, so you're breaking the rule of hooks - specifically "Only Call Hooks at the Top Level".
Move the ProjectButton component out of the scope of Projects and it will be happy.
This is happening because you are using hooks inside a function and it should be used directly inside a component.
This can solved if you create ProjectButton as a component instead of function.
Here is the updated code:
import { projects } from "../lib/projectList";
const ProjectButton = (button) => {
// Removing this useState hook fixes the problem, but I need it for my design
const [hoverColor, setHoverColor] = useState("#0327d8");
const handleMouseEnter = () => {
setHoverColor("white");
};
const handleMouseLeave = () => {
setHoverColor(original);
};
return (
<a href={button.link} rel="noreferrer" target="_blank" key={button.link}>
<button onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave}>
<WebsiteIcon className="projectButtonIcon" fill={hoverColor} />
<p>{button.type}</p>
</button>
</a>
);
};
const Projects: FC = () => {
// Initially use a portion of the array
const [projectArray, setProjectArray] = useState(projects.slice(0, 3));
// Load the whole array on button click
const loadMoreProjects = () => {
setProjectArray([...projects]);
}
return projectArray.map(project => (
...
<div className="projectLinks">
{project.buttons.map((button) => (
<ProjectButton {...button} />
))}
</div>
...
<Button onClick={loadMoreProjects}>Load More</Button>
));
}

Why doesn't updating my list trigger a re-render of my component?

Given the code below I would expect to see a new list item added to the when I click the button. The state data appears to be updated but the DOM does not change. I confess to being quite a newbie with React and Hooks so hopefully this is just a question of me not understanding the binding model in play here.
import React, {useState,useEffect} from 'react';
import './App.css';
function App() {
const [someList, setSomeList] = useState([]);
useEffect(() =>
{
const someDataForTheList = [1,2,3];
setSomeList(someDataForTheList);
}, []);
const onClickAddToList = () =>
{
let newDataForTheList = someList;
newDataForTheList.push(someList.length + 1);
setSomeList(newDataForTheList);
console.log(someList);
}
return (
<div className="App">
<h1>{someList ? someList.length : null}</h1>
<ul>
{someList ? someList.map((i) =>
<li key={i}>
{i}
</li>)
: null}
</ul>
<button onClick={onClickAddToList}>Add to list</button>
</div>
);
}
export default App;
This is because react does not do deep inspection when checking for updates (by default). So pushing a new item on the same list and then assigning it again using the hook's dispatch method React determines this to be the same object/item, thus concluding the view does not have to be updated.
You can however do the following:
const onClickAddToList = () => {
// creating a new list
const newDataForTheList = [...someList, someList.length + 1];
setSomeList(newDataForTheList)
}

Why does Component re-renders on Click and state change?

Why does a whole component in react re-render when you change state in a onClick?
exmaple : https://codesandbox.io/s/vibrant-firefly-sgk5g?file=/src/App.js
When you click on the numbers the whole components re-renders , and if you remove the setCount from the on click function it works just fine
The idea behind the component is to add a "Active" class to the number that you have clicked, and it updated a random counter, that counter prevents the addition the "active" class, since it re-renders the whole component
EDIT: code here aswell
import React, { useState } from "react";
const Hours = () => {
const days = [1, 2, 3, 4, 5, 6];
const [count, setCount] = useState(1);
const TestClick = (e, item) => {
setCount(count + 1);
e.currentTarget.className = "active";
};
const HandleHours = () => {
let block = <span />;
if (days) {
block = days.map((hour, index) => {
return (
<span
style={{ display: "block" }}
onClick={e => {
TestClick(e, hour);
}}
className={`col-md-4`} key={index}>
{hour}
</span>
);
});
}
return block;
};
return (
<div>
<HandleHours />
</div>
);
};
export default Hours;
The issue here isn't coming from the fact that the HandleHours components render but because it gets remounted everytime you change the state in the Hours component.
This happens because HandleHours is defined as a component within Hours component and everytime Hours re-renders a new reference to HandleHours is created which fools react into thinking that the component detached from DOM and a new component replaces it, since it essentialy works on reference.
Now when you render HandleHours like
<div>
{ HandleHours () }
</div>
Suddenly HandleHours turns from being a component to a function which returns JSX so this time when the Hours component re-renders, even though the function reference to HandleHours has changed. It returns the JSX with a key prop on it, which remains the same and hence React treats it as a re-render and hour changes to DOM elements aren't lost
Now there is a solution to the first approach too
All you need to do is to create a component HandleHours outside of your Hours component and render it by passing the required props like
import React, { useState } from "react";
import "./styles.css";
const HandleHours = ({ days, TestClick }) => {
let block = <span />;
if (days) {
block = days.map((hour, index) => {
return (
<span
style={{ display: "block" }}
onClick={e => {
TestClick(e, hour);
}}
className={`col-md-4`}
key={index}
>
{hour}
</span>
);
});
}
return block;
};
const days = [1, 2, 3, 4, 5, 6];
const Hours = () => {
const [count, setCount] = useState(1);
const TestClick = (e, item) => {
setCount(count + 1);
console.log("TestClick");
e.currentTarget.className = "active";
};
return (
<div>
<HandleHours days={days} TestClick={TestClick} />
</div>
);
};
export default Hours;
When you do that the HandleHours component isn't re-mounted on each rerender of Hours component and it maintains the DOM elements correctly.
Here is a working demo for the second approach
It's the way react rerenders when a component state changes. The state hook rerenders the whole component that it's in when the setState function is called which is the second element in the array that useState returns.
If you want to change the class of an element on click, you need to store it as a state. In your code, the class of clicked span is updated on click, but right after that the component is rerendered and set to what the HandleHours returns.
I would probalby have a state that keeps track which day is clicked and render that accordingly (not sure why you need the count, but I left it there):
import React, { useState } from "react";
const Hours = () => {
const days = [1, 2, 3, 4, 5, 6];
const [count, setCount] = useState(1);
const [clickedDays, setClickedDays] = useState([]); // Added clickedDays state
const TestClick = (e, item, isDayClicked) => {
setCount(count + 1);
if (!isDayClicked) { // Setting clicked days if they are not in the array yet
setClickedDays([...clickedDays, item])
}
};
const HandleHours = () => {
let block = <span />;
if (days) {
block = days.map((hour, index) => {
const isDayClicked = clickedDays.includes(hour);
return (
<span
style={{ display: "block" }}
onClick={e => {
TestClick(e, hour, isDayClicked);
}}
className={isDayClicked ? 'active' : 'col-md-4'} // Setting different class depending on state
key={index}
>
{hour}
</span>
);
});
}
return block;
};
return (
<div>
<HandleHours />
</div>
);
};
export default Hours;

React Hook useEffect() run continuously although I pass the second params

I have problem with this code
If I pass the whole pagination object to the second parameters of useEffect() function, then fetchData() will call continuously. If I only pass pagination.current_page so It will call only one time, but when I set new pagination as you see in navigatePage() function, the useEffect() does not call to fetchData() although pagination has changed.
How to solve this. Thank you very much!
Besides I do not want the use useEffect() call when first time component mounted because the items is received from props (It is fetch by server, this is nextjs project).
import React, {useEffect, useState} from 'react';
import Filter from "../Filter/Filter";
import AdsListingItem from "../AdsListingItem/AdsListingItem";
import {Pagination} from "antd-mobile";
import styles from './AdsListing.module.css';
import axios from 'axios';
const locale = {
prevText: 'Trang trước',
nextText: 'Trang sau'
};
const AdsListing = ({items, meta}) => {
const [data, setData] = useState(items);
const [pagination, setPagination] = useState(meta);
const {last_page, current_page} = pagination;
const fetchData = async (params = {}) => {
axios.get('/ads', {...params})
.then(({data}) => {
setData(data.data);
setPagination(data.meta);
})
.catch(error => console.log(error))
};
useEffect( () => {
fetchData({page: pagination.current_page});
}, [pagination.current_page]);
const navigatePage = (pager) => {
const newPagination = pagination;
newPagination.current_page = pager;
setPagination(newPagination);
};
return (
<>
<Filter/>
<div className="row no-gutters">
<div className="col-md-8">
<div>
{data.map(item => (
<AdsListingItem key={item.id} item={item}/>
))}
</div>
<div className={styles.pagination__container}>
<Pagination onChange={navigatePage} total={last_page} current={current_page} locale={locale}/>
</div>
</div>
<div className="col-md-4" style={{padding: '15px'}}>
<img style={{width: '100%'}} src="https://tpc.googlesyndication.com/simgad/10559698493288182074"
alt="ads"/>
</div>
</div>
</>
)
};
export default AdsListing;
The issue is you aren't returning a new object reference. You save a reference to the last state object, mutate a property on it, and save it again.
const navigatePage = (pager) => {
const newPagination = pagination; // copy ref pointing to pagination
newPagination.current_page = pager; // mutate property on ref
setPagination(newPagination); // save ref still pointing to pagination
};
In this case the location in memory that is pagination remains static. You should instead copy all the pagination properties into a new object.
const navigatePage = (pager) => {
const newPagination = {...pagination}; // shallow copy into new object
newPagination.current_page = pager;
setPagination(newPagination); // save new object
};
To take it a step further you really should be doing functional updates in order to correctly queue up updates. This is in the case that setPagination is called multiple times during a single render cycle.
const navigatePage = (pager) => {
setPagination(prevPagination => {
const newPagination = {...prevPagination};
newPagination.current_page = pager;
});
};
In the case of pagination queueing updates may not be an issue (last current page set wins the next render battle), but if any state updates actually depend on a previous value then definitely use the functional update pattern,

Resources