Why counter defined with useRef hook is not incrementing on each render? - javascript

I am trying use the hook useRef() to keep a counter of which items to display next, and increment the counter on each render and using the counter value to slice in elements in an array. On each render the counter index.current should increase by 10 but in my case its not increasing and I keep getting the same 10 elements over and over. I can't figure out why. The collection overview component here is mapping over the collection preview component 4 times.
//CollectionPreview
const CollectionPreview = ({title,movieItems,Counter}) => {
const index = React.useRef(0)
const movieData = movieItems.slice(index.current, index.current + 10)
index.current += 10
return (
<div className="collection-preview">
<h1 className="title">{title.toUpperCase()}</h1>
<div className="preview">
{console.log(movieData)}
</div>
</div>
);
}
const mapStateToProps = state => ({
movieItems: selectMovieItems(state),
})
export default connect(mapStateToProps)(CollectionPreview);
//CollectionOverview
class CollectionOverview extends React.Component {
render() {
return (
<div className="collection-overview">
{
CollectionData.map(items =>
<CollectionPreview key={items.id} title={items.title} />)
}
</div>
);
}
}
export default CollectionOverview;

Personally I think counting the number of renders is bad pattern, as renders can be called multiple times in different occasions and its difficult to keep track of every single render call as your app gets bigger.
I think a better approach is to have a initialIndex or pageIndex in some state somewhere. As you are using Redux, I think you should put that in the reducer and create the appropriate actions to increment this value.

Related

Too many re-renders useState React js

I want to display objects from the array one by one by clicking on the card.
To do this, I made a check for array length (props.tasks.length), and if it is more than 1, then the array is shuffled and then I want to display these objects one by one on click.
But after a click, a new array is generated each time and often the objects are repeated 2-4 times.
But I get an error: Too many re-renders. React limits the number of renders to prevent an infinite loop.
function App(props) {
let [random, setRandom] = useState({});
let [newArr, setNewArr] = useState(props.tasks.sort(() => Math.random() - 0.5));
let i = 0;
random = newArr[i]
setRandom(newArr[i])
const randomCard =()=> {
console.log(random);
console.log(i);
console.log(newArr[0],newArr[1],newArr[2],newArr[3], newArr[4], newArr[5]);
console.log(newArr[i]);
if (i <= newArr.length) {
i++
} else {
newArr = props.tasks.sort(() => Math.random() - 0.5);
console.log('clean');
i=0
}
}
return (
<div>
{newArr.length > 1 ? (
<div className="item" onClick={randomCard}>
<p>{random.name}</p>
<p>{random.translate}</p>
<p>{random.note}</p>
</div>
) : (
<p>Nothing</p>
)}
</div>
);
}
export default App;
The main culprit in your code example is
setRandom(newArr[i])
Using a hook's set method should only be done via an action or wrapped in a useEffect for side effects. In your code, setNewArr is called on every rerender and will cause the component to rerender... causing the infinite loop of rerenders.
You also don't need to store the selected element from your array in a state, you're essentially doing this by just storying the index in the use state.
Your solution should look something like this.
Also you want to reset i when it's < newArr and not <= newArr because if your array is of length "2" and i is "2" then you're setting i to "3" which doesn't point to any element in your list.
const shuffle = (arr) => arr.sort(() => Math.random() - 0.5);
function App(props) {
// Default empty list
const [newArr, setNewArr] = useState([]);
// Sets the default index to 0
const [i, setI] = useState(0);
// We don't want to call shuffle on every rerender, so only setNewArr
// Once when the component is mounted.
useEffect(() => {
setNewArr(shuffle(props.tasks));
}, []);
const randomCard =()=> {
if (i < newArr.length) {
setI(i + 1);
} else {
setNewArr(shuffle(props.tasks));
setI(0);
}
}
return (
<div>
{newArr.length > 1 ? (
<div className="item" onClick={randomCard}>
<p>{newArr[i].name}</p>
<p>{newArr[i].translate}</p>
<p>{newArr[i].note}</p>
</div>
) : (
<p>Nothing</p>
)}
</div>
);
}
export default App;

React Hool re-render loop test Condition on state

