API call in ReactJS and setting response to state - node.js

I have an array with ids like
[123,345,212,232.....]
I have a Rest API that responds the particular product with respect to an id .
So in ReactJS, how would I loop through the array and get the products and store in state
I tried doing this in useEffect{running once as in componentDidMount} but it only stores the first product in array.
Are there better ways to accomplish this?
My Actual Code that has the problem is
useEffect(() => {
const getBought = async () => {
user.bought.map(async (boughtID) => {
let bought = await get(`/bought/${boughtID}`, {}, true);
let boughtDate = bought.createdAt.slice(0, 10);
bought.products.map(async (pr) => {
let product = await get(`product/${pr.product}`);
let quantityBought = pr.quantity;
setAllBoughts({
...allBoughts,
[boughtDate]: [
...(allBoughts.boughtDate || []),
{
product,
quantityBought,
},
],
});
});
});
};
getBought();
}, []);
So I have a user which has arrays of boughtIds in bought key...
so I am looping throughout those bought keys
A bought schema has products which has product id and number of product bought (pr.product and pr.quantity).. So I am hitting the API , and save all the products in a state object.
so allBoughts is state which is an object that has keys as (dates) as in the code .createdAt.slice(0,10) which gives me date removing rest of stuffs mongodb added.
But everytime I do it, there are few things that happen.
Either only the first item is saved, and rest are not
Either the same item gets overwritten
or the page reloads infinitely(especially whrn I try to remove dependency array from useEffect)

setAllBoughts({
...allBoughts,
[boughtDate]: [
...(allBoughts.boughtDate || []),
{
product,
quantityBought,
},
],
});
allBoughts is the state that existed when this effect ran. Ie, it's the empty state, with no results in it yet. So every time a result comes back, you are copying the empty state, and adding one entry to it. This erases any results you have.
Instead, you need to use the most recent version of the state, which you can do with the function version of set state:
setAllBoughts(prevState => {
return {
...prevState,
[boughtDate]: [
...(prevState.boughtDate || []),
{
product,
quantityBought,
},
],
};
})

From what you're saying, it seems to me that you're erasing the previous stored result in the state (You get result for id 123, store it, then result of 345, store it and erase the previous value). Or you're just not looping through the array.
Here is an example of how to GET multiple times in a useEffect and assign all the results in the state.
Fully working example at Codesandbox
export default function App() {
const ids = useRef([1, 3, 5]);
const [items, setItems] = useState([]);
useEffect(() => {
const promises = ids.current.map((id) => fetchData(id));
Promise.all(promises).then((responses) => {
setItems(responses);
});
}, []);
async function fetchData(id) {
const response = await fetch(
`https://jsonplaceholder.typicode.com/posts/${id}`
);
return response.json();
}
return (
<div className="App">
{items.map((item) => (
<div key={item.id}>
{item.id} - {item.title}
</div>
))}
</div>
);
}

Related

Firestore promise wait for product details before returning products

I know similar questions like this have been asked 1000 times but for the life of me I am struggling with something I feel is quite simple.
We have 2 tables, one called order_lines the other called order_lines_meta, I need to first query order_lines and for each line get the order_lines_meta and return that
I have tried a lot of variations, here is where I am at and stuck, I need it to wait for the order_lines_meta to come back because otherwise I get blank metaData as the data comes after nodejs has already outputted the order_lines
At the end an object that contains order info, line items of objects and within line items a meta data object
Appreciate the help, I just can't seem to wrap my brain on this one , and I am certainly open to other ways of doing this as well
Using nodejs, express, typescript, firestore
const orderNumber = req.query.orderNumber as string;
const customerName = req.query.customerName as string;
const orderDate = req.query.orderDate as string;
const pickListObj = {
orderNumber: orderNumber,
customerName: customerName,
orderDate: orderDate,
line_items: <any>[],
};
db.collection('order_lines').where('number', '==', orderNumber).get().then((snap) => {
const promises = <any>[];
snap.forEach(async (order: any) => {
// get meta data
const metaDataObj = <any>[];
const productName = order.data().name;
const productQty = order.data().quantity;
promises.push(db.collection('worder_line_meta').where('lineId', '==', order.data().lineId).get().then((doc: any) => {
if (doc.display_value != '') {
const meta = [{display_key: doc.data().display_key, display_value: doc.data().display_value}];
metaDataObj.push(meta);
}
}));
});
return Promise.all(promises);
}).then(() => {
pickListObj.line_items.push({name: productName, quantity: productQty, meta_data: metaDataObj});
});
Move the push statement from the last .then inside the previous .then:
promises.push(db.collection('worder_line_meta')...then((doc: any) => {
if (doc.display_value != '') {
...
}
pickListObj.line_items.push({name: productName,
quantity: productQty,
meta_data: metaDataObj});
}));
In the last .then, you will then find the complete pickListObj.
However, I wonder whether it might be simpler and faster to join the two database collections right on the database and retrieve everything with one db.collection operation.

