I am trying to use react-select in combination with match-sorter as described in this stackoverflow answer (their working version). I have an initial array of objects that get mapped to an array of objects with the value and label properties required by react-select, which is stored in state. That array is passed directly to react-select, and when you first click the search box everything looks good, all the options are there. The onInputChange prop is given a call to matchSorter, which in turn is given the array, the new input value, and the key the objects should be sorted on. In my project, and reproduced in the sandbox, as soon as you type anything into the input field, all the options disappear and are replaced by the no options message. If you click out of the box and back into it, the sorted options show up the way they should. See my sandbox for the issue, and here's the sandbox code:
import "./styles.css";
import { matchSorter } from "match-sorter";
import { useState } from "react";
import Select from "react-select";
const objs = [
{ name: "hello", id: 1 },
{ name: "world", id: 2 },
{ name: "stack", id: 3 },
{ name: "other", id: 4 },
{ name: "name", id: 5 }
];
const myMapper = (obj) => {
return {
value: obj.id,
label: <div>{obj.name}</div>,
name: obj.name
};
};
export default function App() {
const [options, setOptions] = useState(objs.map((obj) => myMapper(obj)));
return (
<Select
options={options}
onInputChange={(val) => {
setOptions(matchSorter(options, val, { keys: ["name", "value"] }));
}}
/>
);
}
I am sure that the array in state is not getting removed or anything, I've console logged each step of the way and the array is definitely getting properly sorted by match-sorter. It's just that as soon as you type anything, react-select stops rendering any options until you click out and back in again. Does it have something to do with using JSX as the label value? I'm doing that in my project in order to display an image along with the options.
I had to do two things to make your code work:
Replaced label: <div>{obj.name}</div> with label: obj.name in your mapper function.
I am not sure if react-select allows html nodes as labels. Their documentation just defines it as type OptionType = { [string]: any } which is way too generic for anything.
The list supplied to matchSorter for matching must be the full list (with all options). You were supplying the filtered list of previous match (from component's state).
const objs = [
{ name: "hello", id: 1 },
{ name: "world", id: 2 },
{ name: "stack", id: 3 },
{ name: "other", id: 4 },
{ name: "name", id: 5 }
];
const myMapper = (obj) => {
return {
value: obj.id,
label: obj.name, // -------------------- (1)
name: obj.name
};
};
const allOptions = objs.map((obj) => myMapper(obj));
export default function App() {
const [options, setOptions] = useState(allOptions);
return (
<Select
options={options}
onInputChange={(val) => {
setOptions(
matchSorter(
allOptions, // ----------------> (2)
val,
{ keys: ["name", "value"]
}
));
}}
/>
);
}
I am new to react, I am programming the function onAdd(), when I call that it should add the item to state and then I update the hook with the new item setBooks(state).
const state = {
books: [
{ id: 0, rating: 4, title: "Harry Potter y el cáliz de fuego", image: "libro01.jpg"},
{ id: 1, rating: 3, title: "The shining", image: "libro02.jpg" },
{ id: 2, rating: 5, title: "Código Da Vinci", image: "libro03.jpg" },
{ id: 3, rating: 5, title: "El principito", image: "libro04.jpg" },
{ id: 4, rating: 5, title: "Sobrenatural", image: "libro05.jpg" },
],
copyBooks: []
};
function App() {
const [books, setBooks] = useState(state);
const onAdd = (item)=>{
//add new item to books
console.log('Add: ', item);
const id = state.books[state.books.length-1].id++;
item['id'] = id;
state.books.push(item);
setBooks(state);
}
return (
<div className="App">
<Menu onadd={onAdd}/>
<List items={books.books}/>
</div>
);
}
this works fine internally, I print the books object and it is up to date.
When I try to print the data of the child <List /> component is not printed, until I make a change on the server and it is updated.
I came to the conclusion that the problem is that the List component <List /> is not refreshed.
books refers to hooks and books.books to the data.
I do not know how I can solve it, I know it is something basic but I am starting in this technology.
Your onAdd function is mutating state, meaning React sees the same object reference as before and will not update. To make sure an update happens, copy the array and set the state to the new copy:
const onAdd = (item) => {
//add new item to books
console.log('Add: ', item);
const id = state.books[state.books.length-1].id + 1;
item['id'] = id;
const newBooks = [...state.books, item];
setBooks({ ...state, books: newBooks });
}
Edit: by the way, I might recommend some less confusing terminology when naming variables. The local books variable actually refers to the entire state and then books.books refers to the actual books... that's going to cause mistakes because it's very confusing.
I have an array of objects like this getting mapped and rendered.
const items = [
{
id: '1',
name: 'name',
comment: 'text',
},
{
id: '2',
name: 'name',
},
etc...
]
var hasComment = items.some(item => item.hasOwnProperty('comment'));
const card = items.map(item =>
<div key={item.id}>
<ul className="detail">
<li>{item.name}</li>
{hasComment ? <li>{item.comment}</li> : ''}
</ul>
</div>
I want the comment to be displayed depending of the property of each individual object. I googled the above solution but as soon as any object of my array has this property it gets displayed for all, leaving some empty list items. The comment on each individual item should only be rendered, if the object actually has this property.
Hopefully it's clear what I mean.
You need to put the property check inside a .filter before mapping to render:
const card = items
.filter(item => item.hasOwnProperty('comment')
.map(item => {
// ...
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.
So my Reducer is:
const initialState = {
1: {
id: '1',
user: 'User1',
text: 'Dummy Text id1',
SomeFiled: 'SomeValue',
},
2: {
id: '2',
user: 'User1',
text: 'Dummy Text id2',
SomeFiled: 'SomeValue',
},
3: {
id: '3',
user: 'User1',
text: 'Dummy Text id3',
SomeFiled: 'SomeValue',
},
4: {
id: '4',
user: 'User1',
text: 'Dummy Text id4',
SomeFiled: 'SomeValue',
},
5: {
id: '5',
user: 'User1',
text: 'Dummy Text id5',
SomeFiled: 'SomeValue',
}
}
I've mapStateToProps with prop users and able to show the data:
const renData = Object.keys(this.props.users).map((key, idx) => {
let user = this.props.users[key]
return(
<View key={idx} style={styles.myStyle}>
<Text style={styles.myStyleText}>
{ user.id } - { user.user } - { user.Text }
</Text>
</View>
)
});
I want to show only 2 objects from the Reducer. So the first (id: '1') and second (id: '2') but not based on id, only the first 2. And then have a Button which onPress will load more 2 values. And If there are any more values, the button will show, else not show. Not worried about the display of the Button for now. I want to know how to apply limit in rendering values from a reducer.
Many thanks.
You have to use slice method.
The slice() method returns a shallow copy of a portion of an array into a new array object selected from begin to end (end not included). The original array will not be modified.
let size=2;
const renData = Object.keys(this.props.users).slice(0,size).map((key, idx) => {
let user = this.props.users[key]
return(
<View key={idx} style={styles.myStyle}>
<Text style={styles.myStyleText}>
{ user.id } - { user.user } - { user.Text }
</Text>
</View>
)
});
You can declare an object in the state of the component:
this.state={
data:{
count:count,
dataArray:array
}
}
and use setState method in order to bind values.
this.setState({
data: {
count:newCount
dataArray: newArray
}
});
The same scenario what are you are expecting with pure JS. Using same Array#slice logic as #MayankShukla said.
var count = 0;
const data = [1,2,3,4,5,6,7,8,9,10,11];
function renderMore(){
count+= 2;
if(count > data.length) {
count = data.length;
document.getElementById("button").disabled = true;
}
let renData = data.slice(0,count);
console.log(renData)
}
<button id="button" onclick="renderMore()">Show more</button>
Hope this helps :)
Maintain a state variable inside component, that will hold the count of items that you want to render, initial value of that variable will be 2.
Then use #array.slice to get the part of data, and run #array.map on it, To load more items, update the count of that variable.
Write it like this:
const renData = Object.keys(this.props.users).slice(0, 2).map((key, idx) => {
......
}
Note: In place of two use that variable.