I'm working on a shopping cart in react by using context. My problem is with changing the state that has an array of objects.
My array will look like this [{itemId: 'ps-5', qty:4}, {itemId: 'iphone-xr', qty:2}]
Here is my code check the comment
export const CartContext = createContext()
class CartContextProvider extends Component {
state = {
productsToPurchase: []
}
addProduct = (itemId)=> {
if (JSON.stringify(this.state.productsToPurchase).includes(itemId)){
// Add one to the qty of the product
this.state.productsToPurchase.map(product=>{
if (product.itemId === itemId){
// This is wrong I have to use setState(), but the syntax is a little bit complex
product.qty = product.qty + 1
}
})
}
else {
this.state.productsToPurchase.push({itemId: itemId, qty: 1})
}
}
render() {
return (
<CartContext.Provider value={{...this.state, addProduct: this.addProduct}}>
{this.props.children}
</CartContext.Provider>
)
}
}
export default CartContextProvider;
You are updating the state directly, but you have to use this.setState to update it,
Live Demo
addProduct = (itemId) => {
this.setState((oldState) => {
const objWithIdExist = oldState.productsToPurchase.find((o) => o.itemId === itemId);
return {
productsToPurchase: !objWithIdExist
? [...oldState.productsToPurchase, { itemId, qty: 1 }]
: oldState.productsToPurchase.map((o) =>
o.itemId !== itemId ? o : { ...o, qty: o.qty + 1 }
)
};
});
};
Related
I have a filter panel where the user can select different color filters:
// ColorButton.jsx
function ColorButton({ color }) {
const handleFilterSelected = ({ id }) => {
dispatch(applyFilter({ type: "colors", value: id }));
};
return (
<div className="color-button" onClick={() => handleFilterSelected(color)}>
{isFilterSelected({ type: "colors", value: color.id }) ? "yay" : "nay"}
</div>
);
}
The selected filters are stored in the redux store and the isFilterSelect function looks like this:
// redux/utils/filter.utils.js
export const isFilterSelected = ({ type, value }) => {
const {
filters: { applied }
} = store.getState();
return applied
.filter((f) => f.type === type)
.map((f) => f.value)
.includes(value);
};
The issue is that the check runs before a selected filter is added to the applied array.
As a more general question - is it even a good idea to have "helper" functions that depend on the redux store?
Your helper function there should be written as a selector that gets the current state, and you should be using useSelector instead of store.getState manually, as that will update your component when the selector value changes.
function ColorButton({ color }) {
const isSelected = useSelector(state => isFilterSelected(state, { type: "colors", value: color.id }));
return (
<div className="color-button">
{isSelected ? "yay" : "nay"}
</div>
);
}
// redux/utils/filter.utils.js
export const isFilterSelected = (state, { type, value }) => {
return state.filters.applied
.filter((f) => f.type === type)
.map((f) => f.value)
.includes(value);
};
My code is basically a form with a text input and a submit button. Each time the user input data, my code adds it to an array and shows it under the form.
It is working fine; however, when I add duplicate values, it still adds it to the list. I want my code to count these duplicates and show them next to each input.
For example, if I input two "Hello" and one "Hi" I want my result to be like this:
2 Hello
1 Hi
Here is my code
import React from 'react';
import ShoppingItem from './ShoppingItem';
class ShoppingList extends React.Component {
constructor (props){
super(props);
this.state ={
shoppingCart: [],
newItem :'',
counter: 0 };
}
handleChange =(e) =>
{
this.setState ({newItem: e.target.value });
}
handleSubmit = (e) =>
{
e.preventDefault();
let newList;
let myItem ={
name: this.state.newItem,
id:Date.now()
}
if(!this.state.shoppingCart.includes(myItem.name))
{
newList = this.state.shoppingCart.concat(myItem);
}
if (this.state.newItem !=='')
{
this.setState(
{
shoppingCart: newList
}
);
}
this.state.newItem ="" ;
}
the rest of my code is like this:
render(){
return(
<div className = "App">
<form onSubmit = {this.handleSubmit}>
<h6>Add New Item</h6>
<input type = "text" value = {this.state.newItem} onChange ={this.handleChange}/>
<button type = "submit">Add to Shopping list</button>
</form>
<ul>
{this.state.shoppingCart.map(item =>(
<ShoppingItem item={item} key={item.id} />
)
)}
</ul>
</div>
);
}
}
export default ShoppingList;
Issues
this.state.shoppingCart is an array of objects, so this.state.shoppingCart.includes(myItem.name) will always return false as it won't find a value that is a string.
this.state.newItem = ""; is a state mutation
Solution
Check the newItem state first, if empty then return early
Search this.state.shoppingCart for the index of the first matching item by name property
If found then you want to map the cart to a new array and then also copy the item into a new object reference and update the quantity.
If not found then copy the array and append a new object to the end with an initial quantity 1 property.
Update the shopping cart and newItem state.
Code
handleSubmit = (e) => {
e.preventDefault();
if (!this.state.newItem) return;
let newList;
const itemIndex = this.state.shoppingCart.findIndex(
(item) => item.name === this.state.newItem
);
if (itemIndex !== -1) {
newList = this.state.shoppingCart.map((item, index) =>
index === itemIndex
? {
...item,
quantity: item.quantity + 1
}
: item
);
} else {
newList = [
...this.state.shoppingCart,
{
name: this.state.newItem,
id: Date.now(),
quantity: 1
}
];
}
this.setState({
shoppingCart: newList,
newItem: ""
});
};
Note: Remember to use item.name and item.quantity in your ShoppingItem component.
Replace your "handleSubmit" with below one and check
handleSubmit = (e) => {
e.preventDefault();
const { shoppingCart, newItem } = this.state;
const isInCart = shoppingCart.some(({ itemName }) => itemName === newItem);
let updatedCart = [];
let numberOfSameItem = 1;
if (!isInCart && newItem) {
updatedCart = [
...shoppingCart,
{
name: `${numberOfSameItem} ${newItem}`,
id: Date.now(),
itemName: newItem,
counter: numberOfSameItem
}
];
} else if (isInCart && newItem) {
updatedCart = shoppingCart.map((item) => {
const { itemName, counter } = item;
if (itemName === newItem) {
numberOfSameItem = counter + 1;
return {
...item,
name: `${numberOfSameItem} ${itemName}`,
itemName,
counter: numberOfSameItem
};
}
return item;
});
}
this.setState({
shoppingCart: updatedCart,
newItem: ""
});
};
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()
}
I want to filter the data from my multiple states at one time. But I am getting the data of only second state.
I have two states and both states are getting seprate data from seprate apis. Now I want to filter the data from it. thank youI don't know what i m doing wrong so pls help me and look at my code.
searchFeatured = value => {
const filterFeatured = (
this.state.latestuploads || this.state.featuredspeakers
).filter(item => {
let featureLowercase = (item.name + " " + item.title).toLowerCase();
let searchTermLowercase = value.toLowerCase();
return featureLowercase.indexOf(searchTermLowercase) > -1;
});
this.setState({
featuredspeakers: filterFeatured,
latestuploads: filterFeatured
});
};
class SearchPage extends Component {
constructor(props) {
super(props);
this.state = {
loading: false,
featuredspeakers: [],
latestuploads: [],
};
}
componentDidMount() {
axios
.all([
axios.get(
'https://staging.islamicmedia.com.au/wp-json/islamic-media/v1/featured/speakers',
),
axios.get(
'https://staging.islamicmedia.com.au/wp-json/islamic-media/v1/featured/latest-uploads',
),
])
.then(responseArr => {
//this will be executed only when all requests are complete
this.setState({
featuredspeakers: responseArr[0].data,
latestuploads: responseArr[1].data,
loading: !this.state.loading,
});
});
}
Using the || (OR) statement will take the first value if not null/false or the second. What you should do is combine the arrays
You should try something like
[...this.state.latestuploads, ... this.state.featuredspeakers].filter(item=>{});
Ahmed, I couldn't get your code to work at all - searchFeatured is not called anywhere. But I have some thoughts, which I hope will help.
I see that you're setting featuredspeakers and latestuploads in componentDidMount. Those are large arrays with lots of data.
But then, in searchFeatured, you are completely overwriting the data that you downloaded and replacing it with search/filter results. Do you really intend to do that?
Also, as other people mentioned, your use of the || operator is just returning the first array, this.state.latestuploads, so only that array is filtered.
One suggestion that might help is to set up a very simple demo class which only does the filtering that you want. Don't use axios at all. Instead, set up the initial state with some mocked data - an array of just a few elements. Use that to fix the filter and search functionality the way that you want. Here's some demo code:
import React from 'react';
import { Button, View, Text } from 'react-native';
class App extends React.Component {
constructor(props) {
super(props);
this.searchFeatured = this.searchFeatured.bind(this);
this.customSearch = this.customSearch.bind(this);
this.state = {
loading: false,
featuredspeakers: [],
latestuploads: [],
};
}
searchFeatured = value => {
// overwrite featuredspeakers and latestuploads! Downloaded data is lost
this.setState({
featuredspeakers: this.customSearch(this.state.featuredspeakers, value),
latestuploads: this.customSearch(this.state.latestuploads, value),
});
};
customSearch = (items, value) => {
let searchTermLowercase = value.toLowerCase();
let result = items.filter(item => {
let featureLowercase = (item.name + " " + item.title).toLowerCase();
return featureLowercase.indexOf(searchTermLowercase) > -1;
});
return result;
}
handlePress(obj) {
let name = obj.name;
this.searchFeatured(name);
}
handleReset() {
this.setState({
featuredspeakers: [{ name: 'Buffy', title: 'Slayer' }, { name: 'Spike', title: 'Vampire' }, { name: 'Angel', title: 'Vampire' }],
latestuploads: [{ name: 'Sarah Michelle Gellar', 'title': 'Actress' }, { name: 'David Boreanaz', title: 'Actor' }],
loading: !this.state.loading,
});
}
componentDidMount() {
this.handleReset();
}
getList(arr) {
let output = [];
if (arr) {
arr.forEach((el, i) => {
output.push(<Text>{el.name}</Text>);
});
}
return output;
}
render() {
let slayerList = this.getList(this.state.featuredspeakers);
let actorList = this.getList(this.state.latestuploads);
return (
<View>
<Button title="Search results for Slayer"
onPress={this.handlePress.bind(this, {name: 'Slayer'})}></Button>
<Button title="Search results for Actor"
onPress={this.handlePress.bind(this, {name: 'Actor'})}></Button>
<Button title="Reset"
onPress={this.handleReset.bind(this)}></Button>
<Text>Found Slayers?</Text>
{slayerList}
<Text>Found Actors?</Text>
{actorList}
</View>
);
}
};
export default App;
You should apply your filter on the lists separately then. Sample code below =>
const searchFeatured = value => {
this.setState({
featuredspeakers: customSearch(this.state.featuredspeakers, value),
latestuploads: customSearch(this.state.latestuploads, value)
});
};
const customSearch = (items, value) => {
return items.filter(item => {
let featureLowercase = (item.name + " " + item.title).toLowerCase();
let searchTermLowercase = value.toLowerCase();
return featureLowercase.indexOf(searchTermLowercase) > -1;
});
}
I have the following structure in my Redux state:
"equipment" : {
"l656k75ny-r7w4mq" : {
"id" : "l656k75ny-r7w4mq",
"image" : "cam.png",
"isChecked" : false,
"list" : {
"2taa652p5-mongp0" : {
"id" : "2taa652p5-mongp0",
"image" : "dst.png",
"isChecked" : false,
"name" : "Dst"
},
"y3ds1rspk-ftdg2t" : {
"id" : "y3ds1rspk-ftdg2t",
"image" : "flm.png",
"isChecked" : false,
"name" : "Flm"
}
},
"name" : "EQ 1"
},
"lpbixy0f7-bmiwzl" : {
"image" : "prm.png",
"isChecked" : false,
"list" : {
"l40snp2y6-o7gudg" : {
"image" : "tlp.png",
"isChecked" : false,
"name" : "Tlp"
},
"qmxf9bn3v-9x1nky" : {
"image" : "prm.png",
"isChecked" : false,
"name" : "Prm"
}
},
"name" : "EQ 2"
}
}
I have an Equipment component (sort of higher order component), in which I subscribe to this part of the state and I pass this its data to my CustomExpansionPanel component:
class Equipment extends Component {
//...different methods
render() {
const { data } = this.props;
let content = error ? <p>Oops, couldn't load equipment list...</p> : <Spinner />;
if ( data.length > 0 ) {
content = (
<Fragment>
<CustomExpansionPanel data={data}/>
</Fragment>
);
}
return (
<SectionContainer>
{content}
</SectionContainer>
);
}
}
const mapStateToProps = state => {
return {
data: state.common.equipment,
};
};
connect(mapStateToProps, mapDispatchToProps)(withStyles(styles)(Equipment));
In my CustomExpansionPanel I map the received data and create an expansion panel for each section, while passing this section to CustomTable component, which is displayed as ExpansionPanelDetails content.
class CustomExpansionPanel extends Component {
//... different methods
render() {
const { data } = this.props;
return (
<Fragment>
{
data.map((section, index) => {
return (
<ExpansionPanel defaultExpanded={false} key={section.id} disabled={!section.hasOwnProperty('list')} >
<ExpansionPanelSummary expandIcon={<StyledIcon classes={{root: classes.expansionIcon}} />}>
<Avatar
alt={section.name}
src={images('./' + section.image)}
classes={{root: classes.avatar}}
/>
<Typography variant='body1' classes={{root: classes.summaryText}}>{section.name}</Typography>
</ExpansionPanelSummary>
<ExpansionPanelDetails>
<CustomTable data={section} imageVariant={imageVariant} parentId={section.id} />
</ExpansionPanelDetails>
</ExpansionPanel>
);
})
}
</Fragment>
);
}
}
And in my CustomTable component I map the list property of the received section and display its data. When clicking on the row I build a Form component with the appropriate fields and display it for editing the list item object:
class CustomTable extends Component {
//...local state
//...some methods
cellClickHandler = (object, index) => {
this.setState({
editedObject: object,
editedObjectIndex: index
}, () => {
this.setFormData(object);
});
};
handleSaveClicked = data => {
data.id = this.state.editedObject.id;
if(this.props.parentId) {
data.parentId = this.props.parentId;
}
if ( this.state.editedObject && this.state.editedObject.list ) data.list = this.state.editedObject.list;
this.props.onSaveItemEditClicked(this.props.updatedSection, data, this.state.editItemLevel, this.state.editedObjectIndex, () => {
this.handleDialogState();
});
};
componentWillMount() {
if ( this.props.data.list ) {
if ( Array.isArray(this.props.data.list) ) {
this.setState({rows: [...this.props.data]});
} else {
let transformedData = [];
for (let [key, value] of Object.entries(this.props.data.list)) {
let obj = value;
obj['id'] = key;
transformedData.push(obj);
}
this.setState({
rows: [...transformedData],
sortedProperties: sortProperties(Object.keys(transformedData[0]))
});
}
} else if (Array.isArray(this.props.data) && this.props.data.length) {
this.setState({
rows: [...this.props.data],
sortedProperties: sortProperties(Object.keys(this.props.data[0]))
});
}
if (this.props.parentId) {
this.setState({editItemLevel: 'inner'})
}
}
componentWillReceiveProps(nextProps, nextContext) {
if ( nextProps.data.list ) {
if ( Array.isArray(nextProps.data.list) ) {
this.setState({rows: [...nextProps.data]});
} else {
let transformedData = [];
for (let [key, value] of Object.entries(nextProps.data.list)) {
let obj = value;
obj['id'] = key;
transformedData.push(obj);
}
this.setState({
rows: [...transformedData],
sortedProperties: sortProperties(Object.keys(transformedData[0]))
});
}
} else if (Array.isArray(nextProps.data) && nextProps.data.length) {
this.setState({
rows: [...nextProps.data],
sortedProperties: sortProperties(Object.keys(nextProps.data[0]))
});
}
}
render() {
// Table, TableHead, TableRow, TableCell, TableFooter definitions...
<FormDialog
open={this.state.dialogOpen}
title={this.state.dialogTitle}
content={this.state.dialogContentText}
handleClose={this.handleDialogState}
formElements={this.state.formElements}
action={this.handleSaveClicked}
onCancelClicked={this.handleDialogState} />
}
}
const mapStateToProps = state => {
return {
updatedSection: state.control.Update.updatedSection
};
};
const mapDispatchToProps = dispatch => {
return {
onSaveItemEditClicked: (updatedSection, object, level, index, callback) => dispatch(commonActions.onSaveEdit(updatedSection, object, level, index, callback))
};
};
export default connect(mapStateToProps, mapDispatchToProps)(withStyles(styles)(CustomTable));
The form opens, I can change, for example, the name of the object and hit the save button, that triggers the onSaveItemEditClicked method, which dispatches the onSaveEdit action.
onSaveEdit = (updatedSection, object, level='root', index, callback) => {
return dispatch => {
dispatch(controlActions.onSetSnackbarMessage(SnackbarMessages.PROCESS.UPDATING + ' \'' + capitalize(object.name) + '\''));
dispatch(controlActions.DBProcessStart());
let parentId = null;
let url = '/' + updatedSection + '/' + object.id;
if (level === 'inner') {
url = '/' + updatedSection + '/' + object.parentId + '/list/' + object.id;
parentId = object.parentId;
delete object.parentId;
}
updateData(url, object)
.then(response => {
dispatch(controlActions.onSetSnackbarMessage('\'' + capitalize(object.name) + '\' ' + SnackbarMessages.SUCCESS.UPDATED_SUCCESSFULLY));
dispatch(controlActions.DBProcessSuccess());
if (parentId) {
object.parentId = parentId;
}
dispatch(this[saveEditSuccess](updatedSection, object, level, index));
if (callback != null) {
callback();
}
})
.catch(error => {
console.error('onSaveEdit error', error);
});
}
};
[saveEditSuccess] = (updatedSection, object, level='root', index) => {
return {
type: CommonCommands.UPDATE.SUCCESS,
section: updatedSection,
object: object,
level: level,
index: index
};
};
And finally that's my reducer:
const onUpdateSuccess = (state, action) => {
switch (action.level) {
case 'root':
let section = [...state[action.section]];
section = [
...section.slice(0, action.index),
action.object,
...section.slice(action.index + 1),
];
return updateObject(state, {
[action.section]: section
});
case 'inner':
console.log('inner update success', action);
section = [...state[action.section]];
let parentId = action.object.parentId;
let subsectionIndex = state[action.section].findIndex(member => member.id === parentId);
delete action.object.parentId;
let updatedObject = {...section[subsectionIndex].list[action.object.id], ...action.object};
section[subsectionIndex].list[action.object.id] = updatedObject;
return updateObject(state, {
[action.section]: [...section]
});
default:
return state;
}
};
This is the updateObject method:
const updateObject = (oldObject, updatedProperties) => {
return {
...oldObject,
...updatedProperties
};
};
Now if I inspect the state with Debug Redux tool, I can see the new name on the object, I see that the state does change. But the CustomTable component doesn't rerender, I still see the old name.
The exact same methodology works for me in every other case. If I have an array or object in a state property and I display it in the same CustomTable - I can edit it and see the results immediately (hence I have these 'root' and 'inner' separations). But not in this case.
I tried to use Object.assign({}, state, {[action.section]:section}); instead of updateObject method, but that also didn't help.
Any ideas how to make that component rerender with the changed properties?