React how to handle add to Cart - javascript

So I have an e-commerce webpage but for some reason, after adding an item past the first time to a cart it starts to double the value of units in cart as well as double my Toasts. I am wondering what I am doing wrong here. My initial State is 0 for cartItem. Any help will be much appreciated.
here is what I am working with:
Example of cartItem Object:
[{
description: "...."
featured: false
id: "6u7pLcaGApuGtiNAf6zLMf"
image: ["//images.ctfassets.net/f1r553pes4gs/17rq7BQ76Q7ouq…010636019e9b3cbc3a10/il_794xN.2378509691_kaep.jpg"]
inCart: false
ingredients: (2) ["Distilled Water", "99.99% Fine Silver Rods"]
price: 20
productName: "Quintessence Colloidal Silver (4 fl oz)"
slug: "quintessence-colloidal-silver-4oz"
units: 9
}]
Shorted Version of Code:
export class StoreProvider extends Component {
//Initialized State ready for API Data
state = {
products: [],
featuredProducts: [],
sortedProducts: [],
price: 0,
maxPrice: 0,
minPrice: 0,
units: 0,
loading: true,
//FIXME CART
cartItem: []
}
handleAddToCart = (e, products) => {
this.setState(state => {
const cartItem = state.cartItem;
let productsAlreadyInCart = false;
cartItem.forEach(cp => {
if (cp.id === products.id) {
//Have tried ++
cp.units+= 1;
productsAlreadyInCart = true;
this.successfullCartToast()
}
});
if (!productsAlreadyInCart) {
cartItem.push({ ...products});
}
localStorage.setItem('cartItem', JSON.stringify(cartItem));
return { cartItem: cartItem };
});
}
}
//Button is in seperate component
<button
className="btn-primary rounded col-sm-6 col-lg-12 align-self-center ml-1 p-2"
onClick={(e) => handleAddToCart(e, product)}>
+ Cart
</button>

Issue
You are mutating existing state and toasting every cart item you check.
Solution
First search the cart array if item is already contained. If it is already contained then simply map the cart and update the appropriate index, otherwise, append to a shallowly copied cart item array.
Also, setState should be a pure function, so don't do side-effects like setting localStorage inside the setState functional update, instead use the setState callback, or preferably, the componentDidUpdate lifecycle function. Or you can just set localStorage with the same value you're updating state with.
handleAddToCart = (e, products) => {
const itemFoundIndex = this.state.cartItem.findIndex(
cp => cp.id === products.id
);
let cartItem;
if (itemFoundIndex !== -1) {
this.successfullCartToast();
// map cart item array and update item at found index
cartItem = this.state.cartItem.map((item, i) =>
i === itemFoundIndex ? { ...item, units: item.units + 1 } : item
);
} else {
// shallow copy into new array, append new item
cartItem = [...this.state.cartItem, products];
}
localStorage.setItem("cartItem", JSON.stringify(cartItem));
this.setState({ cartItem });
};

So, cartItem is an Object, looking something like:
[{'id': 123, 'units': 2}, {'id': 345, 'units': 1}, ...] ?
One thing to try would be to make a deep copy of the Object, so it doesn't changes while updating:
handleAddToCart = (e, products) => {
this.setState(state => {
//const cartItem = state.cartItem;
// make deep copy, so state doesn't change while manipulating:
const cartItem = JSON.parse(JSON.stringify( state.cartItem ));
let productsAlreadyInCart = false;
cartItem.forEach(cp => {
if (cp.id === products.id) {
//Have tried ++
cp.units += 1;
productsAlreadyInCart = true;
this.successfullCartToast()
}
});
if (!productsAlreadyInCart) {
cartItem.push({ ...products});
}
localStorage.setItem('cartItem', JSON.stringify(cartItem));
return { cartItem: cartItem };
});
}
}

Related

Removing an object from array based on properties value

