I am struggling to understand a strange behaviour while deleting an element from an array of divs.
What I want to do is create an array of divs representing a list of purchases. Each purchase has a delete button that must delete only the clicked one. What is happening is that when the delete button is clicked on the purchase x all the elements with indexes greather than x are deleted.
Any help will be appreciated, including syntax advices :)
import React, { useState } from "react";
const InvestmentSimulator = () => {
const [counter, increment] = useState(0);
const [purchases, setPurchases] = useState([
<div key={`purchase${counter}`}>Item 0</div>
]);
function addNewPurchase() {
increment(counter + 1);
const uniqueId = `purchase${counter}`;
const newPurchases = [
...purchases,
<div key={uniqueId}>
<button onClick={() => removePurchase(uniqueId)}>delete</button>
Item number {uniqueId}
</div>
];
setPurchases(newPurchases);
}
const removePurchase = id => {
setPurchases(
purchases.filter(function(purchase) {
return purchase.key !== `purchase${id}`;
})
);
};
const purchasesList = (
<div>
{purchases.map(purchase => {
if (purchases.indexOf(purchase) === purchases.length - 1) {
return (
<div key={purchases.indexOf(purchase)}>
{purchase}
<button onClick={() => addNewPurchase()}>add</button>
</div>
);
}
return purchase;
})}
</div>
);
return <div>{purchasesList}</div>;
};
export default InvestmentSimulator;
There are several issues with your code, so I'll go through them one at a time:
Don't store JSX in state
State is for storing serializable data, not UI. You can store numbers, booleans, strings, arrays, objects, etc... but don't store components.
Keep your JSX simple
The JSX you are returning is a bit convoluted. You are mapping through purchases, but then also returning an add button if it is the last purchase. The add button is not related to mapping the purchases, so define it separately:
return (
<div>
// Map purchases
{purchases.map(purchase => (
// The JSX for purchases is defined here, not in state
<div key={purchase.id}>
<button onClick={() => removePurchase(purchase.id)}>
delete
</button>
Item number {purchase.id}
</div>
))}
// An add button at the end of the list of purchases
<button onClick={() => addNewPurchase()}>add</button>
</div>
)
Since we should not be storing JSX in state, the return statement is where we turn our state values into JSX.
Don't give confusing names to setter functions.
You have created a state variable counter, and named the setter function increment. This is misleading - the function increment does not increment the counter, it sets the counter. If I call increment(0), the count is not incremented, it is set to 0.
Be consistent with naming setter functions. It is the accepted best practice in the React community that the setter function has the same name as the variable it sets, prefixed with the word "set". In other words, your state value is counter, so your setter function should be called setCounter. That is accurate and descriptive of what the function does:
const [counter, setCounter] = useState(0)
State is updated asynchronously - don't treat it synchronously
In the addNewPurchase function, you have:
increment(counter + 1)
const uniqueId = `purchase${counter}`
This will not work the way you expect it to. For example:
const [myVal, setMyVal] = useState(0)
const updateMyVal = () => {
console.log(myVal)
setMyVal(1)
console.log(myVal)
}
Consider the above example. The first console.log(myVal) would log 0 to the console. What do you expect the second console.log(myVal) to log? You might expect 1, but it actually logs 0 also.
State does not update until the function finishes execution and the component re-renders, so the value of myVal will never change part way through a function. It remains the same for the whole function.
In your case, you're creating an ID with the old value of counter.
The component
Here is an updated version of your component:
const InvestmentSimulator = () => {
// Use sensible setter function naming
const [counter, setCounter] = useState(0)
// Don't store JSX in state
const [purchases, setPurchases] = useState([])
const addNewPurchase = () => {
setCounter(prev => prev + 1)
setPurchases(prev => [...prev, { id: `purchase${counter + 1}` }])
}
const removePurchase = id => {
setPurchases(prev => prev.filter(p => p.id !== id))
}
// Keep your JSX simple
return (
<div>
{purchases.map(purchase => (
<div key={purchase.id}>
<button onClick={() => removePurchase(purchase.id)}>
delete
</button>
Item number {purchase.id}
</div>
))}
<button onClick={() => addNewPurchase()}>add</button>
</div>
)
}
Final thoughts
Even with these changes, the component still requires a bit of a re-design.
For example, it is not good practice to use a counter to create unique IDs. If the counter is reset, items will share the same ID. I expect that each of these items will eventually store more data than just an ID, so give them each a unique ID that is related to the item, not related to its place in a list.
Never use indices of an array as key. Check out this article for more information about that. If you want to use index doing something as I did below.
const purchasesList = (
<div>
{purchases.map((purchase, i) => {
const idx = i;
if (purchases.indexOf(purchase) === purchases.length - 1) {
return (
<div key={idx}>
{purchase}
<button onClick={() => addNewPurchase()}>add</button>
</div>
);
}
return purchase;
})}
</div>
);
Related
I render 12 cards on the screen each of which have a unique key attribute.
I only started learning react and I want function to only to be called when a card has been clicked and save only that key which been clicked in state if an other has been clicked then update the state and then have 2 ids there and not all 12.
This is my Card component (the others are irrelevant I think)
import React, {useState} from "react";
import {CardData} from "./Data/CardsData.js"
import style from "../style/Cards.css"
import Score from "./Score.js";
const Cards = () =>{
const [bestScore, setBestScore] = useState(0);
const [scoreRN, setScoreRN] = useState(0);
const [clickedCard, setClickedCard] =useState([]);
const CardClick = () =>{
for(let card of CardData)
setClickedCard( clickedCard.concat(card))
console.log(clickedCard)
}
return(
<div id="content">
<Score bestScore={bestScore} scoreRN={scoreRN}/>
{CardData.map((data) => {
return(
<div key={data.id} className="card" onClick={CardClick}>
<img src={data.img}></img>
<h2>{data.name}</h2>
</div>
)
})}
</div>
)
}
export default Cards;
This way I only get the last card's id into my clickedCard array, every time I click on a card.
I have also could do it where I added every cardid on every click.
And an additional thing that I don't fully understand: Why is it that now the 1. console log is returns an empty array (then 1, 2,3 ... elemement)?
CardClick callback is a MouseEventHandler so it receives event as first argument.
You can then get event.currentTarget.id in the CardClick function (if your element has an id)
Another way is to explicit pass data to the onClick handler like onClick={event => CardClick(event, data.id)}. Then your CardClick function can accept your aditionnal arguments
You don't need the for loop if you just want to add the clicked card into the array, but you do need to pass a card parameter so that the callback knows which card was clicked:
const CardClick = (card) =>{
// for(let card of CardData) - you can remove this line
setClickedCard( clickedCard.concat(card))
console.log(clickedCard)
}
In your JSX:
<div key={data.id} className="card" onClick={() => CardClick(data)}>
<img src={data.img}></img>
<h2>{data.name}</h2>
</div>
Also, your console.log is returning an empty array because react state changes aren't immediate, so even after you called setClickedCard, clickedCard will still be its old value in the same function until React's next render.
I made arr as a state and updated in Box.js on handleClick when I log it on console it updates the state perfectly. but doesn't render html
it is supposed to update arr and re-render all arr dependent variables
//Board.js
import React from 'react'
import Box from './Box'
import './Board.css'
function Board() {
//array of null grid
const [arr, setArr] = React.useState(Array(9).fill(null))
//check if current player is X
const [isXTurn, ,setIsXTurn] = React.useState(true)
//all props as object
const props={arr,setArr,isXTurn,setIsXTurn}
//array as Box element
const arrayEle = arr.map((ele,i)=>(<Box {...props} value={ele} index={i} key={i}/>))
return (
<div className='board'>
{arrayEle}
{arr}
</div>
);
}
export default Board;
//Box.js
import React from 'react'
import './Box.css'
function Box(props) {
//style as x & o
const style = props.value==='X'? 'box x': 'box o'
function handleClick(){
if(props.value)
return
console.log(props)
props.setArr(prevArr=>{
const updatedArray = prevArr
updatedArray[props.index] = (props.isXTurn?'X':'O')
return updatedArray
})
}
return (
<button className={style} onClick={handleClick}>
{props.value}
</button>
);
}
export default Box;
This is because you are mistakenly using setArr() function.
To invoke setArr() correctly in order to update state, you have to return a new value inside this function, not the same as previous one.
props.setArr(prevArr=>{
const updatedArray = prevArr
updatedArray[props.index] = (props.isXTurn?'X':'O')
return updatedArray
})
In your code up here, you assigned prevArr to the const updatedArray and return it. But you should know cloning object in a javascript is a bit tricky.
You just cloned prevArr shallowly. So internally, updatedArray is exactly still same value as prevArr. So even though you changed the keys or values inside of it, it only refers to the same value and that's because your setArr() doesn't recognize change.
So, you should clone deeply the prevArr and return it.
I just googled and attach a reference that would be helpful for you
The Best Way to Deep Copy an Object in JavaScript
I am trying to update a React.useState("") using setDisplay in a function that takes an array of strings and joined them into a single string before setDisplay.
Without the setDisplay, I'm able to console.log() and get the joined array.
const App = () => {
const [display, setDisplay] = React.useState("");
const values = [];
const onClick = (value) => {
values.push(value);
console.log(values) // w/o setDisplay output: ["1","2","3","4"]; w/ setDisplay output: ["1"]; ["2"]; ["3"]; ["4"];
setDisplay(values.join(""))
};
return (
<div className="p-5">
{display}
<NumPads data={keypress} onClick={onClick} />
<OperatorPads data={keypress} onClick={onClick} />
</div>
);
};
setDisplay((values) => ([...values, value].join("")]))
The reason the console.log seems to work when you don't call setDisplay is simply because the state of the component did not change.
When you call setDisplay, you change the state, so the entire component re-renders, which causes values to reset back to an empty array.
Solution(s)
One way to solve this is already shown in a previous answer.
Another way is to wrap your values array with useRef:
const values = useRef([]);
Then access the array using values.current
Yet another way is to eliminate the use of values all together and change the callback to:
const onClick = (value) => {
setDisplay(display + value);
};
const onClick = (value) => setDisplay((dispay) => display.concat('', value));
I'm learning react by making a battleship game. When the component loads, I create a new object (board) which I set as a state. Then I'd like to map the board itself, which is any array. However, react says
cannot read property board of undefined.
With console logging, I found out that at first when the page loads, playerBoard is an empty object, and only THEN sets it to the given object with setPlayerBoard.
How could I avoid this?
App.js looks like this:
const GameControl = () => {
const [playerBoard, setPlayerBoard] = useState({})
//creating the board object when component mounts, setting it as a state
useEffect(() => {
const playerBoard = GameBoard('player');
setPlayerBoard({...playerBoard});
},[])
return (
<div className = 'board-container'>
<div className = "board player-board">
{ //mapping an array
playerBoard.boardInfo.board.map((cell, i) => {
return (
<div className = {`cell player-cell`key = {i}></div>
)
} )
}
</div>
</div>
)
}
If creating the game board is synchronous, then just use that as your default value for the state:
const [playerBoard, setPlayerBoard] = useState(GameBoard('player'));
// No use effect after this
If creating the gameboard is synchronous but expensive, you can instead pass a function into useState, and that function will only be called on the first render:
const [playerBoard, setPlayerBoard] = useState(() => GameBoard('player'));
If creating the game board is asynchronous, then you're right to use an effect, but there is no way to avoid the first render not having data. You will just need to make sure your component can work when it doesn't have data. A common way to do this is to start with the state being an value that indicates no data, and then check for it and return a placeholder. null's easier to check for than an empty object, so i'd recommend that:
const [playerBoard, setPlayerBoard] = useState(null);
useEffect(() => {
// some asynchronous stuff, followed by a call to setPlayerBoard
},[])
if (!playerBoard) {
return <div>Loading...</div>
// Or if you don't want to show anything:
// return null;
}
return (
<div className='board-container'>
// etc
</div>
);
I have some experience with class components in React, but am trying to learn hooks and functional components better.
I have the following code:
import React, { useState } from "react";
import { Button } from "reactstrap";
import StyleRow from "./StyleRow";
export default function Controls(props) {
const [styles, setStyles] = useState([]);
function removeStyle(index) {
let newStyles = styles;
newStyles.splice(index, 1);
setStyles(newStyles);
}
return (
<div>
{styles}
<Button
color="primary"
onClick={() => {
setStyles(styles.concat(
<div>
<StyleRow />
<Button onClick={() => removeStyle(styles.length)}>x</Button>
</div>
));
}}
>
+
</Button>
</div>
);
}
The goal of this code is to have an array of components that have an "x" button next to each one that removes that specific component, as well as a "+" button at the bottom that adds a new one. The StyleRow component just returns a paragraph JSX element right now.
The unusual behavior is that when I click the "x" button by a row, it removes the element and all elements following it. It seems that each new StyleRow component that is added takes the state of styles at the moment of its creation and modifies that instead of always modifying the current styles state. This is different behavior than I would expect from a class component.
The freezing of state leads me to believe this has something to do with closures, which I don't fully understand, and I am curious to know what here triggered them. If anyone knows how to solve this problem and always modify the same state, I would greatly appreciate it.
Finally, I think this post on SO is similar, but I believe it addresses a slightly different question. If someone can explain how that answer solves this problem, of course feel free to close this question. Thank you in advance!
You are modifying the existing state of styles, so you will need to create a deep copy of the array first.
You can either write your own clone function, or you can import the Lodash cloneDeep function.
Add the following dependency to your package.json using:
npm install lodash
Also, you are passing the length of the array to the removeStyle function. You should be passing the last index which is length - 1.
// ...
import { cloneDeep } from 'lodash';
// ...
function removeStyle(index) {
let newStyles = cloneDeep(styles); // Copy styles
newStyles.splice(index, 1); // Splice from copy
setStyles(newStyles); // Assign copy to styles
}
// ...
<Button onClick={() => removeStyle(styles.length - 1)}>x</Button>
// ...
If you want to use a different clone function or write your own, there is a performance benchmark here:
"What is the most efficient way to deep clone an object in JavaScript?"
I would also move the function assigned to the onClick event handler in the button outside of the render function. It looks like you are calling setStyles which adds a button with a removeStyle event which itself calls setStyles. Once you move it out, you may be able to better diagnose your issue.
Update
I rewrote your component below. Try to render elements using the map method.
import React, { useState } from "react";
import { Button } from "reactstrap";
const Controls = (props) => {
const [styles, setStyles] = useState([]);
const removeStyle = (index) => {
const newStyles = [...styles];
newStyles.splice(index, 1);
setStyles(newStyles);
};
const getChildNodeIndex = (elem) => {
let position = 0;
let curr = elem.previousSibling;
while (curr != null) {
if (curr.nodeType !== Node.TEXT_NODE) {
position++;
}
curr = curr.previousSibling;
}
return position;
};
const handleRemove = (e) => {
//removeStyle(parseInt(e.target.dataset.index, 10));
removeStyle(getChildNodeIndex(e.target.closest("div")));
};
const handleAdd = (e) => setStyles([...styles, styles.length]);
return (
<div>
{styles.map((style, index) => (
<div key={index}>
{style}
<Button data-index={index} onClick={handleRemove}>
×
</Button>
</div>
))}
<Button color="primary" onClick={handleAdd}>
+
</Button>
</div>
);
};
export default Controls;
I've added the most preferred way in a new answer as the previous one was becoming too long.
The explanation lies in my previous answer.
import React, { useState } from "react";
import { Button } from "reactstrap";
export default function Controls(props) {
const [styles, setStyles] = useState([]);
function removeStyle(index) {
let newStyles = [...styles]
newStyles.splice(index, 1);
setStyles(newStyles);
}
const addStyle = () => {
const newStyles = [...styles];
newStyles.push({content: 'ABC'});
setStyles(newStyles);
};
// we are mapping through styles and adding removeStyle newly and rerendering all the divs again every time the state updates with new styles.
// this always ensures that the removeStyle callback has reference to the latest state at all times.
return (
<div>
{styles.map((style, index) => {
return (
<div>
<p>{style.content} - Index: {index}</p>
<Button onClick={() => removeStyle(index)}>x</Button>
</div>
);
})}
<Button color="primary" onClick={addStyle}>+</Button>
</div>
);
}
Here is a CodeSandbox for you to play around.
Let's try to understand what's going on here.
<Button
color="primary"
onClick={() => {
setStyles(styles.concat(
<div>
<StyleRow />
<Button onClick={() => removeStyle(styles.length)}>x</Button>
</div>
));
}}
>
+
</Button>
First render:
// styles = []
You add a new style.
// styles = [<div1>]
The remove callback from the div is holding the reference to styles, whose length is now 0
You add one more style. // styles = [<div1>, <div2>]
Since div1 was created previously and didn't get created now, its still holding a reference to styles whose length is still 0.
div2 is now holding a reference to styles whose length is 1.
Now the same goes for the removeStyle callback that you have. Its a closure, which means it's holding a reference to a value of its outer function, even after the outer function has done executing. So when removeStyles is called for the first div1 the following lines will execute:
let newStyles = styles; // newStyles []
newStyles.splice(index, 1); // index(length) = 0;
// newStyles remain unchanged
setStyles(newStyles); // styles = [] (new state)
Now consider you have added 5 styles. So this is how the references will be held by each div
div1 // styles = [];
div2 // styles = [div1];
div3 // styles = [div1, div2];
div4 // styles = [div1, div2, div3];
div5 // styles = [div1, div2, div3, div4];
So what happens if you try to remove div3, the following removeStyly will execute:
let newStyles = styles; // newStyles = [div1, div2]
newStyles.splice(index, 1); // index(length) = 2;
// newStyles remain unchanged; newStyles = [div1, div2]
setStyles(newStyles); // styles = [div2, div2] (new state)
Hope that helps and addresses your concern. Feel free to drop any questions in the comments.
Here is a CodeSandbox for you to play around with and understand the issue properly.