React hook error when changing size of rendered array - reactjs

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>
));
}

Related

Obtaining actual array state

I've been exploring React in practice and now I've stuck.
There is the app that contains: "FlashCardsSection" (parent component)
export const FlashCardsSection = () => {
const [cardsList, setCardsList] = useState([]);
const cardAddition = (item) => {
setCardsList(() => [item, ...cardsList]);
};
const handlerDeleteCard = (cardsList) => {
console.log(cardsList);
};
return (
<section className = "cards-section">
<div className = "addition-card-block">
<PopUpOpenBtn cardAddition={cardAddition} cardsList={cardsList}
handlerDeleteCard = {handlerDeleteCard}
/>
</div>
<CardsBlock cardsList={cardsList}/>
</section>
)
};
At the "PopUpOpenBtn" there is the "AddFlashCardBtn" component which adds the "CreatedFlashCard" component to the "cardsList" property by the "cardAddition" function. Each "CreatedFlashCard" component contains the "FlashCardRemoveBtn" component. I would like to implement the function which deletes the card where occurred click on the "FlashCardRemoveBtn" component which calls the "handlerDeleteCard" function. I need an actual array version at any click but I get: when a click at the first card - empty array, the second card - array with one element, third - array with two elements, etc.
The "PopUpOpenBtn" contains intermediate components which related with creation and addition flashcard, so I passed the properties "cardsList", "handlerDeleteCard" through all to the "FlashCardRemoveBtn".
export const FlashCardRemoveBtn = (props) => {
let {handlerDeleteCard, cardsList} = props;
return(
<button className = "remove-btn" onClick={() => {
handlerDeleteCard(cardsList);
}}>Remove
</button>
)};

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

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)}/>);

Like Button with Local Storage in ReactJS

I developed a Simple React Application that read an external API and now I'm trying to develop a Like Button from each item. I read a lot about localStorage and persistence, but I don't know where I'm doing wrong. Could someone help me?
1-First, the component where I put item as props. This item bring me the name of each character
<LikeButtonTest items={item.name} />
2-Then, inside component:
import React, { useState, useEffect } from 'react';
import './style.css';
const LikeButtonTest = ({items}) => {
const [isLike, setIsLike] = useState(
JSON.parse(localStorage.getItem('data', items))
);
useEffect(() => {
localStorage.setItem('data', JSON.stringify(items));
}, [isLike]);
const toggleLike = () => {
setIsLike(!isLike);
}
return(
<div>
<button
onClick={toggleLike}
className={"bt-like like-button " + (isLike ? "liked" : "")
}>
</button>
</div>
);
};
export default LikeButtonTest;
My thoughts are:
First, I receive 'items' as props
Then, I create a localStorage called 'data' and set in a variable 'isLike'
So, I make a button where I add a class that checks if is liked or not and I created a toggle that changes the state
The problem is: I need to store the names in an array after click. For now, my app is generating this:
App item view
localStorage with name of character
You're approach is almost there. The ideal case here is to define your like function in the parent component of the like button and pass the function to the button. See the example below.
const ITEMS = ['item1', 'item2']
const WrapperComponent = () => {
const likes = JSON.parse(localStorage.getItem('likes'))
const handleLike = item => {
// you have the item name here, do whatever you want with it.
const existingLikes = likes
localStorage.setItem('likes', JSON.stringify(existingLikes.push(item)))
}
return (<>
{ITEMS.map(item => <ItemComponent item={item} onLike={handleLike} liked={likes.includes(item)} />)}
</>)
}
const ItemComponent = ({ item, onLike, liked }) => {
return (
<button
onClick={() => onLike(item)}
className={liked ? 'liked' : 'not-liked'}
}>
{item}
</button>
)
}
Hope that helps!
note: not tested, but pretty standard stuff

passing a state hook to another element using props is not working in React?

I have tow components one should have the states of showing and hiding an icon. the other should at least show that icon on click since the default state in not showing anything, so I am trying to pass down the showIcon function down to the other element using props but its not working showing an error showIcon is not a function
//Component A Row
import anItem from './anItem';
function Row(props) {
const [iconState, setIconState] = useState([]);
const icon = <FontAwesomeIcon icon={["fas", "check"]}/>
// from anItem component
const showIcon = ()=>{
setIconState([icon])
}
// from this component
const removeIcon = ()=>{
setIconState([])
}
// Pass the funtion down to the below componant so we can hide the element from there
let item = [<anItem icon=iconState showIcon={showIcon}/>]
return (
<li className="day-row check faderin" onClick={()=> reomveIcon()}>
// render all the items in the initial state
{item}
</li>
)
}
// Component B anItem
function anItem(props) {
return (
<div onClick={() =>{props.showIcon}>{porps.icon}</div>
)
}
export default anlItem
Here I see some typo:
let item = [<anItem icon={iconState} showIcon={showIcon}/>] // add curly braces
Make sure you correctly define the functions below (with const keyword):
const showIcon = ()=> {
setIconState([icon])
}
const removeIcon = ()=> {
setIconState([])
}
I see also another problem in your anItem component:
function anItem(props) {
return (
<div onClick={() => props.showIcon()}>{porps.icon}</div> // here onClick param
)
}
Or like this:
function anItem(props) {
return (
<div onClick={props.showIcon}>{porps.icon}</div> // here onClick param
)
}

Set React Context inside function-only component

My goal is very simple. I am just looking to set my react context from within a reusable function-only (stateless?) react component.
When this reusable function gets called it will set the context (state inside) to values i provide. The problem is of course you can't import react inside a function-only component and hence I cannot set the context throughout my app.
There's nothing really to show its a simple problem.
But just in case:
<button onCLick={() => PlaySong()}></button>
export function PlaySong() {
const {currentSong, setCurrentSong} = useContext(StoreContext) //cannot call useContext in this component
}
If i use a regular react component, i cannot call this function onClick:
export default function PlaySong() {
const {currentSong, setCurrentSong} = useContext(StoreContext) //fine
}
But:
<button onCLick={() => <PlaySong />}></button> //not an executable function
One solution: I know i can easily solve this problem by simply creating a Playbtn component and place that in every song so it plays the song. The problem with this approach is that i am using a react-player library so i cannot place a Playbtn component in there...
You're so close! You just need to define the callback inside the function component.
export const PlaySongButton = ({...props}) => {
const {setCurrentSong} = useContext(StoreContext);
const playSong = () => {
setCurrentSong("some song");
}
return (
<button
{...props}
onClick={() => playSong()}
/>
)
}
If you want greater re-usability, you can create custom hooks to consume your context. Of course where you use these still has to follow the rules of hooks.
export const useSetCurrentSong = (song) => {
const {setCurrentSong} = useContext(StoreContext);
setCurrentSong(song);
}
It is possible to trigger a hook function by rendering a component, but you cannot call a component like you are trying to do.
const PlaySong = () => {
const {setCurrentSong} = useContext(StoreContext);
useEffect( () => {
setCurrentSong("some song");
}, []
}
return null;
}
const MyComponent = () => {
const [shouldPlay, setShouldPlay] = useState(false);
return (
<>
<button onClick={() => setShouldPlay(true)}>Play</button>
{shouldPlay && <PlaySong />}
</>
)
}

Resources