I want to remove an object from my List array based on its properties value.
Right now I am using findIndex to see if there is an index ID matching my event.target.id.
This is an example of one of the objects in my list array:
{artist: "artist name",
genre: "RnB",
id: 1,
rating: 0,
title: "song name"}
This is my code:
console.log(e.target.id);
const list = this.state.playlist;
list.splice(
list.findIndex(function(i) {
return i.id === e.target.id;
}),
1
);
console.log(list);
}
how ever, instead of it removing the clicked item from the array, it removes the last item, always.
When I do this:
const foundIndex = list.findIndex((i) => i.id === e.target.id)
console.log(foundIndex)
I get -1 back.
What's the problem here?
Use filter for this. Use it to filter out the objects from state where the id of the button doesn't match the id of the current object you're iterating over. filter will create a new array you can then update your state with rather than mutating the existing state (which is bad) which is what is currently happening.
Assuming you're using React here's a working example.
const { Component } = React;
class Example extends Component {
constructor() {
super();
this.state = {
playlist: [
{id: 1, artist: 'Billy Joel'},
{id: 2, artist: 'Madonna'},
{id: 3, artist: 'Miley Cyrus'},
{id: 4, artist: 'Genesis'},
{id: 5, artist: 'Jethro Tull'}
]
};
}
// Get the id from the button (which will be
// a string so coerce it to a number),
// and if it matches the object id
// don't filter it into the new array
// And then update the state with the new filtered array
removeItem = (e) => {
const { playlist } = this.state;
const { id } = e.target.dataset;
const updated = playlist.filter(obj => {
return obj.id !== Number(id);
});
this.setState({ playlist: updated });
}
render() {
const { playlist } = this.state;
return (
<div>
{playlist.map(obj => {
return (
<div>{obj.artist}
<button
data-id={obj.id}
onClick={this.removeItem}
>Remove
</button>
</div>
);
})}
</div>
);
}
};
ReactDOM.render(
<Example />,
document.getElementById('react')
);
<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>
<div id="react"></div>

Mapping through state: (TypeError): this.state.[something].map is not a function