Is there a way to put an array of objects in localstorage for the user then iterate through it in reactjs? [duplicate]

This question already has answers here:
How to store objects in HTML5 localStorage/sessionStorage
(24 answers)
Closed 12 months ago.
Basically I've been making a web app and been trying to figure out a way to be able to move the data from an api on a dropdown list and make it where when the user clicks on the item they want to "save" to the localstorage, but for some reason when you click on it the first time it doesn't register and you have to click on it a second time and then I tried to get it to save in localstorage and it just gives me [object Object]
const [count, setCount] = useState(0);
const [products, setProducts] = useState([]);
const [product, setProduct] = useState([]);
const setProductData = (data, i) => {
setCount(count + 1);
let product = {
id: data.id,
product_number: data.product_number
}
setProduct([...product, { data }]);
localStorage.setItem(`Product${i + 1}`, [product.id, product.product_number])
console.log(data);
}
const fetchProducts = async () => {
await Axios.get(`${process.env.REACT_APP_API}/products`, {
withCredentials: true
})
.then(res => {
let data = res.data;
setProducts(data.data);
}).catch(error => {
console.error('Error:', error);
});
}
useEffect(() => {
fetchProducts();
}, [count]);
return (
<div>
<h1>Select products</h1>
<div className="mt-5 mb-5">
<InputGroup>
<DropdownButton
variant="outline-secondary"
title={'Products'}
id="dropdown"
>
{
products.map((obj, i) => {
let product = obj.product_number;
return (
<Dropdown.Item
key={i}
onClick={() => setProductData(obj, i)}>{product}</Dropdown.Item>
)
})
}
</DropdownButton>
</InputGroup>
</div>
</div>
)
I've tried multiple ways to make the localStorage.setItem() to try and get the whole object but either shows as [object Object] or only part of what I am needing
and with the project object in the setProductData function, I already tried using the data argument and that just gives the [object Object]
General issue is that localStorage stores data as string so to make it work you have to serialise data on storing and unserialise them on read
So for such case JSON.stringify on storing and JSON.parse on read
Imo you should serialize and store whole products object
localStorage.setItem(`Product${i + 1}`, JSON.stringify([product.id, product.product_number]))
//while fetching you need to parse
JSON.parse(localStorage.getItem(Product${i + 1}))
You can use localDataStorage to transparently store JavaScript data types (Array, Boolean, Date, Float, Integer, String and Object). It also provides lightweight data obfuscation, automatically compresses strings, facilitates query by key (name) as well as query by (key) value, and helps to enforce segmented shared storage within the same domain by prefixing keys. Additionally, it dispatches a custom event allowing you to respond to localStorage change events on the same page/tab that originated them.
[DISCLAIMER] I am the author of the utility [/DISCLAIMER]
Examples:
localDataStorage.set( 'key1', 'Belgian Chonklit' )
localDataStorage.set( 'key2', 1200.0047 )
localDataStorage.set( 'key3', true )
localDataStorage.set( 'key4', { 'RSK' : [1,'3',5,'7',9] } )
localDataStorage.set( 'key5', null )
localDataStorage.get( 'key1' ) --> 'Belgian Chonklit'
localDataStorage.get( 'key2' ) --> 1200.0047
localDataStorage.get( 'key3' ) --> true
localDataStorage.get( 'key4' ) --> Object {RSK: Array(5)}
localDataStorage.get( 'key5' ) --> null
As you can see, primitive values are respected, including objects. Use it in Angular, ReactJS or Vanilla JavaScript.

Cloud firestore trigger query by documentID for array of ids

I am trying to write a transaction that first query documents by documentId from a list of ids, then makes some updates.
I am getting the error:
The corresponding value for FieldPath.documentId() must be a string or a DocumentReference.
For example:
const indexArray = [..list of doc ids...]
const personQueryRef = db.collection("person").where(admin.firestore.FieldPath.documentId(), "in", indexArray)
return db.runTransaction(transaction => {
return transaction.get(personQueryRef).then(personQuery => {
return personQuery.forEach(personRef => {
transaction.update(personRef, { ...update values here })
//more updates etc
})
})
})
I am wanting to do this in an onCreate and onUpdate trigger. Is there another approach I should be taking?
Update
The error still persists when not using a transaction, so this is unrelated to the problem.
The problem does not occur when the query is .where(admin.firestore.FieldPath.documentId(), "==", "just_one_doc_id"). So, the problem is with using FieldPath.documentId() and in.
It sounds like the type of query you're trying to do just isn't supported by the SDK. Whether or not that's intentional, I don't know. But if you want to transact with multiple documents, and you already know all of their IDs, you can use getAll(...) instead:
// build an array of DocumentReference objects
cost refs = indexArray.map(id => db.collection("person").doc(id))
return db.runTransaction(transaction => {
// pass the array to getAll()
return transaction.getAll(refs).then(docs => {
docs.forEach(doc => {
transaction.update(doc.ref, { ...update values here })
})
})
})