I want to update and re-render for loop test condition on hook state update. After I update state hook the state is rerendered but I want to the loop to be rerendered too. I make a form. The length of the form depends on what number I enter on input.
Here is what I tried:
const [formLength, setFormLength] = useState(0);
let eachForm = () => {
for (let x = 0; x < formLength; x++) {
return <div>{x+1} inpute</div>;
}
};
useEffect(() => {
eachForm();
}, [formLength]);
{formLength <= 0 ? (<>Zeroo</>) : (<><eachForm /></>)}
There is no need for the useEffect hook here, it's not doing anything for you. You can render directly the JSX from the formLength state. eachForm also isn't a valid React component, so it can't be used like <eachForm />. The for-loop will also return during the first iteration and not return an array of div elements like I suspect you are expecting.
Example:
const [formLength, setFormLength] = useState(10);
return (
<>
{formLength
? Array.from({ length: formLength }).map((_, i) => (
<div key={i}>{i+1} input</div>
))
: <>Zero</>
</>
);
When the state changes your component is rerendered so there is no need for useEffect. Try something like this:
const [formFields, setFormFields] = useState([]);
return (
<div className='App'>
<input type='button' onClick={()=>
setFormFields([...formFields,<p key={formFields.length}>{formFields.length}</p>])}
value="+"/>
<form>
{formFields}
</form>
</div>
);

Too many re-renders/infinite loop - setting state in mapping function

I am trying to map through items in my cart and compare the id of the item to the id returned in the data object, and then displaying some info on the browser such as quantity for each item etc. I am also trying to add a total cart items display element, however, when I add the quantity of each item in the cart (eg one item has quantity of 3, another has 1 etc) and try to set the state within the mapping function, I get a re-render error. I understand from reading online that its because when you set state, the component re-renders, however, I am unsure how to set state with the total quantity of items in the cart without setting state from within the function.
I also tried declaring the variable outside the mapping function and adding to that variable on each iteration, but that does not work either.
IF i console log cartItemsTotal within the mapping function, I get the correct value of the addition of all the items being mapped through's quantity.
If I console log it outside the mapping function, its always 0. Is this because of scope? You cant manipulate a variable outside of the scope of the function?
Any help is appreciated.
Thanks
const CartIconSummary = ({data}) => {
const [cartQuantity, setCartQuantity] = useState()
const deleteCart = useCartDelete()
const cart = useCart()
let cartTotal = 0
let vat = cartTotal * .23.toFixed(2)
let cartItemsTotal = 0
const displayCartItems = () => {
setCartQuantity(cartItemsTotal)
return cart && cart.map((cartItem, index) => {
return data && data.map((dataItem) => {
if(cartItem.id === dataItem.id) {
cartTotal = cartTotal + dataItem.price * cartItem.quantity
vat = vat + (dataItem.price * cartItem.quantity) * .23
cartItemsTotal = cartItemsTotal + cartItem.quantity
setCartQuantity(cartItemsTotal)
return (
<div key={index} className={`${className}CartItemContainer`}>
<div className={`${className}ImageContainer`}>
<img className={`${className}Image`} src={data ? `${process.env.PUBLIC_URL}${dataItem.cartImage.slice(1)}` : ""} alt="cart-item-preview"/>
</div>
<div className={`${className}CartItemNamePriceContainer`}>
<div className={`${className}CartItemName`}>
{dataItem.shortName}
</div>
<div className={`${className}CartItemPrice`}>
${dataItem.price}
</div>
</div>
<div className={`${className}CartItemQuantity`}>
x {cartItem.quantity}
</div>
</div>
)
} else {
return ""
}
})
})
}
return (
<>
<div className={`${className}CartDeleteContainer`}>
<div className={`${className}CartQuantity`}> CART({cartQuantity ? cartQuantity : 0}) </div>
<div className={`${className}CartDelete`} onClick={() => deleteCart([])}> Remove all </div>
</div>
{displayCartItems()}
</>
)
}
Currently you call setCartQuantity inside the function displayCartItems, which means that whenever the component renders the function displayCartItems will run again as well. That means when the function displayCartItems executes, it will change the state cartQuantity with setCartQuantity and if a state changes the whole component will re-render, which means displayCartItems will be exectued again.
To solve this, you should use a useEffect hook and add cartItemsTotal in the dependency array so that setCartQuantity is only executed when cartItemsTotal changes.

React - sort array of child components with state

Currently I'm working on a react project, but I'm seeing some unexpected behavior when sorting an array of stateful child components.
If I have a parent component
export function Parent(){
const [children, setChildren] = useState([
{name:'Orange',value:2},
{name:'Apple',value:1},
{name:'Melon',value:3}
])
var count = 0
function handleSort() {
var newChildren=[...children]
newChildren.sort((a,b)=>{return a.value-b.value})
setChildren(newChildren)
}
return (
<div>
<button onClick={handleSort}>Sort</button>
{children.map((child) => {
count++
return(<ChildComp key={count} details={child}/>)
})}
</div>
)
}
And a child component
function ChildComp(props){
const[intCount,setIntCount] = useState(0)
function handleCount(){
setIntCount(intCount+1)
}
return (
<div>
<p>{props.details.name}</p>
<button onClick={handleCount}>{intCount}</button>
</div>
)
}
When the page first renders everything looks great, three divs render with a button showing the number of times it was clicked and the prop name as it was declared in the array. I've noticed that when I sort, it sorts the props being passed to the child components which then rerender, but the intCount state of the child component stays tied to the original location and is not sorted. is there any way to keep the state coupled with the array element through the sort while still maintaining state data at the child level, or is the only way to accomplish this to raise the state up to the parent component and pass a callback or dispatch to the child to update it?
The count is not is not sorted. It just got updated when you sorted.
Keys help React identify which items have changed, are added, or are
removed. Keys should be given to the elements inside the array to give
the elements a stable identity
Every time you sort, key stay the same, as you use count.
Try using value as key
export function Parent(){
// ....
return (
<div>
<button onClick={handleSort}>Sort</button>
{children.map(child => {
return <ChildComp key={child.value} details={child}/> // key is important
})}
</div>
)
}
More info: https://reactjs.org/docs/lists-and-keys.html#keys

How React's diffing algorithm works with a list of elements?

UPD: I used React Dev Tools with Highlight Updates which seems to highlight each render() method call
I've written a simple React App which works the next way:
It has a button and a list of numbers
After clicking the button it adds a new number to the bottom of the list increased by 1 from the previous one
The problem is that React updates all list elements each time even when I use key property. I know that this problem can be solved by using PureComponents class or self-implemented shouldComponentUpdate()
Why React updates all list elements if they are not changed? I though that React uses special diffing algorithm for comparing elements and it should have worked, but now I see that it works not as I expected.
Could any body explain why React's diffing algorithm doesn't work in this case?
class App extends Component {
constructor(props) {
super(props);
this.state = {
numbers: []
}
this.handleButtonClick = this.handleButtonClick.bind(this);
}
handleButtonClick() {
const numbers = this.state.numbers;
const length = this.state.numbers.length ;
const newNumber = length > 0 ? numbers[length - 1] + 1 : 0;
this.setState({
numbers: [...numbers, newNumber]
})
}
render() {
return (
<div>
<button onClick={this.handleButtonClick}>Add</button>
<NumbersList numbers={this.state.numbers} />
</div>
);
}
}
class NumbersList extends Component {
render() {
return (
<ul>
{
this.props.numbers.map((number) => <NumberItem key={number} number={number} />)
}
</ul>
)
}
}
class NumberItem extends Component {
render() {
return (
<li>{this.props.number}</li>
)
}
}
The Javascript/React side uses shouldComponentUpdate to determine if it should rerender. This defaults to true and will rerender the component any time that there is a state change.
https://reactjs.org/docs/react-component.html#shouldcomponentupdate
Using this rerendered virtual representation of your app, it uses this https://reactjs.org/docs/reconciliation.html#the-diffing-algorithm to reconcile your virtualDOM with the real DOM
Have you tried:
this.setState({numbers[index-1]: newNumber})?
Your code here:
this.setState({
numbers: [...numbers, newNumber]
})
reads to me as a replacement of the numbers array and not an update of it.

Categories

Resources