I'm using setState to update a particular object but for some reason, another object is being mutated as well even though I haven't added it in the setState function.
Basically I have an 'add to cart' function, if item already exists in cart state object, just increments it's quantity by 1. If not, update state of cart with a copy that contains said item.
this.state = {
items: {
0: { name: 'Red shirt', price: 10 },
1: { name: 'Blue shirt', price: 11 },
2: { name: 'Green shirt', price: 12 },
3: { name: 'Yellow shirt', price: 13 }
},
cart: {},
user: {}
}
addToCart = (key, item) => {
let newCart;
newCart = this.state.cart;
// item already exists in cart
if (newCart[key]) {
// increment qty of added item
newCart[key].qty++;
// does not exist, need to add to cart
} else {
newCart[key] = item;
newCart[key].qty = 1;
}
this.setState({ cart: newCart });
}
render() {
const { cart, items } = this.state;
return (
<div className="App">
<h1>Streat</h1>
<Checkout cart={cart} />
<Items
items={items}
addToCart={this.addToCart}
/>
</div>
);
}
}
// Items component which holds Item component
class Items extends Component {
constructor(props) {
super(props)
}
render() {
const { items, addToCart } = this.props;
return (
<div>
{
map(items, function renderItems(item, key) {
return <Item
item={item}
itemRef={key}
key={key}
addToCart={addToCart} />
})
}
</div>
)
}
}
// Item functional component which has the add to cart button
const Item = (props) => {
const { item, itemRef, addToCart } = props;
return (
<div>
<p>{item.name}</p>
<p>{item.price}</p>
<p>{item.qty || ""}</p>
<button onClick={() => addToCart(itemRef, item)}>Add</button>
</div>
)
}
When inspecting the state of my items object in react developer tools, I see that every time I click the button to add an item to the cart, it does correctly add the item to the cart / update the quantity of the item but it also updates the item in the items object in my state, adding a 'qty' key/ value pair which is not what I want.
I'm using setState on my cart object but my items object is being changed too for some reason.
Try to update your state by using spread syntax so you don't get into this kind of trouble. Redux also uses that a lot. By doing it you don't mutate your object and create a clean copy of them.
Your code is not copying the item on line (newCart[key] = item;). Instead, it is putting a reference from the same item and by changing the qty in the next line you consequently update it as well in the items key.
const Item = ({ name, price, onClick }) =>
<div onClick={onClick}>
{name} {price}
</div>
class App extends React.Component {
constructor() {
super()
this.state = {
items: {
0: { name: 'Red shirt', price: 10 },
1: { name: 'Blue shirt', price: 11 },
2: { name: 'Green shirt', price: 12 },
3: { name: 'Yellow shirt', price: 13 }
},
cart: {},
user: {}
}
}
addToCart(key, item) {
const hasItem = this.state.cart[key]
this.setState({
...this.state,
cart: {
...this.state.cart,
[key]: {
...(hasItem ? this.state.cart[key] : item),
qty: hasItem ? this.state.cart[key].qty + 1 : 1,
},
},
})
}
render() {
const { cart, items } = this.state
return (
<div>
<div>Click to add:</div>
{Object.keys(items).map(key =>
<Item
{...items[key]}
key={key}
onClick={this.addToCart.bind(this, key, items[key])}
/>
)}
<div style={{ marginTop: 20 }}>
{Object.keys(cart).map(key =>
<div key={key}>{cart[key].name}:{cart[key].qty}</div>
)}
</div>
</div>
)
}
}
ReactDOM.render(
<App />,
document.getElementById('root')
)
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="root"></div>
Related
I'm working on a shopping cart.
<Cart /> is Cart Page which render Products in cartList Array.
<CartProduct /> render each one product in cartList Array
I want to make the quantity data change, when i click quantity button.
Here is My First Try Code
function Cart(props) {
const cartsList = useRecoilValue(cartsListState);
return(
{cartsList
.filter(cart => cart.keep === 'cold')
.map((cart) => {
return <CartProduct cart={cart} getNowQuantity={getNowQuantity} />
})
}
{cartsList
.filter(cart => cart.keep === 'freeze')
.map((cart) => {
return <CartProduct cart={cart} getNowQuantity={getNowQuantity} />
})
}
{cartsList
.filter(cart => cart.keep === 'normal')
.map((cart) => {
return <CartProduct cart={cart} getNowQuantity={getNowQuantity} />
})
}
)
}
function CartProduct({ cart, getNowQuantity}) {
const [cartsList, setCartsList] = useRecoilState(cartsListState);
return(
const PlusQuantity = () => {
setCounterQuantity(counterQuantity => counterQuantity + 1);
cart.quantity += 1;
getNowQuantity(cart.quantity);
}
const MinusQuantity = () => {
if (cart.quantity>=2) {
setCounterQuantity(counterQuantity => counterQuantity - 1);
cart.quantity -= 1;
getNowQuantity(cart.quantity);
}
else return;
}
)
}
Firts code make error
Uncaught TypeError: Cannot assign to read only property 'quantity' of
object '#
So i tried to use spread operator in CartProduct.js Like this way
const cartProduct = ([ ...cart]);
return(
CartProduct.quantity = +1
~~
~~~
)
This code make error
cart is not iterable
so i tried
let iterableCart = cart[Symbol.iterator]
It doesn't work.
How can i change cart.property for ChangeQuantityButton?
By default Recoil makes everything immutable so code like this
cart.quantity += 1;
won't work because you're trying to update a value on a frozen object.
Instead you need to create a new object, and use the existing values to update it.
Here I'm using a sample data set and using it to build three product items using an <Item> component. This component displays the name, the current quantity, and two buttons to decrement/increment the values. On the buttons are a couple of data attributes that identify the product id, and the button action.
When a button is clicked the handleClick function is called. This destructures the id and action from the button dataset, and then map over the current cart using those values to check the cart items and return updated objects to the state.
const { atom, useRecoilState, RecoilRoot } = Recoil;
// Initialise the cart data
const cart = [
{ id: 1, name: 'Banana', qty: 0 },
{ id: 2, name: 'Beef', qty: 0 },
{ id: 3, name: 'Mop', qty: 0 }
];
// Initialise the cart atom setting its
// default to the cart data
const cartAtom = atom({
key: 'cartAtom',
default: cart
});
function Example() {
// Use the recoil state
const [ cart, setCart ] = useRecoilState(cartAtom);
// When a button is clicked
function handleClick(e) {
// Get its id and action
const { dataset: { id, action } } = e.target;
// Update the cart using the id to identify the item
// and return an updated object where appropriate
setCart(prev => {
return prev.map(item => {
if (item.id === +id) {
if (action === 'decrement' && item.qty > 0) {
return { ...item, qty: item.qty - 1 };
}
if (action === 'increment') {
return { ...item, qty: item.qty + 1 };
}
}
return item;
});
});
}
// Iterate over the cart data using the Item
// component to display the item details
return (
<div>
{cart.map(item => {
const { id, name, qty } = item;
return (
<Item
key={id}
id={id}
name={name}
qty={qty}
handleClick={handleClick}
/>
);
})}
</div>
);
}
function Item({ id, name, qty, handleClick }) {
return (
<div className="item">
<span className="name">{name}</span>
<button
data-id={id}
data-action="decrement"
type="button"
onClick={handleClick}
>-
</button>
{qty}
<button
data-id={id}
data-action="increment"
type="button"
onClick={handleClick}
>+
</button>
</div>
);
}
ReactDOM.render(
<RecoilRoot>
<Example />
</RecoilRoot>,
document.getElementById('react')
);
.item:not(:last-child) { margin-bottom: 0.25em; }
.name { display: inline-block; width: 60px; }
button { width: 30px; height: 30px; margin: 0 0.25em; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.production.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/recoil#0.7.6/umd/index.js"></script>
<div id="react"></div>
I want to change the text of a specific button when I click on that button in React. But the issue is when I click the button the title will change for all buttons!
class Results extends Component {
constructor() {
super();
this.state = {
title: "Add to watchlist"
}
}
changeTitle = () => {
this.setState({ title: "Added" });
};
render() {
return (
<div className='results'>
{
this.props.movies.map((movie, index) => {
return (
<div className='card wrapper' key={index}>
<button className='watchListButton' onClick={this.changeTitle}>{this.state.title}</button>
</div>
)
})
}
</div>
)
}
}
You would need to come up with a mechanism to track added/removed titles per movie. For that, you would have to set your state properly. Example:
this.state = {
movies: [
{id: 1, title: 'Casino', added: false},
{id: 2, title: 'Goodfellas', added: false}
]
This way you can track what's added and what's not by passing the movie id to the function that marks movies as Added/Removed. I have put together this basic Sandbox for you to get you going in the right direction:
https://codesandbox.io/s/keen-moon-9dct9?file=/src/App.js
And here is the code for future reference:
import React, { Component } from "react";
import "./styles.css";
class App extends Component {
constructor() {
super();
this.state = {
movies: [
{ id: 1, title: "Casino", added: false },
{ id: 2, title: "Goodfellas", added: false }
]
};
}
changeTitle = (id) => {
this.setState(
this.state.movies.map((item) => {
if (item.id === id) item.added = !item.added;
return item;
})
);
};
render() {
const { movies } = this.state;
return (
<div className="results">
{movies.map((movie, index) => {
return (
<div className="card wrapper" key={index}>
{movie.title}
<button
className="watchListButton"
onClick={() => this.changeTitle(movie.id)}
>
{movie.added ? "Remove" : "Add"}
</button>
</div>
);
})}
</div>
);
}
}
export default App;
I have an issue with my reactjs code where the render() function won't print an array in the order that it is stored in my state object.
Here's my code which is pretty simple:
import React from "react";
export default class DonationDetail extends React.Component {
constructor(props) {
super(props);
this.state = { content: [] };
}
componentDidMount() {
let state = this.state;
state.content.push({ food: "burger" });
state.content.push({ food: "pizza" });
state.content.push({ food: "tacos" });
this.setState(state);
}
addPaymentItem() {
const item = { food: "" };
let state = this.state;
state.content.unshift(item);
this.setState(state);
}
render() {
console.log(this.state);
let ui = (
<div>
<button type="button" onClick={() => this.addPaymentItem()}>
add to top
</button>
{this.state.content.map((item, key) => (
<input type="text" key={key} defaultValue={item.food} />
))}
</div>
);
return ui;
}
}
When you press the button add to top, a new item is placed to the front of the state.content array, which you can verify from the console.log(this.state) statement. But what's unusual is that the HTML that is generated does NOT add this new item to the top of the user interface output. Instead, another input field with the word taco is placed at the bottom of the list in the user interface.
Why won't ReactJS print my state.content array in the order that it is actually stored?
You can use the array index as key when the order of the elements in the array will not change, but when you add an element to the beginning of the array the order is changed.
You could add a unique id to all your foods and use that as key instead.
Example
class DonationDetail extends React.Component {
state = { content: [] };
componentDidMount() {
const content = [];
content.push({ id: 1, food: "burger" });
content.push({ id: 2, food: "pizza" });
content.push({ id: 3, food: "tacos" });
this.setState({ content });
}
addPaymentItem = () => {
const item = { id: Math.random(), food: "" };
this.setState(prevState => ({ content: [item, ...prevState.content] }));
};
handleChange = (event, index) => {
const { value } = event.target;
this.setState(prevState => {
const content = [...prevState.content];
content[index] = { ...content[index], food: value };
return { content };
});
};
render() {
return (
<div>
<button type="button" onClick={this.addPaymentItem}>
add to top
</button>
{this.state.content.map((item, index) => (
<input
type="text"
key={item.id}
value={item.food}
onChange={event => this.handleChange(event, index)}
/>
))}
</div>
);
}
}
ReactDOM.render(<DonationDetail />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="root"></div>
Instead of:
componentDidMount() {
let state = this.state;
state.content.push({ food: "burger" });
state.content.push({ food: "pizza" });
state.content.push({ food: "tacos" });
this.setState(state);
}
Try
componentDidMount() {
this.setState(prevState => ({
content: [
...prevState.content,
{ food: "burger" },
{ food: "pizza" },
{ food: "tacos" },
]
}));
}
and
addPaymentItem() {
const item = { food: "" };
let state = this.state;
state.content.unshift(item);
this.setState(state);
}
to
addPaymentItem() {
this.setState(prevState => ({
content: [
{ food: "" },
...prevState.content,
]
}));
}
I am having 4 buttons each button have name id and selected boolean flag.
What I am trying to achieve is, on click of button, boolean button flag should be changed of that particular button. For this, I need to setState in map function for that particular button Id.
My issue is I am unable to setState in map function for that particular clicked button, its btnSelected should be changed
My aim is to create a multi-select deselect button.Its kind of interest selection for the user and based on that reflect the UI as well my array. Here is my code.
Thanks in anticipation.
import React, { Component } from "react";
import { Redirect } from "react-router-dom";
export default class Test extends Component {
constructor(props, context) {
super(props, context);
this.handleChange = this.handleChange.bind(this);
this.state = {
value: "",
numbers: [1, 2, 3, 4, 5],
posts: [
{
id: 1,
topic: "Animal",
btnSelected: false
},
{
id: 2,
topic: "Food",
btnSelected: false
},
{
id: 3,
topic: "Planet",
btnSelected: false
},
{ id: 4, topic: "Nature", btnSelected: false }
],
allInterest: []
};
}
handleChange(e) {
//console.log(e.target.value);
const name = e.target.name;
const value = e.target.value;
this.setState({ [name]: value });
}
getInterest(id) {
this.state.posts.map(post => {
if (id === post.id) {
//How to setState of post only btnSelected should change
}
});
console.log(this.state.allInterest);
if (this.state.allInterest.length > 0) {
console.log("Yes we exits");
} else {
console.log(id);
this.setState(
{
allInterest: this.state.allInterest.concat(id)
},
function() {
console.log(this.state);
}
);
}
}
render() {
return (
<div>
{this.state.posts.map((posts, index) => (
<li
key={"tab" + index}
class="btn btn-default"
onClick={() => this.getInterest(posts.id)}
>
{posts.topic}
<Glyphicon
glyph={posts.btnSelected === true ? "ok-sign" : "remove-circle"}
/>
</li>
))}
</div>
);
}
}
Here's how you do something like this:
class App extends Component {
state = {
posts: [{
name: 'cat',
selected: false,
}, {
name: 'dog',
selected: false
}]
}
handleClick = (e) => {
const { posts } = this.state;
const { id } = e.target;
posts[id].selected = !this.state.posts[id].selected
this.setState({ posts })
}
render() {
return (
<div>
<form>
{this.state.posts.map((p, i) => {
return (
<div>
<label>{p.name}</label>
<input type="radio" id={i} key={i} checked={p.selected} onClick={this.handleClick} />
</div>
)
})}
</form>
</div>
);
}
}
render(<App />, document.getElementById('root'));
Working example here.
You can do this by passing the index from the map into each button's handleClick function, which would then return another function that can be triggered by an onClick event.
In contrast to Colin Ricardo's answer, this approach avoids adding an id prop onto each child of the map function that is only used for determining the index in the handleClick. I've modified Colin's example here to show the comparison. Notice the event parameter is no longer necessary.
class App extends Component {
state = {
posts: [{
name: 'cat',
selected: false,
}, {
name: 'dog',
selected: false
}]
}
handleClick = (index) => () => {
const { posts } = this.state;
posts[index].selected = !this.state.posts[index].selected
this.setState({ posts })
}
render() {
return (
<div>
<form>
{this.state.posts.map((p, i) => {
return (
<div>
<label>{p.name}</label>
<input type="checkbox" key={i} checked={p.selected} onClick={this.handleClick(i)} />
</div>
)
})}
</form>
</div>
);
}
}
render(<App />, document.getElementById('root'));
Working example here
The following code creates a simple movie rating app. Everything works except that when an up or down vote is clicked in one of the array items, the votes state for all items in the array update, rather than just for the item that was clicked. How do I code this so that the vote only applies to the item where it was clicked?
class Ratings extends React.Component {
constructor(props){
super(props);
this.state = {
votes: 0
};
this.add = this.add.bind(this);
this.subtract = this.subtract.bind(this);
this.reset = this.reset.bind(this);
}
add(event){
this.setState ({
votes: this.state.votes + 1
})
}
subtract(event){
this.setState ({
votes: this.state.votes - 1
})
}
reset(event){
this.setState ({
votes: 0
})
}
render () {
this.movies = this.props.list.map(x => {
return (
<div key={x.id} className="movierater">
<MoviePoster poster={x.img}/>
<h1 className="title">{x.name}</h1>
<div className="votewrapper">
<button onClick={this.add}><i className="votebutton fa fa-thumbs-o-up" aria-hidden="true"></i></button>
<Votes count={this.state.votes} />
<button onClick={this.subtract}><i className="votebutton fa fa-thumbs-o-down" aria-hidden="true"></i></button>
</div>
<button onClick={this.reset} className="reset">Reset</button>
</div>
)
});
return (
<div>
{this.movies}
</div>
);
}
}
function MoviePoster(props) {
return (
<img src={props.poster} alt="Movie Poster" className="poster"/>
);
}
function Votes(props) {
return (
<h2>Votes: {props.count}</h2>
);
}
var movieposters = [
{id: 1, img:"http://www.impawards.com/2017/posters/med_alien_covenant_ver4.jpg", name: "Alien Covenant"},
{id: 2, img:"http://www.impawards.com/2017/posters/med_atomic_blonde_ver4.jpg", name: "Atomic Blonde"},
{id: 3, img:"http://www.impawards.com/2017/posters/med_easy_living_ver3.jpg", name: "Easy Living"},
{id: 4, img:"http://www.impawards.com/2017/posters/med_once_upon_a_time_in_venice_ver3.jpg", name: "Once Upon a Time in Venice"},
{id: 5, img:"http://www.impawards.com/2017/posters/med_scorched_earth.jpg", name: "Scorched Earth"},
{id: 6, img:"http://www.impawards.com/2017/posters/med_underworld_blood_wars_ver9.jpg", name: "Underworld: Blood Wars"},
{id: 7, img:"http://www.impawards.com/2017/posters/med_void.jpg", name: "The Void"},
{id: 8, img:"http://www.impawards.com/2017/posters/med_war_for_the_planet_of_the_apes.jpg", name: "War for the Planet of the Apes"},
]
ReactDOM.render(
<Ratings list={movieposters} />,
document.getElementById('app')
);
You need a separate vote count for each movie entity.
This can be accomplished by providing an id to each movie and setting the vote for that specific movie by id.
I would also recommend to extract a new component for a Movie.
this component will get the movieId as a prop and the handlers, it will invoke the up or down handlers and provide to them the current movieId.
See a running example:
class Movie extends React.Component {
onSubtract = () => {
const { subtract, movieId } = this.props;
subtract(movieId);
};
onAdd = () => {
const { add, movieId } = this.props;
add(movieId);
};
onReset = () => {
const { reset, movieId } = this.props;
reset(movieId);
};
render() {
const { movie, votes = 0 } = this.props;
return (
<div className="movierater">
<MoviePoster poster={movie.img} />
<h1 className="title">{movie.name}</h1>
<div className="votewrapper">
<button onClick={this.onAdd}>
<i className="votebutton fa fa-thumbs-o-up" aria-hidden="true" />
</button>
<Votes count={votes} />
<button onClick={this.onSubtract}>
<i className="votebutton fa fa-thumbs-o-down" aria-hidden="true" />
</button>
</div>
<button onClick={this.onReset} className="reset">
Reset
</button>
</div>
);
}
}
class Ratings extends React.Component {
constructor(props) {
super(props);
this.state = {
allVotes: {}
};
}
subtract = movieId => {
const { allVotes } = this.state;
const currentVote = allVotes[movieId] || 0;
const nextState = {
...allVotes,
[movieId]: currentVote - 1
};
this.setState({allVotes: nextState});
};
add = movieId => {
const { allVotes } = this.state;
const currentVote = allVotes[movieId] || 0;
const nextState = {
...allVotes,
[movieId]: currentVote + 1
};
this.setState({ allVotes: nextState });
};
reset = movieId => {
const { allVotes } = this.state;
const nextState = {
...allVotes,
[movieId]: 0
};
this.setState({ allVotes: nextState });
};
render() {
const { allVotes } = this.state;
this.movies = this.props.list.map(x => {
const votes = allVotes[x.id];
return (
<Movie
movieId={x.id}
movie={x}
votes={votes}
reset={this.reset}
subtract={this.subtract}
add={this.add}
/>
);
});
return <div>{this.movies}</div>;
}
}
function MoviePoster(props) {
return <img src={props.poster} alt="Movie Poster" className="poster" />;
}
function Votes(props) {
return <h2>Votes: {props.count}</h2>;
}
var movieposters = [
{
id: 1,
img: "http://www.impawards.com/2017/posters/med_alien_covenant_ver4.jpg",
name: "Alien Covenant"
},
{
id: 2,
img: "http://www.impawards.com/2017/posters/med_atomic_blonde_ver4.jpg",
name: "Atomic Blonde"
},
{
id: 3,
img: "http://www.impawards.com/2017/posters/med_easy_living_ver3.jpg",
name: "Easy Living"
},
{
id: 4,
img:
"http://www.impawards.com/2017/posters/med_once_upon_a_time_in_venice_ver3.jpg",
name: "Once Upon a Time in Venice"
},
{
id: 5,
img: "http://www.impawards.com/2017/posters/med_scorched_earth.jpg",
name: "Scorched Earth"
},
{
id: 6,
img:
"http://www.impawards.com/2017/posters/med_underworld_blood_wars_ver9.jpg",
name: "Underworld: Blood Wars"
},
{
id: 7,
img: "http://www.impawards.com/2017/posters/med_void.jpg",
name: "The Void"
},
{
id: 8,
img:
"http://www.impawards.com/2017/posters/med_war_for_the_planet_of_the_apes.jpg",
name: "War for the Planet of the Apes"
}
];
ReactDOM.render(<Ratings list={movieposters} />, document.getElementById("root"));
<link href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet"/>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="root"></div>
You need to keep track of votes for each element, thus this.state.votes +/- 1 doesn't do the job, so:
Change
this.state = {
votes: 0
}
to
this.state = {
votes: {}
}
then change the functions:
add(id){
return function(event) {
this.setState ({ ...this.state.votes, [id]: parseInt(this.state.votes[id]) + 1 })
}
}
and the same for subtract. Then change your buttons to:
<button onClick={this.add(x.id)} ... (same for subtract)
and last change your Vote component:
<Votes count={this.state.votes[x.id] || 0} />
On reset just do:
reset(event){
this.setState ({ votes: {} })
}