How to implement a selector in easy: search for Meteor, using React instead of Blaze

I'm trying to follow the documentation and examples to add a server-side selector to a search function in my Meteor app, implemented using the Easy Search plugin. The end goal is to ensure that only documents the user has permission to see are returned by searching.
I can see a selector working in the Leaderboard example, but I can't get it to work in my code.
Versions:
Meteor 1.7.0.1
easy:search#2.2.1
easysearch:components#2.2.2
easysearch:core#2.2.2
I modified the Meteor 'todos' example app to demonstrate the problem, and my demo code is in a repo.
NOTE! to demonstrate the problem, you need to create an account in the demo app, then create a list and make it private. This add the 'userId' field to the list.
Then you can search for the name of the list, by typing in the search box near the top of the main section; search results are written to the browser console.
The first problem is that if I copy the code from the example in the documentation, I see a server error 'searchObject is not defined:
copied from docs, causes an error: imports/api/lists/lists.js
export const MyIndex = new Index({
'collection': Lists,
'fields': ['name'],
engine: new MongoDBEngine({
selector(searchDefinition, options, aggregation) {
// retrieve the default selector
const selector = this.defaultConfiguration()
.selector(searchObject, options, aggregation)
// options.search.userId contains the userId of the logged in user
selector.userId = options.search.userId
return selector
},
}),
});
It seems there is an error in the docs.
Working instead from the leaderboard example, the code below runs but intermittently returns no results. For example if I have a list called "My list", and I type the search term 's', sometimes the list is returned from the search and sometimes it is not. If I use the MiniMongo engine it all works perfectly.
index selector {"$or":[{"name":{"$regex":".*my.*","$options":"i"}}],"userId":"Wtrr5FRHhkKuAcrLZ"}
client and server: imports/api/lists/lists.js
export const MyIndex = new Index({
'collection': Lists,
'fields': ['name'],
'engine': new MongoDBEngine({
selector: function (searchObject, options, aggregation) {
let selector = this.defaultConfiguration().selector(searchObject, options, aggregation);
selector.userId = options.search.userId;
console.log('index selector', JSON.stringify(selector));
return selector;
}
}),
permission: () => {
return true;
},
});
client: imports/ui/components/lists-show.js
Template.Lists_show.events({
'keyup #search'(event) {
console.log('search for ', event.target.value);
const cursor = MyIndex.search(event.target.value);
console.log('count',cursor.count());
console.log('results', cursor.fetch());
},
});
client: imports/ui/components/lists-show.html
<input id="search" type="text" placeholder="search..." />
Edit: I think the problem is that while the Minimongo engine runs on the client, the MongoDBEngine runs on the server and there are timing issues with the results. The docs show using Tracker.autorun, but that's not a natural fit with my React / Redux app. I'll post an answer if I manage to figure something out - I can't be the only person trying to do something like this.
I got it working in my React / Redux / Meteor app. Things to note:
The cursor MyIndex.search(searchTerm) is a reactive data source - you can't just use it as a return value. When searching on the client with MiniMongo this isn't an issue, but it's important when you use MongoDBEngine to search on the server, because it's asynchronous. In React you can wrap the cursor in withTracker to pass data to the component reactively. In Blaze you would use autorun.tracker. This is shown in the docs but not explained, and it took me a while to understand what was happening.
The docs have an error in the selector example, easily corrected but it's confusing if you have other problems in your code.
With MongoDBEngine, 'permission' must be specified - it does not default to 'true'. Without it, you will see no results.
Writing out the default selector object to the console let me see how it's constructed, and then create a new selector that returns MyDocs that are either public or created by the user.
My code is below. In case it helps anybody else, I've shown how to search on tags also, which are objects with a name property stored in a collection Tags. Each MyDoc has a 'tags' property which is an array of tag ids. The selector first searches the Tags collection to find tags whose name matches the search term, then selects docs in MyDocs with the ids of those tags in their doc.tags array.
There may be a better way to find the search term, or to structure the Tags search, but this is what I could get working.
On server and client:
import { Index, MongoDBEngine } from 'meteor/easy:search';
export const MyDocs = new Mongo.Collection('mydocs');
export const Tags = new Mongo.Collection('tags');
export const MyIndex = new Index({
'collection': MyDocs,
'fields': ['name'],
'engine': new MongoDBEngine({
'selector': function (searchObject, options, aggregation) {
const selector = this.defaultConfiguration().selector(searchObject, options, aggregation);
console.log('default selector', selector); // this searches on name only
// find docs by tag as well as by name
const searchTerm = searchObject.name;
const matchingTags = Tags.find({ 'name': { '$regex': searchTerm } }).fetch();
const matchingTagIds = matchingTags.map((tag) => tag._id);
selector.$or.push({ 'tags': { '$in': matchingTagIds } });
const newSelector = {
'$and': [
{
'$or': [
{ 'isPublic': { '$eq': true } },
{ 'createdBy': options.search.userId },
],
},
{
'$or': selector.$or,
},
],
};
return newSelector;
},
'fields': (searchObject, options) => ({
'_id': 1,
'createdBy': 1,
'name': 1,
}),
'sort': () => ({ 'name': 1 }),
}),
'permission': () => true,
});
React component in client only code:
import React from 'react';
import { connect } from 'react-redux';
import { withTracker } from 'meteor/react-meteor-data';
import PropTypes from 'prop-types';
import store from '../modules/store';
import {
getSearchTerm,
searchStart,
} from '../modules/search'; // contains Redux actions and partial store for search
import { MyIndex } from '../../modules/collection';
function Search(props) {
// functional React component that contains the search box
...
const onChange = (value) => {
clearTimeout(global.searchTimeout);
if (value.length >= 2) {
// user has entered a search term
// of at least 2 characters
// wait until they stop typing
global.searchTimeout = setTimeout(() => {
dispatch(searchStart(value)); // Redux action which sets the searchTerm in Redux state
}, 500);
}
};
...
// the component returns html which calls onChange when the user types in the search input
// and a list which displays the search results, accessed in props.searchResults
}
const Tracker = withTracker(({ dispatch }) => {
// searchTerm is saved in Redux state.
const state = store.getState();
const searchTerm = getSearchTerm(state); // Redux function to get searchTerm out of Redux state
let results = [];
if (searchTerm) {
const cursor = MyIndex.search(searchTerm); // search is a reactive data source
results = cursor.fetch();
console.log('*** cursor count', cursor.count());
return {
'searchResults': results,
};
})(Search);
export default connect()(Tracker);

Cannot read property 'map' of undefined with promise all

I was initially just running one query in node.js but I now need two sets of data so I ran two queries and used Promise.all like this:
Promise.all([products, subcats]);
res.status(200).json({
products,
subcats
});
In React I have:
class ProductList extends Component {
state = {
products: []
};
async componentDidMount() {
const catslug = this.props.match.params.catslug;
const { data: products } = await getCatProducts(catslug);
this.setState({ products: products });
}
When I was only running the one query I was running this without any issue:
this.state.products.map(product => (
Now because I have the 2 queries I need to change it to:
this.state.products.products.map(product => (
But as soon as I do that I get the error:
Cannot read property 'map' of undefined
So, I changed it to this and now it works with no errors:
{this.state.products.products &&
this.state.products.products.map(product => (
My question is why did it work before without having to put in the ... && bit but now I have to use it with the Promise.all in node.js?
It's because of the initial shape of your state
state = {
products: []
};
You see this.state.products is already defined as an array, so it can be mapped over (despite being empty). However this.products.products is NOT an array, it is undefined, so trying to map will return the error you are seeing. The line you have entered
{this.state.products.products &&
this.state.products.products.map(product => (
checks that the array exists before attempting to map it (which wont evaluate to true until your async code finishes, and then the array is defined).
An alternative fix would be to set your initial state shape to match your final state shape. i.e
state ={
products:{
products:[]
}
};
Or if you don't want the nested products property you can change
async componentDidMount() {
const catslug = this.props.match.params.catslug;
const { data } = await getCatProducts(catslug);
this.setState({ products: data.products });
}
and return to your old this.state.products.map()
The issue is in the following line:
const { data: products } = await getCatProducts(catslug);
If I understand correctly, when you were sending one value, you were sending it like:
res.status(200).json(products); // an array
But now you are sending an object, which further contains 2 arrays products and subcats.
What you have 2 do is add below changes to make it work:
const obj = await getCatProducts(catslug);
const products = obj.products
const subcats = obj.subcats

Resources