database (firestore) call is only taking the first value - javascript

I want to make a call the database (firestore) to return the data of the products in real time that the user has added them to the cart then this data will be used in a code to get the total price of the cart items so I have tried the following approach:
This useEfect will get make the database call and will set the items state to to an array of objects that contains the id and quantity of each cart item on real time
const [items, setItems] = useState([]);
const [subTotal, setSubTotal] = useState([]);
const [Total, setTotal] = useState(0);
const oneSubTotal = [];
useEffect(() => {
db.collection("users").doc("4sfrRMB5ROMxXDvmVdwL").collection("basket").onSnapshot((docs) => {
let array = []
docs.forEach(doc =>{
array.push(doc.data())
console.log(array)
setItems(array)
})
});
}, [])
This code should loop through the items array to add an the element item.price to each object after getting the price of each item by another call to the database then it push to the Subtotal array the total price of each item by pushing the quantity multiplied by the price
useEffect(() => {
items && items.forEach((item) => {
// console.log(item)
const id = item.id
db.collection("products").doc(id).get().then((e)=>{
item.price = (e.data().price)
oneSubTotal.push(item.price * item.quantity)
setSubTotal(oneSubTotal)
})
})
then this code will loop through the subtotal array to get the sum of the price of the items
let sum = 0;
for (let num of subTotal){
sum = sum + num
}
useEffect(() => {
setTotal(sum)
}, [sum, items])
but the issue is the value of Total when the page renders first time always will be the total price of the first item which is represented with the first object in the items array and when I modify the quantity of any item (without refreshing the page) the Total value shows the correct amount for few seconds then the value of it returns to show the first items total price only

In your onSnapshot handler, you are calling console.log(array) and setItems(array) prematurely. This potentially causes your app to be rendered multiple times. You should make sure to be calling these lines outside of the forEach loop.
.onSnapshot((docs) => {
let array = []
docs.forEach(doc => {
array.push(doc.data())
});
console.log(array)
setItems(array)
});
But even so, it would be more efficient to fetch the item prices before calling setItems. Plus, instead of calling out to the database one-by-one using forEach, you should bundle the requests into batches like shown in this answer which is available as a utility function, fetchDocumentsWithId().
.onSnapshot((docs) => {
const cartItems = [], itemPriceObject = {};
// cartItems will be ({ id: string, quantity: number, price: number | null, lineTotal: number })[]
// itemPriceObject will be a map of IDs to their price (a Record<string, number | null>) (used to deduplicate IDs & store prices)
docs.forEach(doc => {
const cartItem = doc.data()
cartItems.push(cartItem)
itemPriceObject[cartItem.id] = null
});
// idsArray is a deduplicated list of IDs
const idsArray = Object.keys(itemPriceObject);
fetchDocumentsWithId(
db.collection("products"),
idsArray,
(itemDoc) => {
itemPriceObject[itemDoc.id] = itemDoc.get("price") // more efficient than itemDoc.data().price
}
)
.then(() => {
// if here, all prices (that are available) have been retrieved
// MAY BE NULL! Consider these items to be "not available"
const totalSum = 0
// put prices in their items, calculate line cost and add to total
cartItems.forEach(item => {
item.price = itemPriceObject[item.id]
item.lineTotal = item.price === null ? 0 : item.price * item.quantity
totalSum += item.lineTotal
}
// set items & total sum
setItems(cartItems)
setTotal(totalSum)
})
.catch((error) => {
// failed to retrieve some documents from the database
// TODO: update UI
});
});
Note: For clarity, subTotal (meaning: the sum of some, but not all values) was renamed to lineTotal (meaning: the cost of items in this entry/line, the cost x quantity)

Related

GroupBy Array elements from CSV file and help reduce code

Each csv file that is imported has the same data structure.
I need to sum the ['Net Charge Amount'] by each '[Service Type'].
I am currently doing this by assigning each unique ['Service Type'] to their own array. My current script is probably overkill but it is very easy to follow, however I am looking for a more compact way of doing this otherwise this script could get very long.
const fs = require('fs')
const { parse } = require('csv-parse')
// Arrays for each service type
const GroundShipments = []
const HomeDeliveryShipments = []
const SmartPostShipments = []
const Shipments = []
The [Shipments] array will hold all data and I would assume this is the array
we want to work with
//functions for each service type
function isGround(shipment) {
return shipment['Service Type'] === 'Ground'
}
function isHomeDelivery(data) {
return data['Service Type'] === 'Home Delivery'
}
function isSmartpost(shipment) {
return shipment['Service Type'] === 'SmartPost'
}
function isShipment(shipment) {
return shipment['Service Type'] === 'Ground' || shipment['Service Type'] === 'Home Delivery' ||
shipment['Service Type'] === 'SmartPost'
}
// Import csv file / perform business rules by service type
// output sum total by each service type
fs.createReadStream('repco.csv')
.pipe(parse({
columns: true
}))
.on('data', (data) => {
//push data to proper service type array
// Ground
if (isGround(data)) {
GroundShipments.push(data)
}
// Home Delivery
if (isHomeDelivery(data)) {
HomeDeliveryShipments.push(data)
}
// Smartpost
if (isSmartpost(data)) {
SmartPostShipments.push(data)
}
// All shipment types, including Ground, Home Delivery, and Smartpost
if (isShipment(data)) {
Shipments.push(data)
}
})
.on('error', (err) => {
console.log(err)
})
.on('end', (data) => {
// sum data by service type
// Ground Only
const sumGround = GroundShipments.reduce((acc, data) =>
acc + parseFloat(data['Net Charge Amount']), 0)
// Home Delivery Only
const sumHomeDelivery = HomeDeliveryShipments.reduce((acc, data) =>
acc + parseFloat(data['Net Charge Amount']), 0)
// SmartPost Only
const sumSmartPost = SmartPostShipments.reduce((acc, data) =>
acc + parseFloat(data['Net Charge Amount']), 0)
// All services
const sumAllShipments = Shipments.reduce((acc, data) =>
acc + parseFloat(data['Net Charge Amount']), 0)
//output sum by service type to console
console.log(`${GroundShipments.length} Ground shipments: ${sumGround}`)
console.log(`${HomeDeliveryShipments.length} Home Delivery shipments: ${sumHomeDelivery}`)
console.log(`${SmartPostShipments.length} Smartpost shipments: ${sumSmartPost}`)
console.log(`${Shipments.length} All shipments: ${sumAllShipments}`)
})
Here is the console output:
[1]: https://i.stack.imgur.com/FltTU.png
Instead of separating each ['Service Type'] by its own Array and Function, I would like one Array [Shipments] to output each unique ['Service Type'] and sum total of ['Net Charge Amount']
The two keys to simplifying this are:
separating the CSV parsing from the data processing
using a groupBy function
First, you should parse the CSV into a simple JS array. Then you can use regular JS utility functions to operate on the data, such as the groupBy function. It is a utility that can be found in the lodash and ramda libraries. It's probably going to be added to vanilla JS as the .group method but that's a while from now.
I was looking for a sample problem to play with my own JS evaluation framework, so I answered your question there:
You can explore the underlying val yourself: https://www.val.town/stevekrouse.exampleGroupByShppingCSV
There are a couple things about my answer that wouldn't make sense in a normal NodeJS codebase, but that I had to do to make it work in val.town (async/await, using a custom groupBy method instead of importing one). If you'd like help getting it to work in your application, just let me know.
A solution would be to use a Map instance to keep track of the stats of different service types.
For each shipment find the associated stats (based on service type), or create a new stats object { count: 0, sum: 0 }. Then increment the count, and add the amount to the sum.
When all data is iterated (on end), you can loop through the serviceTypeStats which and log the values. You can also use this loop to calculate the total by adding all count and sum of each service type group.
const serviceTypeStats = new Map();
// ...
.on('data', (shipment) => {
const serviceType = shipment['Service Type'];
const amount = parseFloat(shipment['Net Charge Amount']);
if (!serviceTypeStats.has(serviceType)) {
serviceTypeStats.set(serviceType, { count: 0, sum: 0 });
}
const stats = serviceTypeStats.get(serviceType);
stats.count += 1;
stats.sum += amount;
})
// ...
.on('end', () => {
const total = { count: 0, sum: 0 };
for (const [serviceType, stats] of serviceTypeStats) {
total.count += stats.count;
total.sum += stats.sum;
console.log(`${stats.count} ${shipmentType}: ${stats.sum}`);
}
console.log(`${total.count} All shipments: ${total.sum}`);
})
If you want to loop keys in a specific order you can define the order in an array, or sort the keys of the Map instance.
// pre-defined order
const serviceTypeOrder = ["Ground", "Home Delivery", "SmartPost"];
// or
// alphabetic order (case insensitive)
const serviceTypeOrder = Array.from(serviceTypeStats.keys());
serviceTypeOrder.sort((a, b) => a.localeCompare(b, undefined, { sensitivity: "base" }));
// ...
for (const serviceType of sericeTypeOrder) {
const stats = serviceTypeStats.get(serviceType);
// ...
}

Stop rendering UseEffect of Click of a specific function

In my Application when ever i change the number i want run the UseEffect Hook and re-render the updated values in the array which is working, but one condition i don't want to run the use UseEffect Hook is when i click addRow function i want bank field should be added in the rows except the number
JsCode
const [number, setNumber] = useState(3);
const [data, setData] = useState([]);
const [insValue,setInstValue]=useState(90);
useEffect(() => {
const list = [];
for (let i = 1; i <= number; i++) {
let ins = instValue/number;
list.push({
insNo: i,
installmentAmount: ins,
chequeNumber: '',
});
}
setData(list);
}, [number]);
const addRow = () => {
setNumber((number) => number + 1);
console.log('Add item clicked');
data.push({
insNo: number + 1,
installmentAmount: '',
chequeNumber: ''
});
const list = [...data];
setData(list);
};
Html
<div onClick={addRow}>
ADD ROW
</div>
currently what it is happening is when i click on add row number get changed and useffect Runs and data get updated i need is blank field in the row on the last object of an array
Output Getting
[{"insNo":1,"installmentAmount":22.5,"chequeNumber":""},
{"insNo":2,"installmentAmount":22.5,"chequeNumber":""}
{"insNo":3,"installmentAmount":22.5,"chequeNumber":""}
{"insNo":4,"installmentAmount":22.5,"chequeNumber":""}]
Output i need on click of addRow
[{"insNo":1,"installmentAmount":30,"chequeNumber":""},
{"insNo":2,"installmentAmount":30,"chequeNumber":""}
{"insNo":3,"installmentAmount":30,"chequeNumber":""}
{"insNo":4,"installmentAmount":"","chequeNumber":""}]
It's hard to understand what you exactly want.
But according to your code, you can't get the desired result.
Because you increase number and add a new row with empty values. And useEffect updates your array data again. So the installmentAmount in the last row will be filled.
To avoid it, you can change the code block like the following:
const initialNumber = 3; // Define initial value to check if data array is in the initial step.
const [number, setNumber] = useState(initialNumber);
// ... ...
useEffect(() => {
const list = [];
for (let i = 1; i <= number; i++) {
list.push({
insNo: i,
installmentAmount: i > initialNumber && i == number? '' : instValue/number,
chequeNumber: '',
});
}
setData(list);
}, [number]);
const addRow = () => {
setNumber((number) => number + 1);
console.log('Add item clicked');
const list = [...data];
list.push({
insNo: number + 1,
installmentAmount: '',
chequeNumber: ''
});
setData(list);
};
The data is refreshed again into the useEffect when you change number, losing all data.
You can avoid change number state at click, since the new data is already being saved manually.
setNumber((number) => number + 1);

Get initial value of Firestore document

I am trying to make an addition of a grocery quantity in my react native app. The groceries are coming from firebase firestore with pre-defined prices, I want that when I increase the quantity, the total count of the cart_items should be correctly calculated. I am approaching this by adding it directly from the server.
The issue is, I need to be able to get only the initial price of a grocery, so I can add and subtract at will, instead, I am getting the updated price when I add the quantity, and that updated price is being added to the current price when I need to increase the quantity again. I hope you get what I mean.
const increment = async (id) => {
const itemRef = doc(db, "cartItems", id);
await getDoc(itemRef).then(async (snapshot) => {
// This Line of code is supposed to capture the initial value of the price
let price = snapshot.data().data.price;
console.log(price);
// This Line of code is supposed to capture the initial value of the price
await updateDoc(itemRef, {
quantity: snapshot.data().quantity + 1,
data: {
...snapshot.data().data,
price: snapshot.data().data.price + price,
// I am supposed to use that initial value for this calculation
},
});
});
};
And here's for decreasing the quantity
const decrement = async (id) => {
const itemRef = doc(db, "cartItems", id);
await getDoc(itemRef).then(async (snapshot) => {
// This Line of code is supposed to capture the initial value of the price
let price = snapshot.data().data.price;
console.log(price);
// This Line of code is supposed to capture the initial value of the price
await updateDoc(itemRef, {
quantity:
snapshot.data().quantity === 1 ? 1 : snapshot.data().quantity - 1,
data: {
...snapshot.data().data,
price:
snapshot.data().data.price === price
? price
: snapshot.data().data.price - price,
// I am supposed to use that initial value for this calculation
},
});
});
};
So I just need to know if there's a way I can get only the initial value of the price and not the updated value. Please let me know if I need to clarify anything about the question. It's a really pressing issue for me right now.
I got my answer guys.
I just had to add an initial value that remains constant and doesn't change to my database. That's what I use to make the necessary calculations on my app.

Filter out object elements that do not have associated products in DB

I have an object containing a list of categories, and I want to remove the individual elements (categories) that have no associated products in the products table.
How would I loop through the cats object, read the catid property for each element, search the products table and get all products matching that catid value, then filter out from the original object elements that do not have any products for the associated category?
I'm using nodejs with knex as the middleware to access the MySQL table... but that should be irrelevant to the question of "how do I filter certain elements out of the object"?
const cats = await knex('categories').select('catid', 'catname', 'desc', 'refnum');
const newCats = cats.filter(async (item) => {
const prods = await knex('products').select('prodid').where({ category: item.catid });
if (prods.length > 1){
console.log('ThisCat:', item.catname, ' and LENGTH:', prods.length);
return d;
}
});
console.log({ newCats}); //SHOULD have dropped elements with no associated products
My ThisCat console.log correctly skips the categories without an associated product, but the newCats object still contains all the original entries.
Filter call back function needs to return true or false. you can do it like:
const newCats = cats.filter(async (item) => {
const prods = await knex('products').select('prodid').where({ category: item.catid });
if (prods.length > 1){
console.log('ThisCat:', item.catname, ' and LENGTH:', prods.length);
return true;
}
return false;
});
or just use the if condition:
const newCats = cats.filter(async (item) => {
const prods = await knex('products').select('prodid').where({ category: item.catid });
return prods.length > 1
});

How to dynamically handle a multitude of filters with react?

I am currently working on an online store that filters products based on certain criteria such as size, stock, gender, etc.
While I have been able to make it work to a certain extent. My program currently filters by size, gender, sorts by price etc. However, I cannot get it to filter by brand. For some reason once I click on the brand, I am able to filter the function once, however, once I click on another brand the filter for that particular brand does not run.
Here is the link to the code sandbox:
https://codesandbox.io/s/mystifying-roentgen-7mp0t
I am currently stuck with filtering by brand and I have tried to compared my filtered result to the state of the item clicked, by checking if the brand is included in the item and by using localeCompare().
Here is the link to the code sandbox:
https://codesandbox.io/s/mystifying-roentgen-7mp0t
createCheckboxes = () => available_sizes.map(this.createCheckbox);
handleFormSubmit = event => {
//4) this button updates the filters on the sizes, which I think I need to fix to update the brands, the price and the gender
event.preventDefault();
//5) right here I am storing the selected checkboxes which is what I was doing before by pushing the checkboxes
const selectedSizes = [...this.selectedCheckboxes];
const shallowCopy = [...this.state.filteredProducts];
let filteredProducts = shallowCopy.filter(product =>
selectedSizes.every(size =>
product.stock.some(s => s.stock > 0 && s.size === size)
)
);
let filteredGender = filteredProducts.filter(product => {
return product.gender.some((item, idx, arr) => {
return item[this.selectedGender] === false ? null : product;
});
});
//***this is the function that is not currently running***//
let filteredData = filteredGender.filter(product => {
//console.log(product.brand.includes(this.state.activeBrand))
//console.log(product.brand = this.state.brand)
return product.brand.includes(this.state.activeBrand)
});
let sortedPrice = filteredData.sort((a, b) => {
return this.state.sortBy === "min"
? a.price - b.price
: b.price - a.price;
});
this.setState({
filteredProducts: sortedPrice
});
};
I am expecting to be able to filter by brand wit this function, once an item is clicked.
Here is the link to the code sandbox:
https://codesandbox.io/s/mystifying-roentgen-7mp0t
There are 2 errors in your application:
1) the first one is reported by #user753642 in comment to your question, remove this line from index.js, because it sets your brand of all products to "":
console.log(product.brand = this.state.brand)
2) you are filtering filteredProducts and no the all products. While after first filtering on brand the filterdProducts does not have any item of other brands, it returns an empty collection after filtering on another brand. Change line in handleFormSubmit in index.js, from:
const shallowCopy = [...this.state.filteredProducts];
to:
const shallowCopy = [...this.state.products];

Categories