React: How to update object in array map - javascript

I'm trying to update the value of an array when a button is clicked. But I can't figure out how to do so using this.setState.
export default class App extends React.Component {
state = {
counters: [{ name: "item1", value: 0 }, { name: "item2", value: 5 }]
};
render() {
return this.state.counters.map((counter, i) => {
return (
<div>
{counter.name}, {counter.value}
<button onClick={/* increment counter.value here */}>+</button>
</div>
);
});
}
}
How do I increment counter.value when the button is clicked?

Update
Since this is the accepted answer now, let me give another optimal method after #Auskennfuchs' comment. Here we use Object.assign and index to update the current counter. With this method, we are avoiding to use unnecessary map over counters.
class App extends React.Component {
state = {
counters: [{ name: "item1", value: 0 }, { name: "item2", value: 5 }]
};
increment = e => {
const {
target: {
dataset: { i }
}
} = e;
const { counters } = this.state;
const newCounters = Object.assign(counters, {
...counters,
[i]: { ...counters[i], value: counters[i].value + 1 }
});
this.setState({ counters: newCounters });
};
render() {
return this.state.counters.map((counter, i) => {
return (
<div>
{counter.name}, {counter.value}
{/* We are using function reference here */}
<button data-i={i} onClick={this.increment}>
+
</button>
</div>
);
});
}
}
ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<div id="root" />
As an alternative method, you can use counter name, map over the counters and increment the matched one.
class App extends React.Component {
state = {
counters: [{ name: "item1", value: 0 }, { name: "item2", value: 5 }]
};
increment = name => {
const newCounters = this.state.counters.map(counter => {
// Does not match, so return the counter without changing.
if (counter.name !== name) return counter;
// Else (means match) return a new counter but change only the value
return { ...counter, value: counter.value + 1};
});
this.setState({counters: newCounters});
};
render() {
return this.state.counters.map((counter, i) => {
return (
<div>
{counter.name}, {counter.value}
<button onClick={() => this.increment(counter.name)}>+</button>
</div>
);
});
}
}
ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<div id="root" />
If you use the handler like above it will be recreated on every render. You can either extract it to a separate component or use datasets.
class App extends React.Component {
state = {
counters: [{ name: "item1", value: 0 }, { name: "item2", value: 5 }]
};
increment = e => {
const {target} = e;
const newCounters = this.state.counters.map(counter => {
if (counter.name !== target.dataset.name) return counter;
return { ...counter, value: counter.value + 1};
});
this.setState({counters: newCounters});
};
render() {
return this.state.counters.map((counter, i) => {
return (
<div>
{counter.name}, {counter.value}
{ /* We are using function reference here */ }
<button data-name={counter.name} onClick={this.increment}>+</button>
</div>
);
});
}
}
ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<div id="root" />

Consider refactoring the actual counter into its own component. That simplifies the state management as it encapsulates the component responsibility. Suddenly you don't need to update a nested array of objects, but you update just a single state property:
class App extends React.Component {
state = {
counters: [{ name: "item1", value: 0 }, { name: "item2", value: 5 }]
};
render() {
return this.state.counters.map((counter, i) => {
return (
<Counter name={counter.name} count={counter.value} />
);
});
}
}
class Counter extends React.Component {
state = {
count: this.props.count
}
increment = () => {
this.setState({
count: this.state.count+1
})
}
render() {
return (
<div>
{this.props.name}, {this.state.count}
<button onClick={this.increment}>+</button>
</div>
);
}
}
ReactDOM.render( < App / > , document.getElementById("root"))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<div id="root"></div>

You could use the i that you already have to map the counters to a new array, replacing the target item with an updated count:
() => this.setState(({ counters }) => ({ counters: counters.map((prev, i2) => i === i2 ? { ...counter, count: counter.count + 1 } : prev) }))

You can have an handler that will map counters and update the corresponding counters item of the button clicked. Here we take i from the parent scope, and compare it to find the right item to change.
<button onClick={() => {
this.setState(state => ({
counters: state.counters.map((item, j) => {
// is this the counter that I want to update?
if (j === i) {
return {
...item,
value: item.value + 1
}
}
return item
})
}))
}}>+</button>

You can create a click handler like this
handleClick = (index) => {
this.setState(state => {
const obj = state.counters[index]; // assign the object at the index to a variable
obj.value++; // increment the value in the object
state.counters.splice(index, 1); // remove the object from the array
return { counters: [...state.counters, obj] };
});
}
Call it like this... <button onClick={() => handleClick(i)}>
It's possible to make this shorter. Just wanted to explain how you could go about it