I'm trying to map through my state, where I have collected data from an external API.
However, when I do i get this error here:
TypeError: this.state.stocks.map is not a function
I want to render the results to the frontend, through a function so that the site is dynamic to the state.favorites.
Though the console.log(), I can see that the data is stored in the state.
I have found others with a similar issue, but the answers did not work out.
UPDATE:
I have changed the componentDidMount() and it now produces an array. The issue is that I get no render from the renderTableData() functions.
console.log shows this array:
0: {symbol: "ARVL", companyName: "Arrival", primaryExchange: "AESMSBCDA)LNKOS/ TLTE(N GAEQLGAR ", calculationPrice: "tops", open: 0, …}
1: {symbol: "TSLA", companyName: "Tesla Inc", primaryExchange: " RNK EAASGTDACLN)LE/OGMELAQSTB (S", calculationPrice: "tops", open: 0, …}
2: {symbol: "AAPL", companyName: "Apple Inc", primaryExchange: "AMTGS/C) AALGDRSTNLEOEL(S BAE NQK", calculationPrice: "tops", open: 0, …}
length: 3
__proto__: Array(0)
This is my code:
import React, { Component } from 'react'
import './Table.css';
class Table extends Component {
constructor (props) {
super(props);
this.state = {
favorites: ['aapl', 'arvl', 'tsla'],
stocks: []
};
}
componentDidMount() {
this.state.favorites.map((favorites, index) => {
fetch(`API`)
.then((response) => response.json())
.then(stockList => {
const stocksState = this.state.stocks;
const stockListValObj = stockList;
console.log(stocksState)
console.log(stockListValObj)
this.setState({
stocks: [
... stocksState.concat(stockListValObj)
]
}, () => { console.log(this.state.stocks);});
})
})
}
renderTableData() {
this.state.stocks.map((stocks, index) => {
const { companyName, symbol, latestPrice, changePercent, marketCap } = stocks //destructuring
return (
<div key={symbol} className='headers'>
<div className='first-value'>
<h4>{companyName}</h4>
<h4 className='symbol'>{symbol}</h4>
</div>
<div className='align-right'>
<h4>{latestPrice}</h4>
</div>
<div className='align-right'>
<h4 className='changePercent'>{changePercent}</h4>
</div>
<div className='align-right'>
<h4>{marketCap}</h4>
</div>
</div>
);
})
}
render() {
return (
<div className='table'>
<h1 id='title'>Companies</h1>
<div className='headers'>
<h4 className='align-right'></h4>
<h4 className='align-right'>Price</h4>
<h4 className='align-right'>+/-</h4>
<h4 className='align-right'>Market Cap</h4>
</div>
<div>
{this.renderTableData()}
</div>
</div>
)
}
}
export default Table;
You should always aim in making single state update, try reducing the state update to single update.
I suggest 2 solution:
Move the data fetch section into a separate function, update a temporary array variable return the variable at the end of the execution.
async dataFetch() {
const sampleData = this.state.stocks || [];
await this.state.favorites.forEach((favorites, index) => {
fetch(`API`)
.then((response) => response.json())
.then((stockList) => {
sampleData.push(stockList);
// const stocksState = this.state.stocks;
// const stockListValObj = stockList;
// console.log(stocksState);
// console.log(stockListValObj);
// this.setState({
// stocks: [
// ... stocksState.concat(stockListValObj)
// ]
// }, () => { console.log(this.state.stocks);});
});
});
return Promise.resolve(sampleData);
}
componentDidMount() {
this.dataFetch().then((stockValues) => {
this.setState({ stocks: stockValues });
});
}
Use Promise.all() this return a single array value which would be easier to update on to the stock state.
Suggestion : When not returning any values from the array try using Array.forEach instead of Array.map
Return keyword is missing in renderTableData
renderTableData() {
this.state.stocks.map((stocks, index) => { ...});
to
renderTableData() {
return this.state.stocks.map((stocks, index) => { ...});
I would say that this is happening because on the first render, the map looks into state.stock and it's an empty array. After the first render, the componentDidMount method is called fetching the data.
I would suggest to just wrap the map into a condition. If stock doesn't have any object, then return whatever you wish (null or a loader/spinner for example).
It's enough to add it like this for not returning anything in case the array is empty (it will be filled after the first render, but this is useful as well to return error message in case the fetch fails):
this.state.stocks.length > 0 && this.state.stocks.map((stocks, index) => {

How to update list of elements in React

I'm having trouble updating a list of elements using React, when I run the code below and click on a 'star' element, react updates ALL the elements in this.state.stars instead of just the element at the given index:
class Ratings extends React.Component {
constructor(props) {
super(props);
let starArr = new Array(parseInt(this.props.numStars, 10)).fill({
icon: "*",
selected: false
});
this.state = {
stars: starArr
};
this.selectStar = this.selectStar.bind(this);
}
selectStar(ind) {
this.setState({
stars: this.state.stars.map((star, index) => {
if (index === ind) star.selected = !star.selected;
return star;
})
});
}
makeStars() {
return this.state.stars.map((star, ind) => (
<span
className={star.selected ? "star selected" : "star"}
onClick={() => this.selectStar(ind)}
key={ind}
>
{star.icon}
</span>
));
}
render() {
return (
<div className="star-container">
<span>{this.makeStars()}</span>
</div>
);
}
}
Can anyone point me in the right direction here? Not sure why this is happening!
Your problem is in how you're instantiating your Array:
let starArr = new Array(parseInt(this.props.numStars, 10)).fill({
icon: "*",
selected: false
});
What that line is doing is filling each item in the array with the same object. Not objects all with the same values, but a reference to the same object. Then, since you're mutating that object in your click handler, each item in the Array changes because they're all a reference to that same object.
It's better to do a non-mutating update, like this:
this.setState({
stars: this.state.stars.map((star, index) =>
(index === ind)
? { ...star, selected: !star.selected }
: star
)
});
This will instead create a copy of the object at the Array index except with the selected property toggled.

Push Unique Object in React.js

I'm building a terribly flawed e-commerce application using React just for fun and I'm trying to figure out how to set state on a certain object once it's pushed into an array.
I have a cart array where I push the items that I added from my initial items array which holds all my products.
The iffy thing is that I have stock on products. So let's say my chocolate product has 5 in stock and every time I push that the chocolate object, it piles on and adds the same item in the cart as so:
I want to be pushing the chocolates object to the cart array but I don't want to render a duplicate if it's already there. Instead I want to achieve something where the chocolate object is added but the quantity of it is changed accordingly every time it's added. It would look something like this:
How can I achieve something like this? Maybe a check to see if that object is already added to the cart array and if it is then instead of rendering a duplicate, just push the values and update a quantity of that item?
Been stuck for hours and would greatly appreciate some hints.
class App extends Component {
state = {
cart: [],
items: [
{ id: uuid(), name: 'chocolate', price: 10, remaining: 5 },
{ id: uuid(), name: 'strawberry', price: 50, remaining: 10 },
{ id: uuid(), name: 'banana', price: 43, remaining: 20 }
],
total: 0,
addToCartMessage: false,
removeFromCartMessage: false,
searchTerm: ''
}
addItem = item => {
const { cart, items, total } = this.state
cart.push({ id: item.id, name: item.name, price: item.price })
const remaining = item.remaining--
const totalPrice = cart.reduce((a, b) => a + b.price, 0)
this.setState({
total: totalPrice,
cart,
addToCartMessage: true,
...items, remaining
})
}
removeItem = cartItems => {
const { items, cart, total } = this.state
const removeItem = cart.filter(item => item.id !== cartItems.id)
const itemId = items.find(item => item.name === cartItems.name).remaining++
this.setState({
removeFromCartMessage: true,
total: total - cartItems.price,
cart: removeItem,
...items, remaining: itemId
})
}
render() {
const { cart, items, total, addToCartMessage, removeFromCartMessage } =
this.state
if (addToCartMessage || removeFromCartMessage) {
setTimeout(() => {
this.setState({
addToCartMessage: false,
removeFromCartMessage: false
})
}, 1000)
}
const filteredItems = items.filter(item =>
item.name.includes(this.state.searchTerm))
return (
<div className="App">
{cart.length === 0 ? <h3>No items in cart</h3> : (
<div>
<h1>Cart:</h1>
{cart.map(items => (
<div key={items.id}>
<h1>{items.name} x 3</h1>
<p>${items.price}</p>
<button onClick={() => this.removeItem(items)}>Remove From Cart</button>
</div>
))}
</div>
)}
<hr />
<input
type="text"
placeholder="Search for an item..."
onChange={e => this.setState({ searchTerm: e.target.value })}
value={this.state.searchTerm}
/>
{filteredItems.map(item => (
<div key={item.id}>
<h1>{item.name}</h1>
<p>Price: ${item.price}</p>
{item.remaining === 0 ? <p>Sold Out</p> : (
<div>
<p>Remaining: {item.remaining}</p>
<button onClick={() => this.addItem(item)}>Add To Cart</button>
</div>
)}
</div>
))}
{ total !== 0 ? <h1>Total ${total}</h1> : <h1>Total $0</h1> }
{ addToCartMessage && <h1>Item successfully added!</h1> }
{ removeFromCartMessage && <h1>Item successfully removed!</h1> }
</div>
)
}
}
export default App
Store your products in a regular object by id.
1: {
id: 1,
name: 'chocolate'
}
Store your cart as an array of IDs.
[1, 1, 1]
In your component, group IDs cart array by ID to get the count, and look up cart object by ID to get its data.
Computed data should be computed, not stored.
Here's some completely untested, unlinted, code showing the calculations done in the render function:
class App extends Component {
state = {
cart: [],
items: [
{ id: uuid(), name: 'chocolate', price: 10, available: 5 },
{ id: uuid(), name: 'strawberry', price: 50, available: 10 },
{ id: uuid(), name: 'banana', price: 43, available: 20 }
// Convert to an object of { id: { id, name, price } }
].reduce((memo, item) => ({
...memo,
[item.id]: item
}), {}),
}
addItem = id => {
const { cart, } = this.state
this.setState({
cart: [ ...cart, id ]
})
}
removeItem = removeId => {
const { cart, } = this.state
this.setState({
cart: cart.filter(({ id }) => id !== removeId)
})
}
render() {
const { cart, items, total, addToCartMessage, removeFromCartMessage } = this.state
// Given an array of item IDs in our cart, group them into an object
// with the total count and price for each item, and overall count
const accumulatedItems = items.reduce((memo, item) => {
const { id, price } = item;
const { count, price, } = memo[id] || {};
return {
...memo,
cartTotal: memo.cartTotal + price,
[id]: {
count: (count || 0) + 1,
total: (price || 0) + price,
}
};
// Starting object to reduce
}, {
cartTotal: 0,
});
return (
<div className="App">
{cart.length === 0 ? <h3>No items in cart</h3> : (
<div>
<h1>Cart:</h1>
{Object.keys(accumulatedItems).sort().map(id => (
<div key={id}>
<h1>{items[id].name} x {accumulatedItems[id].total}</h1>
<p>${accumulatedItems[id].total}</p>
<button onClick={() => this.removeItem(id)}>Remove From Cart</button>
</div>
))}
</div>
)}
</div>
);
}
}
Juggling computed data, and mutating state like remaining, adds significant logic complexity to your app. You should only start worrying about trying to store/cache/memoize computed state once there are performance issues. Calculating totals in the render function is a fine first pass.
In React in the wild, there are solutions like reselect, which don't require redux technically. They're just caching functions that take in a given state, and produce a computed output, and only re-calculate the output if the input changes.
Maybe you can do it how you think and explain in your question. There are multiple ways of doing this and everyone do it how they like it or how easy they do and maintain it.
Instead of inserting the item itself maybe you can hold an object for each item in your array with item's unique id. That object also could hold quantity. Then you can generate card info via this unique id.
Example:
cart: [
{ id: uniqeId, quantiy: 1 },
{ id: uniqeId, quantiy: 6 },
]
After adding an item to card, you can go and just alter the related object's quantity. But for this you have to find the item in this array, then alter the quantity as you guess.
You can have item ids in your cart object (not array this time) as an array but this time you separate quantity and hold it as an object by item ids. So, after adding the item to cart's id array list, you also go and alter quantity of item's object. With this method you don't have to struggle finding anything but you need to alter two piece of information.
Like:
cart: {
ids: [ uniqueId, uniqueId ],
quantity: { uniqueId: 1, uniqueId: 6 }
}
Or you can do how you describe, just add the item but before doing this check if the item is already there. For example filtering by id. But, with this technique there might be some glitches. When you add items like this, for example with price, remaining etc, you also have to maintain your cart state with your item state. For instance what will happen when you want to change an item's price? Or what if there is another way (somehow) altering the remaining other then adding items into cart? But, if you play only with id's you can extract those information from your single state: items.
But I'm also a learner, maybe there are way better methods apart from those. I haven't written a cart yet, but if I did it I would use the second method maybe.
By the way, do not use push to alter your state. This mutates your state and it is not advisable. Use something like concat or spread operator to create a new state.
item = { foo: bar, fizz: buzz }
// now setting state
cart: [ ...this.state.cart, item ]
And try to use a callback function in setState (since it is an async operation) if your state change depends on the previous state.
this.setState( prevState => ( {
cart: [ ...prevState.cart, item ],
} ) )
Using #devserkan suggestion of restructuring the cart state and #Andy Ray's suggestion of restructuring the items state, I set up my state to look like this:
state = {
items: {
1: {
id: 1, name: 'chocolate', price: 10, available: 5
},
2: {
id: 2, name: 'strawberry', price: 10, available: 10
},
3: {
id: 3, name: 'banana', price: 10, available: 20
}
}
cart: {
ids: [],
quantity: { 1: 0, 2: 0, 3: 0 }
}
}
I then went and rendered out the items and added an onClick function (addItem) to handle some setState calls:
render() {
const { cart, items } = this.state
return (
<div>
<h1>Shopping Area</h1>
{Object.values(items).map(item => (
<div key={item.id}>
<h2>{item.name}</h2>
<h2>{item.price}</h2>
<button onClick={() => this.addItem(item)}>Add To Cart</button>
</div>
))}
In my addItem function, I went ahead and set the state of cart so that I push the item id, and update the quantity on that id as well:
addItem = item => {
const { cart, items } = this.state
this.setState({
cart: {
...cart,
// Push item id to ids array inside cart state
ids: [...cart.ids, item.id],
quantity: {
...cart.quantity,
// Update quantity of the specific id pushed by 1
[item.id]: cart.quantity[item.id] + 1
}
}
})
}
Finally I had to render the cart section: I did so by checking to see if the cart.ids array wasn't empty and made another check to only render the item that has a quantity greater than 0. If we didn't make that check, every time we push an item, it will add all 3 at once and we only want that specific item to show.
{cart.ids.length !== 0 ? Object.keys(items).map(id => (
<div key={id}>
// Check to see if quantity for that item is > 0
{cart.quantity[id] > 0 && (
<h1>{items[id].name} x {cart.quantity[id]}</h1>
)}
</div>
)) : <h1>No Items In Your Cart</h1>}
Full Code (Without Price / Remaining)
export default class App extends Component {
state = {
cart: {
ids: [],
quantity: {
1: 0,
2: 0,
3: 0
}
},
items: {
1: {
id: 1, name: 'chocolate', price: 10, available: 5
},
2: {
id: 2, name: 'strawberry', price: 10, available: 10
},
3: {
id: 3, name: 'banana', price: 10, available: 20
}
}
}
addItem = item => {
const { cart, items } = this.state
this.setState({
cart: {
...cart,
ids: [...cart.ids, item.id],
quantity: {
...cart.quantity,
[item.id]: cart.quantity[item.id] + 1
}
}
})
}
removeItem = removeId => {
const { cart } = this.state
this.setState({
cart: cart.filter(({ id }) => id !== removeId)
})
}
render() {
const { cart, items, total, addToCartMessage, removeFromCartMessage } =
this.state
return (
<div className="App">
<h1>Shopping Area</h1>
{Object.values(items).map(item => (
<div key={item.id}>
<h2>{item.name}</h2>
<h2>{item.price}</h2>
<button onClick={() => this.addItem(item)}>Add To Cart</button>
</div>
))}
<hr style={{'marginTop': '200px'}} />
<h1>Cart</h1>
{cart.ids.length !== 0 ? Object.keys(items).map(id => (
<div key={id}>
{cart.quantity[id] > 0 && (
<h1>{items[id].name} x {cart.quantity[id]}</h1>
)}
</div>
)) : <h1>No Items In Your Cart</h1>}
</div>
)
}
}
Big thanks to #Andy Ray and #devserkan for the suggestions.

Reorder array of objects

In my React state, I want to reorder an array of 3 objects by always putting the selected one in the middle while keeping the others in ascending order.
Right now, I'm using an order property in each object to keep track of the order, but this might not be the best approach.
For example :
this.state = {
selected: 'item1',
items: [
{
id: 'item1',
order: 2
},
{
id: 'item2'
order: 1
},
{
id: 'item3'
order: 3
}
]
}
Resulting array : [item2, item1, item3]
Now, let's imagine that a user selects item2. I will update the selected state property accordingly, but how can I update the items property to end up with a result like this:
this.state = {
selected: 'item2',
items: [
{
id: 'item1',
order: 1
},
{
id: 'item2'
order: 2
},
{
id: 'item3'
order: 3
}
]
}
Resulting array : [item1, item2, item3]
How would you do it? I have seen some lodash utility functions that could help but I would like to accomplish this in vanilla JavaScript.
You could do something crude like this:
// Create a local shallow copy of the state
var items = this.state.items.slice()
// Find the index of the selected item within the current items array.
var selectedItemName = this.state.selected;
function isSelectedItem(element, index, array) {
return element.id === selectedItemName;
};
var selectedIdx = items.findIndex(isSelectedItem);
// Extract that item
var selectedItem = items[selectedIdx];
// Delete the item from the items array
items.splice(selectedIdx, 1);
// Sort the items that are left over
items.sort(function(a, b) {
return a.id < b.id ? -1 : 1;
});
// Insert the selected item back into the array
items.splice(1, 0, selectedItem);
// Set the state to the new array
this.setState({items: items});
This assumes the size of the items array is always 3!
I'm gonna be lazy and just outline the steps you need to take.
Pop the selected item out of the starting array
Push the first item of the starting array into a new array
Push the selected item into the new array
Push the last item of the starting array into the new array
Set your state to use the new array
You can do something like:
NOTE: This works assuming there would three items in the array. However, if there are more we just need to specify the index position in the insert function.
this.state = {
selected: 'item1',
items: [
{
id: 'item1',
order: 1
},
{
id: 'item2',
order: 2
},
{
id: 'item3',
order: 3
}
]
};
// To avoid mutation.
const insert = (list, index, newListItem) => [
...list.slice(0, index), // part of array before index arg
newListItem,
...list.slice(index) // part of array after index arg
];
// Get selected item object.
const selectedValue = value => this.state.items.reduce((res, val) => {
if (val.id === selectedValue) {
res = val;
}
return res;
}, {});
const filtered = this.state.items.filter(i => i.id !== state.selected);
const result = insert(filtered, 1, selectedValue(this.state.selected));
We can get rid of the extra reduce if instead of storing id against selected you store either the index of the item or the whole object.
Of course we need to use this.setState({ items: result }). This solution would also ensure we are not mutating the original state array at any point.
I put together a fully working example what can be extended on so you can experiment with different ways to achieve your intended use-case.
In this case I created a button component and rendered three of them to provide a means of changing the selected state.
Important things to remember, always use the setState() function for updating React Class state. Also, always work on state arrays and objects with a cloned variable as you'll want to update the whole object/array at once. Don't modify attributes of pointer variables pointing to state objects or arrays.
It is very possible to add bugs to your code by referencing state objects/arrays and then changing their properties (accidentally or not) by modifying the pointer referencing the object. You will lose all guarantees on how the state will update, and comparing prevState or nextState with this.state may not work as intended.
/**
* #desc Sub-component that renders a button
* #returns {HTML} Button
*/
class ChangeStateButton extends React.Component {
constructor(props) {
super(props);
this.handleClick = this.handleClick.bind(this);
this.state = ({
//any needed state here
});
}
handleClick(e) {
//calls parent method with the clicked button element and click state
this.props.click(e.nativeEvent.toElement.id);
}
render() {
return (
<button
id = {this.props.id}
name = {this.props.name}
className = {this.props.className}
onClick = {this.handleClick} >
Reorder to {this.props.id}!
</button>
);
}
}
/**
* #desc Creates button components to control items order in state
* #returns {HTML} Bound buttons
*/
class ReorderArrayExample extends React.Component {
constructor(props) {
super(props);
this.reorderItems = this.reorderItems.bind(this);
this.state = ({
selected: 'item1',
//added to give option of where selected will insert
selectedIndexChoice: 1,
items: [
{
id: 'item1',
order: 2
},
{
id: 'item2',
order: 1
},
{
id: 'item3',
order: 3
}
]
});
}
reorderItems(selected) {
const {items, selectedIndexChoice} = this.state,
selectedObjectIndex = items.findIndex(el => el.id === selected);
let orderedItems = items.filter(el => el.id !== selected);
//You could make a faster reorder algo. This shows a working method.
orderedItems.sort((a,b) => { return a.order - b.order })
.splice(selectedIndexChoice, 0, items[selectedObjectIndex]);
//always update state with setState function.
this.setState({ selected, items: orderedItems });
//logging results to show that this is working
console.log('selected: ', selected);
console.log('Ordered Items: ', JSON.stringify(orderedItems));
}
render() {
//buttons added to show functionality
return (
<div>
<ChangeStateButton
id='item1'
name='state-button-1'
className='state-button'
click={this.reorderItems} />
<ChangeStateButton
id='item2'
name='state-button-2'
className='state-button'
click={this.reorderItems} />
<ChangeStateButton
id='item3'
name='state-button-2'
className='state-button'
click={this.reorderItems} />
</div>
);
}
}
/**
* #desc React Class renders full page. Would have more components in a real app.
* #returns {HTML} full app
*/
class App extends React.Component {
render() {
return (
<div className='pg'>
<ReorderArrayExample />
</div>
);
}
}
/**
* Render App to DOM
*/
/**
* #desc ReactDOM renders app to HTML root node
* #returns {DOM} full page
*/
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">
<!-- This div's content will be managed by React. -->
</div>

Categories