I am trying to make a simple game like purple pairs in Purple palace game using React. I have a method named clickBtn, it is supposed to increment the count on click and decrement on second click but I don't know why the handleClick method is changing chosen and clicked property of state even if a copy of the new state is made without using setState method. Can you help me fix this?
class GameBoard extends React.Component {
constructor() {
super();
this.state = {
score: 0,
time: 0,
list: [...generateObList(), ...generateObList()],
count: 0
};
}
handleClick = (id) => {
this.clickBtn(id);
const list = this.state.list.slice();
const current = list.find((a) => a.id === id);
for (let x of list) {
if (x.clicked && x.value == list.find((a) => a.id == id).value) {
x.chosen = true;
x.clicked = false;
current.chosen = true;
current.clicked = false;
// this.setState((prev) => ({
// list: prev.list.map((el) =>
// el.id === id ? current : el.value === current.value ? x : el
// ),
// score: prev.score + 1
// }));
}
}
};
clickBtn = (id) => {
const current = this.state.list.slice().find((e) => e.id === id);
let deClick = current.clicked;
current.clicked = !current.clicked;
console.log(current);
this.setState(
(prev) => ({
list: prev.list.map((el) => (el.id === id ? current : el)),
count: prev.count + (deClick ? -1 : 1)
}),
() => {
console.log(this.state.count, deClick, current);
}
);
};
render() {
const boardStyle = {
gridTemplateColumns: `repeat(5, 1fr)`,
gridTemplateRows: `repeat(5,1r)`
};
let list = this.state.list.map((n) => (
<Card
value={n.value}
onClick={(e) => {
this.handleClick(n.id);
}}
show={!n.chosen}
/>
));
return (
<div class="gameBoard" style={boardStyle}>
{list}
</div>
);
}
}
slice() just return a shallow copy of the array so both original and copy refer to the same array items if the items are reference type ( objects ).
For object slice copies object references into the new array. Both the
original and new array refer to the same object. If a object changes,
the changes are visible to both the new and original arrays.
Try to deep copy the objects. You can easily do deep copy by using, JSON.parse(JSON.stringify(arr))
Also you can try lodash's cloneDeep() method.
For more details about deep cloning - https://stackoverflow.com/a/122704/11306028
Related
I trying to update the quantity by pressing the "up" button.
This is my code.
const increaseHandler = (x) => {
let newCart = [...props.cartItems];
let exist = newCart.find((item) => item.id === x.id);
if (exist) {
exist.quantity++;
} else {
exist = {
...props.cartItems,
quantity: 1,
};
}
newCart.push(exist);
props.setCartItems(newCart);
};
Here is the button:
<button className="up" onClick={() => increaseHandler(items)}>
up
</button>
Every time I click on the "up" button, it appends a duplicate of the item on the cartItems
I increased the quantity but it appends the object to the array.
You can update by using map:
const increaseHandler = (x) => {
props.setCartItems((preState) =>
preState.map((item) => (item.id === x.id ? { ...item, quantity: item.quantity + 1 } : item)),
);
};
Even though you've shallow copied the cart items array you are still mutating the individual items with the post increment.
const increaseHandler = (x) => {
let newCart = [...props.cartItems];
let exist = newCart.find((item) => item.id === x.id);
if (exist) {
exist.quantity++; // <-- mutation!!
} else {
exist = {
...props.cartItems,
quantity: 1,
};
}
newCart.push(exist); // <-- adds duplicate items
props.setCartItems(newCart);
};
You still need to create a new item reference, shallow copying the properties and updating the quantity property. You should only push new elements into the cart array.
const increaseHandler = (x) => {
const newCart = [...props.cartItems];
let exist = newCart.find((item) => item.id === x.id);
if (exist) {
exist = {
...exist,
quantity: exist.quantity + 1;
};
} else {
exist = {
...props.cartItems,
quantity: 1,
};
newCart.push(exist);
}
props.setCartItems(newCart);
};
It's more common to map the previous state to the next state, and I suggest using a functional state update to ensure correctly updating from the previous state versus any state closed over in callback scope (possibly stale).
const increaseHandler = (x) => {
props.setCartItems(items => {
const inCart = items.some((item) => item.id === x.id);
if (inCart) {
return items.map((item) => item.id === x.id
? {
...item,
quantity: item.quantity + 1,
}
: item);
}
return items.concat({
...props.cartItems,
quantity: 1,
});
});
};
You're setting the entire cartItems array into the single exist object you've created. So you're adding the entire cart to exist and then setting exist.quantity to be 1, and pushing that whole thing into newCart and setting it into state.
Try spreading exist rather than props.cartItems in your else block.
Based on your images and nomenclature, is this the FreeCodeCamp/Wiebenfalk tutorial on react & TS? If so, I did the same tutorial, and here's my add to cart function:
const handleAddToCart = (clickedItem: CartItemType) => {
setCartItems((prevState) => {
// check if item is in cart by looking for id of clicked item in array of cartItems already in state
const isItemInCart = prevState.find((item) => item.id === clickedItem.id);
if (isItemInCart) {
// search for item id matching clicked item; increment that amount
return prevState.map((item) =>
item.id === clickedItem.id
? { ...item, amount: item.amount + 1 }
: item
);
}
return [...prevState, { ...clickedItem, amount: 1 }];
});
};
My code is basically a form with a text input and a submit button. Each time the user input data, my code adds it to an array and shows it under the form.
It is working fine; however, when I add duplicate values, it still adds it to the list. I want my code to count these duplicates and show them next to each input.
For example, if I input two "Hello" and one "Hi" I want my result to be like this:
2 Hello
1 Hi
Here is my code
import React from 'react';
import ShoppingItem from './ShoppingItem';
class ShoppingList extends React.Component {
constructor (props){
super(props);
this.state ={
shoppingCart: [],
newItem :'',
counter: 0 };
}
handleChange =(e) =>
{
this.setState ({newItem: e.target.value });
}
handleSubmit = (e) =>
{
e.preventDefault();
let newList;
let myItem ={
name: this.state.newItem,
id:Date.now()
}
if(!this.state.shoppingCart.includes(myItem.name))
{
newList = this.state.shoppingCart.concat(myItem);
}
if (this.state.newItem !=='')
{
this.setState(
{
shoppingCart: newList
}
);
}
this.state.newItem ="" ;
}
the rest of my code is like this:
render(){
return(
<div className = "App">
<form onSubmit = {this.handleSubmit}>
<h6>Add New Item</h6>
<input type = "text" value = {this.state.newItem} onChange ={this.handleChange}/>
<button type = "submit">Add to Shopping list</button>
</form>
<ul>
{this.state.shoppingCart.map(item =>(
<ShoppingItem item={item} key={item.id} />
)
)}
</ul>
</div>
);
}
}
export default ShoppingList;
Issues
this.state.shoppingCart is an array of objects, so this.state.shoppingCart.includes(myItem.name) will always return false as it won't find a value that is a string.
this.state.newItem = ""; is a state mutation
Solution
Check the newItem state first, if empty then return early
Search this.state.shoppingCart for the index of the first matching item by name property
If found then you want to map the cart to a new array and then also copy the item into a new object reference and update the quantity.
If not found then copy the array and append a new object to the end with an initial quantity 1 property.
Update the shopping cart and newItem state.
Code
handleSubmit = (e) => {
e.preventDefault();
if (!this.state.newItem) return;
let newList;
const itemIndex = this.state.shoppingCart.findIndex(
(item) => item.name === this.state.newItem
);
if (itemIndex !== -1) {
newList = this.state.shoppingCart.map((item, index) =>
index === itemIndex
? {
...item,
quantity: item.quantity + 1
}
: item
);
} else {
newList = [
...this.state.shoppingCart,
{
name: this.state.newItem,
id: Date.now(),
quantity: 1
}
];
}
this.setState({
shoppingCart: newList,
newItem: ""
});
};
Note: Remember to use item.name and item.quantity in your ShoppingItem component.
Replace your "handleSubmit" with below one and check
handleSubmit = (e) => {
e.preventDefault();
const { shoppingCart, newItem } = this.state;
const isInCart = shoppingCart.some(({ itemName }) => itemName === newItem);
let updatedCart = [];
let numberOfSameItem = 1;
if (!isInCart && newItem) {
updatedCart = [
...shoppingCart,
{
name: `${numberOfSameItem} ${newItem}`,
id: Date.now(),
itemName: newItem,
counter: numberOfSameItem
}
];
} else if (isInCart && newItem) {
updatedCart = shoppingCart.map((item) => {
const { itemName, counter } = item;
if (itemName === newItem) {
numberOfSameItem = counter + 1;
return {
...item,
name: `${numberOfSameItem} ${itemName}`,
itemName,
counter: numberOfSameItem
};
}
return item;
});
}
this.setState({
shoppingCart: updatedCart,
newItem: ""
});
};
Below is my attempt to create an array of classes. The functionality of app is next: one can add or delete extra Input box and increase or decrease its value. As a result the app displays the sum of the all present tags. The issue comes with Delete function, when deleting any of components from created list it does correct math in array but rerenders the elements incorrectly. It always deletes the last component on the list even when you try to remove any others. Any hint why it's happening? Thanks
class Trade1 extends React.Component {
state = {
vl: this.props.value
}
change = (v) => {
let newValue
if (v) {
newValue = this.state.vl + 1
} else {
newValue = this.state.vl - 1
}
this.setState({vl: newValue})
this.props.onChange(newValue, this.props.index)
}
render() {
const {value, index} = this.props
return (
<div>
<button onClick={() => this.change(false)}>Down</button>
<input class="v_price" value={`${this.state.vl}`}/>
<button onClick={() => this.change(true)}>Up</button>
<button onClick={() => this.props.delete(this.props.index)}>Delete</button>
</div>
)
}
}
class Parent extends React.Component {
constructor(props){
super(props);
this.state = {
arr: [0,0,0]
}
}
onChange = (v, i) => {
let newArr = this.state.arr
newArr[i] = v
this.setState(newArr)
}
plus = () => {
let a = this.state.arr
a.push(0)
this.setState({arr: a})
}
minus = i => {
let a = this.state.arr
a.splice(i, 1)
console.log(a)
this.setState({arr: a})
}
render() {
return (
<div>
{this.state.arr.map((v, i) =>
{
return <Trade1 value={v} index={i} onChange={this.onChange} delete={this.minus}/>
}
)}
<div>{
this.state.arr.reduce((a, b) => a+b, 0 )
}</div>
<div><button onClick={this.plus}>Plus</button></div>
</div>
)
}
}
ReactDOM.render(<Parent />, document.getElementById('root'));
You are mutating the array, you should use filter and remove the element at index which you pass as an argument
minus = i => {
this.setState({
arr: this.state.arr.filter((x, j) => j !== i)
})
}
Issue
You've some state mutations. Try to use functional state updates and always return new state objects.
onChange = (v, i) => {
this.setState(prevState => ({
arr: prevState.arr.map((el, index) => index === i ? v : el)
}));
}
plus = () => {
this.setState(prevState => ({
arr: [...prevState.arr, 0],
}));
}
minus = i => {
this.setState(prevState => ({
arr: prevState.arr.filter((_, index) => index !== i),
}));
}
I am trying to set the state of features, which is an array, based on inputs into textarea. I am having no luck setting the state of features. When I press one key, that key becomes the next index value in the state. If I hit another key, that key becomes the next index value in the state. So if I manually set my initial state of features to features: ["First", "Second", "Third"],, the result will be features: ["First", "Second", "Third", "Firstf" , "Firstd"] if I hit f and then d
The relevant State
class CreateItem extends Component {
state = {
features: [""],
};
This is how I am attempting to create textarea's based on the state
<label htmlFor="features">
Features
{this.state.features.map((feature, i) => (
<div key={i}>
<textarea
key={i}
type="features"
placeholder={`${feature}`}
name={`features.${i}`}
value={this.state.features[`${i}`]}
onChange={this.handleFeatureArrayChange}
onKeyDown={this.keyPress}
/>
</div>
))}
</label>
Here is the handleChange
handleFeatureArrayChange = e => {
const { name, type, value } = e.target;
this.setState(prevState => ({
[name]: [...prevState.features, value]
}));
};
This is how I am attempting to create new textarea's each time the user hits enter:
keyPress = e => {
if (e.keyCode == 13) {
this.setState({
features: this.state.features.concat("")
});
}
};
The problem is you're just appending to state each time a key is pressed.
handleFeatureArrayChange = e => {
const { name, type, value } = e.target;
this.setState(prevState => ({
[name]: [...prevState.features, value] // this is effectively the same as .concat(value)
}));
};
You want to update the value at the current index. Try changing the name of the input to just be key and then mapping instead
handleFeatureArrayChange = e => {
const { name, type, value } = e.target;
this.setState(prevState => ({
features: prevState.features.map((feat, key) => {
if (key == name) {
return value
} else
return feat;
}
)
}));
};
I am mapping multiple radio buttons (group) options and when the user click on radio buttons, would like to add selected values and uniqueIds to an array.
with my current code I can get the value that I am currently clicking on but can't add to array.
{result !== null && result.length > 0 ? (
<div>
{result.map(item => {
const label = item.question
const listOptions = item.options.map(item => {
return (
<Radio
name={item.uniqueId}
inline
value={item.uniqueId}
key={item.uniqueId}
className="radio-options"
checked={item.checked}
onChange={e => {
this.handleChange(label, uniqueId);
}}
>
{item.options}
</Radio>
);
});
return (
<div className="radio-options-overview">
<p>{label}</p>
{listOptions}
</div>
);
})}
handleChange = (label, uniqueId) => {
console.log(label, uniqueId);
let addToArray = [];
this.setState({ selectedItems: addToArray})
};
array would look something like this,
[
{ "label": "ageRange", "uniquId": 2 },
{ "label": "smoker", "uniquId": 1 },
{ "label": "exOption", "uniquId": 3 }
]
You are nearly there. #Clarity provided good solution.
if you wanting to replace exisiting value and replace it with new one
Try This
handleChange = (label, uniqueId) => {
const { selectedItems } = this.state
// Find items that already exists in array
const findExistingItem = selectedItems.find((item) => {
return item.uniqueId === uniqueId;
})
if(findExistingItem) {
// Remove found Item
selectedItems.splice(findExistingItem);
// set the state with new values/object
this.setState(state => ({
selectedItems: [...state.selectedItems, {
label, uniqueId
}]
}))
} else {
// if new Item is being added
this.setState(state => ({
selectedItems: [...state.selectedItems, {
label, uniqueId
}]
}))
}
};
You can do like this:
handleChange = (label, uniqueId) => {
this.setState(state => ({ selectedItems: [...state.selectedItems, uniqueId]}));
};
By using array spread and functional form of setState you make sure that you don't directly mutate the state and add the items to the latest state.
In case you'd want to add an object with a label: uniqueId pair, you could do like so:
handleChange = (label, uniqueId) => {
this.setState(state => ({
selectedItems: [...state.selectedItems, {
[label]: uniqueId
}]
}));
};
EDIT: If you want to overwrite the items with the same labels, the easiest would be to store them as an object and not an array, so the item with the same label would overwrite an existing one:
handleChange = (label, uniqueId) => {
this.setState(state => {
return { selectedItems: { ...state.selectedItems, [label]: uniqueId } };
});
};
Honestly, I don't understand, what are you trying to do, but if you need to add object (or something else) inside an array, you could use .push() method. For example:
let addToArray = [];
let test = {"label": "ageRange", "uniquId": 2};
addToArray.push(test);
console.log(addToArray); //[{label: "ageRange", uniquId: 2}]