With Array.splice you can replace an entry inside of an array. This will return a new array with the replaced value:
const {counters}= this.state
return counters.map((counter, i) => (
<div key={i}>
{counter.name}, {counter.value}
<button onClick={() => this.setState({
counters: counters.splice(i, 1, {
...counter,
value: counter.value+1,
}
)})}>+</button>
</div>
));
Also it's best practise to give every Fragment inside of a loop it's own unique key. And you can get rid of the return, because there's only the return inside of the map function.

Related

Implementation problems while using React's Promises

I'm working on an assignment where I need to implement an order management system. The problem is that I need to make a GET call to get all the orders, and for each order I need to make another GET call so I can get the Items of this order.
My question is how can I make those calls and create some data structure of orders and items before rendering everything.
I tried using async/await but I couldn't manage to create this data structure of orders and their related items before everything rendered.
For now I only have the orders GET call handled
async componentDidMount() {
const orders = await api.getOrders()
this.setState({
orders
});
}
I also created a function for the GET calls of the items which returns a Promise<Item[]>
createItemsList = (order: Order) => {
let a = order.items.map((item) => {
return api.getItem(item.id);
});
return Promise.all(a);
};
Any suggestion for a way to combine those two? Thanks in advance!
*** Editing ***
This is the part of the code where I render the orders
{filteredOrders.map((order) => (
<div className={'orderCard'}>
<div className={'generalData'}>
<h6>{order.id}</h6>
<h4>{order.customer.name}</h4>
<h5>Order Placed: {new Date(order.createdDate).toLocaleDateString()},
At: {new Date(order.createdDate).toLocaleTimeString()}</h5>
</div>
<Fulfilment order={order}/>
<div className={'paymentData'}>
<h4>{order.price.formattedTotalPrice}</h4>
<img src={App.getAssetByStatus(order.billingInfo.status)}/>
</div>
<ItemsList subItemsList={order.items} api={api}/>
</div>
))}
The component ItemsList is where I render the Items of a specific order, and order.items is not the items itself but an array of items ID and quantities which I get with each order
I suggest you move the data retrieval into each component.
Check the sandbox here
import React, { PureComponent } from "react";
const fakeOrderItems = {
1: [
{
id: 1,
name: "Ramen",
qty: 1
},
{
id: 1,
name: "Beer",
qty: 1
}
],
2: [
{
id: 1,
name: "Steak",
qty: 1
},
{
id: 2,
name: "Iced Tea",
qty: 1
}
]
};
const fakeOrders = [
{
id: 1,
name: "Table 1",
totalItems: 2
},
{
id: 2,
name: "Table 3",
totalItems: 2
}
];
const fakeApi = {
getOrders() {
return new Promise((resolve) =>
setTimeout(() => resolve(fakeOrders), 3000)
);
},
getOrderItems(id) {
return new Promise((resolve) =>
setTimeout(() => resolve(fakeOrderItems[id]), 3000)
);
}
};
class OrderItem extends PureComponent {
render() {
const { id, name, qty } = this.props;
return (
<div style={{ marginBottom: 10 }}>
<span>
{id}. {name} qty:{qty}
</span>
</div>
);
}
}
class OrderItemList extends PureComponent {
state = {
orderItems: []
};
componentDidMount() {
fakeApi
.getOrderItems(this.props.orderId)
.then((orderItems) => this.setState({ orderItems }));
}
render() {
const { orderItems } = this.state;
if (!orderItems.length) {
return <span>Loading orderItems...</span>;
}
return orderItems.map((item) => (
<OrderItem key={item.id + item.name} {...item} />
));
}
}
class Order extends PureComponent {
render() {
const { id, name } = this.props;
return (
<div style={{ marginBottom: 10 }}>
<div>
<span>Order #{id}</span>
</div>
<div>
<span>For table {name}</span>
</div>
<OrderItemList orderId={id} />
</div>
);
}
}
class OrderList extends PureComponent {
state = {
orders: []
};
componentDidMount() {
fakeApi.getOrders().then((orders) => this.setState({ orders }));
}
render() {
const { orders } = this.state;
if (!orders.length) {
return <div>Loading orders...</div>;
}
return orders.map((order) => <Order key={order.id} {...order} />);
}
}
export default function App() {
return <OrderList />;
}
Create a separate method outside that gets the data you need.
First get the orders from the API. Then loop through each order and call the api again for each item. Await the Promise.all to wait for each item in the order to finish, then concatenate the result to an array where you store all the fetched items. Then after the loop is finished return the results array.
In your componentDidMount call this method and update the state based on the result that the promised returned.
state = {
orders: []
}
async getOrderItems() {
let orderItems = [];
const orders = await api.getOrders()
for (const order of orders) {
const items = await Promise.all(
order.items.map((item) => api.getItem(item.id))
)
orderItems = [...orderItems, ...items]
}
return orderItems
}
componentDidMount() {
this.getOrderItems().then(orders => {
this.setState({
orders
})
})
}
render() {
if (this.state.orders.length === 0) {
return null
}
return (
{this.state.orders.map(order => (
// Render content.
)}
)
}
You can try this solution.I was stuck in the same issue a few days ago.So what it does is that setState renders after createItemsList function is run
async componentDidMount() {
const orders = await api.getOrders()
this.setState({
orders
}),
() => this.createItemsList()
}

Adding clicked items to new array in React

I am making API calls and rendering different components within an object. One of those is illustrated below:
class Bases extends Component {
constructor() {
super();
this.state = {
'basesObject': {}
}
}
componentDidMount() {
this.getBases();
}
getBases() {
fetch('http://localhost:4000/cupcakes/bases')
.then(results => results.json())
.then(results => this.setState({'basesObject': results}))
}
render() {
let {basesObject} = this.state;
let {bases} = basesObject;
console.log(bases);
//FALSY values: undefined, null, NaN, 0, false, ""
return (
<div>
{bases && bases.map(item =>
<button key={item.key} className="boxes">
{/* <p>{item.key}</p> */}
<p>{item.name}</p>
<p>${item.price}.00</p>
{/* <p>{item.ingredients}</p> */}
</button>
)}
</div>
)
}
}
The above renders a set of buttons. All my components look basically the same.
I render my components here:
class App extends Component {
state = {
ordersArray: []
}
render() {
return (
<div>
<h1>Bases</h1>
<Bases />
<h1>Frostings</h1>
<Frostings />
<h1>Toppings</h1>
<Toppings />
</div>
);
}
}
I need to figure out the simplest way to, when a button is clicked by the user, add the key of each clicked element to a new array and I am not sure where to start. The user must select one of each, but is allowed to select as many toppings as they want.
Try this
We can use the same component for all categories. All the data is handled by the parent (stateless component).
function Buttons({ list, handleClick }) {
return (
<div>
{list.map(({ key, name, price, isSelected }) => (
<button
className={isSelected ? "active" : ""}
key={key}
onClick={() => handleClick(key)}
>
<span>{name}</span>
<span>${price}</span>
</button>
))}
</div>
);
}
Fetch data in App component, pass the data and handleClick method into Buttons.
class App extends Component {
state = {
basesArray: [],
toppingsArray: []
};
componentDidMount() {
// Get bases and toppings list, and add isSelected attribute with default value false
this.setState({
basesArray: [
{ key: "bases1", name: "bases1", price: 1, isSelected: false },
{ key: "bases2", name: "bases2", price: 2, isSelected: false },
{ key: "bases3", name: "bases3", price: 3, isSelected: false }
],
toppingsArray: [
{ key: "topping1", name: "topping1", price: 1, isSelected: false },
{ key: "topping2", name: "topping2", price: 2, isSelected: false },
{ key: "topping3", name: "topping3", price: 3, isSelected: false }
]
});
}
// for single selected category
handleSingleSelected = type => key => {
this.setState(state => ({
[type]: state[type].map(item => ({
...item,
isSelected: item.key === key
}))
}));
};
// for multiple selected category
handleMultiSelected = type => key => {
this.setState(state => ({
[type]: state[type].map(item => {
if (item.key === key) {
return {
...item,
isSelected: !item.isSelected
};
}
return item;
})
}));
};
// get final selected item
handleSubmit = () => {
const { basesArray, toppingsArray } = this.state;
const selectedBases = basesArray.filter(({ isSelected }) => isSelected);
const selectedToppings = toppingsArray.filter(({ isSelected }) => isSelected);
// submit the result here
}
render() {
const { basesArray, toppingsArray } = this.state;
return (
<div>
<h1>Bases</h1>
<Buttons
list={basesArray}
handleClick={this.handleSingleSelected("basesArray")}
/>
<h1>Toppings</h1>
<Buttons
list={toppingsArray}
handleClick={this.handleMultiSelected("toppingsArray")}
/>
</div>
);
}
}
export default App;
CSS
button {
margin: 5px;
}
button.active {
background: lightblue;
}
I think the following example would be a good start for your case.
Define a handleClick function where you can set state with setState as the following:
handleClick(item) {
this.setState(prevState => {
return {
...prevState,
clickedItems: [...prevState.clickedItems, item.key]
};
});
}
Create an array called clickedItems in constructor for state and bind handleClick:
constructor() {
super();
this.state = {
basesObject: {},
clickedItems: [],
}
this.handleClick = this.handleClick.bind(this);
}
You need to add a onClick={() => handleClick(item)} handler for onClick:
<button key={item.key} className="boxes" onClick={() => handleClick(item)}>
{/* <p>{item.key}</p> */}
<p>{item.name}</p>
<p>${item.price}.00</p>
{/* <p>{item.ingredients}</p> */}
</button>
I hope that helps!

Why can't I add any value to the array in state?

I have a lot of hits, which I want to add to an array once a hit is pressed. However, as far as I observed, the array looked like it got the name of the hit, which is the value. The value was gone in like half second.
I have tried the methods like building constructor, and doing things like
onClick={e => this.handleSelect(e)}
value={hit.name}
onClick={this.handleSelect.bind(this)}
value={hit.name}
onClick={this.handleSelect.bind(this)}
defaultValue={hit.name}
and so on
export default class Tagsearch extends Component {
constructor(props) {
super(props);
this.state = {
dropDownOpen:false,
text:"",
tags:[]
};
this.handleRemoveItem = this.handleRemoveItem.bind(this);
this.handleSelect = this.handleSelect.bind(this);
this.handleTextChange = this.handleTextChange.bind(this);
}
handleSelect = (e) => {
this.setState(
{ tags:[...this.state.tags, e.target.value]
});
}
render() {
const HitComponent = ({ hit }) => {
return (
<div className="infos">
<button
className="d-inline-flex p-2"
onClick={e => this.handleSelect(e)}
value={hit.name}
>
<Highlight attribute="name" hit={hit} />
</button>
</div>
);
}
const MyHits = connectHits(({ hits }) => {
const hs = hits.map(hit => <HitComponent key={hit.objectID} hit={hit}/>);
return <div id="hits">{hs}</div>;
})
return (
<InstantSearch
appId="JZR96HCCHL"
apiKey="b6fb26478563473aa77c0930824eb913"
indexName="tags"
>
<CustomSearchBox />
{result}
</InstantSearch>
)
}
}
Basically, what I want is to pass the name of the hit component to handleSelect method once the corresponding button is pressed.
You can simply pass the hit.name value into the arrow function.
Full working code example (simple paste into codesandbox.io):
import React from "react";
import ReactDOM from "react-dom";
const HitComponent = ({ hit, handleSelect }) => {
return <button onClick={() => handleSelect(hit)}>{hit.name}</button>;
};
class Tagsearch extends React.Component {
constructor(props) {
super(props);
this.state = {
tags: []
};
}
handleSelect = value => {
this.setState(prevState => {
return { tags: [...prevState.tags, value] };
});
};
render() {
const hitList = this.props.hitList;
return hitList.map(hit => (
<HitComponent key={hit.id} hit={hit} handleSelect={this.handleSelect} />
));
}
}
function App() {
return (
<div className="App">
<Tagsearch
hitList={[
{ id: 1, name: "First" },
{ id: 2, name: "Second" },
{ id: 3, name: "Third" }
]}
/>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
additionally:
note the use of prevState! This is a best practice when modifying state. You can google as to why!
you should define the HitComponent component outside of the render method. it doesn't need to be redefined each time the component is rendered!

Asynchronously concatenating clicked numbers in a cleared string

The goal of my small React experiment is "clear the initial value of this.state.numString (outputs an empty string), then concatenate the clicked numbers into this.state.numString". To make it execute asynchronously, I took advantage of this.setState's callback where the concatenation of number strings happen.
class App extends Component {
state = {
numString: '12'
}
displayAndConcatNumber = (e) => {
const num = e.target.dataset.num;
this.setState({
numString: ''
}, () => {
this.setState({
numString: this.state.numString.concat(num)
})
})
}
render() {
const nums = Array(9).fill().map((item, index) => index + 1);
const styles = {padding: '1rem 0', fontFamily: 'sans-serif', fontSize: '1.5rem'};
return (
<div>
<div>
{nums.map((num, i) => (
<button key={i} data-num={num} onClick={this.displayAndConcatNumber}>{num}</button>
))}
</div>
<div style={styles}>{this.state.numString}</div>
</div>
);
}
}
The result was not what I expected; it only adds the current number I click into the empty string then change it into the one I click next, no concatenation of string numbers happens.
Here is one way of doing this. As I said in my comment you are resetting the string in every setState. So, you need some kind of condition to do that.
class App extends React.Component {
state = {
numString: '12',
resetted: false,
}
displayAndConcatNumber = (e) => {
const num = e.target.dataset.num;
if ( !this.state.resetted ) {
this.setState({
numString: '',
resetted: true,
}, () => {
this.setState( prevState => ({
numString: prevState.numString.concat(num)
}))
})
} else {
this.setState(prevState => ({
numString: prevState.numString.concat(num)
}))
}
}
render() {
const nums = Array(9).fill().map((item, index) => index + 1);
const styles = { padding: '1rem 0', fontFamily: 'sans-serif', fontSize: '1.5rem' };
return (
<div>
<div>
{nums.map((num, i) => (
<button key={i} data-num={num} onClick={this.displayAndConcatNumber}>{num}</button>
))}
</div>
<div style={styles}>{this.state.numString}</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>
You can do something like below to clear the state immediately and concatenate the state with previous state value
this.setState({
numString: ''
}, () => {
this.setState( prevState => ({
numString: prevState.numString + num
}));
});
The code above in your question , in first setState you are setting variable to empty and in the second setState it is concatenating new value with empty string state. Thats why it is not working.
Try something like below:
class App extends Component {
state = {
numString: '12',
isFirstTime: true
}
displayAndConcatNumber = (e) => {
const num = e.target.dataset.num;
if(this.state.isFirstTime){
this.setState({
numString: '',
isFirstTime: false
}, () => {
this.setState({
numString: this.state.numString.concat(num)
})
})
}else{
this.setState({
numString: this.state.numString.concat(num)
})
}
}
render() {
const nums = Array(9).fill().map((item, index) => index + 1);
const styles = {padding: '1rem 0', fontFamily: 'sans-serif', fontSize: '1.5rem'};
return (
<div>
<div>
{nums.map((num, i) => (
<button key={i} data- num={num} onClick={this.displayAndConcatNumber}>{num}</button>
))}
</div>
<div style={styles}>{this.state.numString}</div>
</div>
);
}
}

Counter app doesn't work on React

I created 2 buttons and wanna them increase their value clicking on each button.
The code below doesn't work. Please, help ! All this code I render in html tag with class buttons.
class Buttons extends React.Component {
state = { score_1: 0, score_2: 0 };
updateScore_1() {
this.setState((prevState) => {
score_1: prevState.score_1 += 1
});
}
updateScore_2() {
this.setState((prevState) => {
score_2: prevState.score_2 += 1
});
}
render() {
return (
<div>
<button onClick={this.updateScore_1.bind(this)}>{this.state.score_1}</button>
<br />
<button onClick={this.updateScore_2.bind(this)}>{this.state.score_2}</button>
</div>
)
}
}
ReactDOM.render(
<Buttons />,
document.querySelector('.buttons')
);
You missed the keyword return in this.setState. Just add return in this.setState:
updateScore_2() {
this.setState((prevState) => {
return { score_2: prevState.score_2 + 1 }
});
}
You should write score_1: prevState.score_1 + 1 instead of score_1: prevState.score_1 += 1. In your case you are mutating the state, however using the setState callback you need to return the updated state
this.setState((prevState) => ({ // notice the `({` here
[key]: prevState[key] +1
}));
Also you can simply use one handler instead of two
class Buttons extends React.Component {
state = { score_1: 0, score_2: 0 };
updateScore(key) {
this.setState((prevState) => ({
[key]: prevState[key] +1
}));
}
render() {
return (
<div>
<button onClick={this.updateScore.bind(this, 'score_1')}>{this.state.score_1}</button>
<br />
<button onClick={this.updateScore.bind(this, 'score_2')}>{this.state.score_2}</button>
</div>
)
}
}
ReactDOM.render(
<Buttons />,
document.querySelector('.buttons')
);
<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 class="buttons"/>
Your issue I guess was from the fact that in the setState methods your were not returning the new state which should have been passed to the setState method
updateScore_1() {
this.setState((prevState) => {
score_1: prevState.score_1 + 1
});
}
A couple of parentheses solves your issue or you can just put a return in from of the new state object. See complete solution below:
class Buttons extends React.Component {
constructor(props) {
super(props);
this.state = { score_1: 0, score_2: 0 };
this.updateScore_1 = this.updateScore_1.bind(this);
this.updateScore_2 = this.updateScore_2.bind(this);
}
updateScore_1() {
this.setState(prevState => ({
score_1: (prevState.score_1 += 1)
}));
}
updateScore_2() {
this.setState(prevState => ({
score_2: (prevState.score_2 += 1)
}));
}
render() {
return (
<div>
<button onClick={this.updateScore_1}>{this.state.score_1}</button>
<br />
<button onClick={this.updateScore_2}>{this.state.score_2}</button>
</div>
);
}
}
ReactDOM.render(
<Buttons />,
document.querySelector('.buttons')
);

Categories

Resources