I am displaying "global posts" on one of my tabs. Currently, there are only 11 posts in the database:
In the app Some of the posts are being duplicated, and I have no idea why these SPECIFIC posts are being duplicated, as it seems to me like it is happening at random.
Here is the code for how I paginate the data.
When the component mounts, I query firestore and pull 5 posts using getCollection().
.
async componentDidMount() {
this.unsubscribe = Firebase.firestore()
.collection('globalPosts')
.orderBy("date_created", "desc")
.limit(5)
.onSnapshot(this.getCollection);
}
I get the posts successfully in getCollection(), and set an index, lastItemIndex, so I know where to query for the next posts
.
getCollection = (querySnapshot) => {
const globalPostsArray = [];
querySnapshot.forEach((res) => {
const {
..fields
} = res.data();
globalPostsArray.push({
..fields
});
});
this.setState({
globalPostsArray,
isLoading: false,
lastItemIndex: globalPostsArray.length - 1
});
}
This gets the first 5 items, no problem, ordered by date_created, descending.
If the user scrolls down the flatlist, I have logic in the flatlist to handle fetching more data:
<FlatList
data={this.state.globalPostsArray}
renderItem={renderItem}
keyExtractor={item => item.key}
contentContainerStyle={{ paddingBottom: 50 }}
showsHorizontalScrollIndicator={false}
showsVerticalScrollIndicator={false}
onRefresh={this._refresh}
refreshing={this.state.isLoading}
onEndReachedThreshold={0.5} <---------------------- Threshold
onEndReached={() => {this.getMore()}} <------------ Get more data
/>
Finally, once it is time to retrieve more data, I call this.getMore()
Here is the code to get the next 5 posts:
getMore = async() => {
const newPostsArray = [] <-------- new array for the next 5 posts
Firebase.firestore()
.collection('globalPosts')
.orderBy("date_created", "desc")
.startAfter(this.state.globalPostsArray[this.state.lastItemIndex].date_created) <--- note start after
.limit(5)
.onSnapshot(querySnapshot => {
querySnapshot.forEach((res) => {
const {
... same fields as getCollection()
} = res.data();
newPostsArray.push({
... same fields as getCollection()
});
});
this.setState({
globalPostsArray: this.state.globalPostsArray.concat(newPostsArray), <--- add to state array
lastItemIndex: this.state.globalPostsArray.length-1 <---- increment index
});
console.log(this.state.lastItemIndex) <------- I print out last item index
})
}
Some notes:
The code works fine in terms of fetching the data
The code works fine in terms of pagination, and only fetches 5 posts at a time
There is no discernible pattern I am seeing in which posts are being duplicated
I am ordering by date_created, descending when querying firestore in both getCollection() and getMore()
I console log "last item index" in my getMore(), and of course the index is higher than the number of posts
I keep getting the following warning/error, with different keys (post ID's in firestore), which shows me the duplication is happening at random, and not specific to one user. This warning/error doesn't break the application, but is telling me this weird behavior is happening:
Encountered two children with the same key, ZJu3FbhzOkXDM5mn6O6T. Keys should be unique so that components maintain their identity across updates. Non-unique keys may cause children to be duplicated and/or omitted — the behavior is unsupported and could change in a future version.
Can someone point me in the right direction, why my pagination is having such unusual behavior?
My issue was with lastItemIndex. Saving it in state was causing problems. I solved the problem by removing lastItemIndex from state, and making it a local variable in getMore():
getMore = async() => {
const newPostsArray = []
const lastItemIndex = this.state.globalPostsArray.length - 1 <---- added it here
await Firebase.firestore()
.collection('globalPosts')
.orderBy("date_created", "desc")
.startAfter(this.state.globalPostsArray[lastItemIndex].date_created)
.limit(5)
.onSnapshot(querySnapshot => {
querySnapshot.forEach((res) => {
const {
..fields
} = res.data();
newPostsArray.push({
key: res.id,
..fields
});
});
this.setState({
globalPostsArray: this.state.globalPostsArray.concat(newPostsArray)
});
})
}
You can only guarantee uniqueness on a document ID, not by the contents of a document's fields.
For example, as a workaround, you could concatenate two IDs into a single string and use it as the document ID. You would then use a transaction to ensure that a document does not already exist with that new composite ID before writing it.
In order to paginate through a list of these unique items you need to use .startAfter() and .limit(). You can then create a lastVisible variable that uses the last document in a batch to start a cursor for the next.
var lastVisible = documentSnapshots.docs[documentSnapshots.docs.length-1];
The example below can be found here if you want to read more about it:
var first = db.collection("cities")
.orderBy("population")
.limit(25);
return first.get().then(function (documentSnapshots) {
// Get the last visible document
var lastVisible = documentSnapshots.docs[documentSnapshots.docs.length-1];
console.log("last", lastVisible);
// Construct a new query starting at this document,
// get the next 25 cities.
var next = db.collection("cities")
.orderBy("population")
.startAfter(lastVisible) // <--- HERE
.limit(25);
});
Related
I'm new to the "async/await" aspect of JS and I'm trying to learn how it works.
The error I'm getting is Line 10 of the following code. I have created a firestore database and am trying to listen for and get a certain document from the Collection 'rooms'. I am trying to get the data from the doc 'joiner' and use that data to update the innerHTML of other elements.
// References and Variables
const db = firebase.firestore();
const roomRef = await db.collection('rooms');
const remoteNameDOM = document.getElementById('remoteName');
const chatNameDOM = document.getElementById('title');
let remoteUser;
// Snapshot Listener
roomRef.onSnapshot(snapshot => {
snapshot.docChanges().forEach(async change => {
if (roomId != null){
if (role == "creator"){
const usersInfo = await roomRef.doc(roomId).collection('userInfo');
usersInfo.doc('joiner').get().then(async (doc) => {
remoteUser = await doc.data().joinerName;
remoteNameDOM.innerHTML = `${remoteUser} (Other)`;
chatNameDOM.innerHTML = `Chatting with ${remoteUser}`;
})
}
}
})
})
})
However, I am getting the error:
Uncaught (in promise) TypeError: Cannot read property 'joinerName' of undefined
Similarly if I change the lines 10-12 to:
remoteUser = await doc.data();
remoteNameDOM.innerHTML = `${remoteUser.joinerName} (Other)`;
chatNameDOM.innerHTML = `Chatting with ${remoteUser.joinerName}`;
I get the same error.
My current understanding is that await will wait for the line/function to finish before moving forward, and so remoteUser shouldn't be null before trying to call it. I will mention that sometimes the code works fine, and the DOM elements are updated and there are no console errors.
My questions: Am I thinking about async/await calls incorrectly? Is this not how I should be getting documents from Firestore? And most importantly, why does it seem to work only sometimes?
Edit: Here are screenshots of the Firestore database as requested by #Dharmaraj. I appreciate the advice.
You are mixing the use of async/await and then(), which is not recommended. I propose below a solution based on Promise.all() which helps understanding the different arrays that are involved in the code. You can adapt it with async/await and a for-of loop as #Dharmaraj proposed.
roomRef.onSnapshot((snapshot) => {
// snapshot.docChanges() Returns an array of the documents changes since the last snapshot.
// you may check the type of the change. I guess you maybe don’t want to treat deletions
const promises = [];
snapshot.docChanges().forEach(docChange => {
// No need to use a roomId, you get the doc via docChange.doc
// see https://firebase.google.com/docs/reference/js/firebase.firestore.DocumentChange
if (role == "creator") { // It is not clear from where you get the value of role...
const joinerRef = docChange.doc.collection('userInfo').doc('joiner');
promises.push(joinerRef.get());
}
});
Promise.all(promises)
.then(docSnapshotArray => {
// docSnapshotArray is an Array of all the docSnapshots
// corresponding to all the joiner docs corresponding to all
// the rooms that changed when the listener was triggered
docSnapshotArray.forEach(docSnapshot => {
remoteUser = docSnapshot.data().joinerName;
remoteNameDOM.innerHTML = `${remoteUser} (Other)`;
chatNameDOM.innerHTML = `Chatting with ${remoteUser}`;
})
});
});
However, what is not clear to me is how you differentiate the different elements of the "first" snapshot (i.e. roomRef.onSnapshot((snapshot) => {...}))). If several rooms change, the snapshot.docChanges() Array will contain several changes and, at the end, you will overwrite the remoteNameDOM and chatNameDOM elements in the last loop.
Or you know upfront that this "first" snapshot will ALWAYS contain a single doc (because of the architecture of your app) and then you could simplify the code by just treating the first and unique element as follows:
roomRef.onSnapshot((snapshot) => {
const roomDoc = snapshot.docChanges()[0];
// ...
});
There are few mistakes in this:
db.collection() does not return a promise and hence await is not necessary there
forEach ignores promises so you can't actually use await inside of forEach. for-of is preferred in that case.
Please try the following code:
const db = firebase.firestore();
const roomRef = db.collection('rooms');
const remoteNameDOM = document.getElementById('remoteName');
const chatNameDOM = document.getElementById('title');
let remoteUser;
// Snapshot Listener
roomRef.onSnapshot(async (snapshot) => {
for (const change of snapshot.docChanges()) {
if (roomId != null){
if (role == "creator"){
const usersInfo = roomRef.doc(roomId).collection('userInfo').doc("joiner");
usersInfo.doc('joiner').get().then(async (doc) => {
remoteUser = doc.data().joinerName;
remoteNameDOM.innerHTML = `${remoteUser} (Other)`;
chatNameDOM.innerHTML = `Chatting with ${remoteUser}`;
})
}
}
}
})
I have about 650 products and each product has a lot of additional information relating to it being stored in metafields. I need all the metafield info to be stored in an array so I can filter through certain bits of info and display it to the user.
In order to get all the metafiled data, you need to make one API call per product using the product id like so: /admin/products/#productid#/metafields.json
So what I have done is got all the product ids then ran a 'for in loop' and made one call at a time. The problem is I run into a '429 error' because I end up making more than 2 requests per second. Is there any way to get around this like with some sort of queuing system?
let products = []
let requestOne = `/admin/products.json?page=1&limit=250`
let requestTwo = `/admin/products.json?page=2&limit=250`
let requestThree = `/admin/products.json?page=3&limit=250`
// let allProducts will return an array with all products
let allProducts
let allMetaFields = []
let merge
$(document).ready(function () {
axios
.all([
axios.get(`${requestOne}`),
axios.get(`${requestTwo}`),
axios.get(`${requestThree}`),
])
.then(
axios.spread((firstResponse, secondResponse, thirdResponse) => {
products.push(
firstResponse.data.products,
secondResponse.data.products,
thirdResponse.data.products
)
})
)
.then(() => {
// all 3 responses into one array
allProducts = [].concat.apply([], products)
})
.then(function () {
for (const element in allProducts) {
axios
.get(
`/admin/products/${allProducts[element].id}/metafields.json`
)
.then(function (response) {
let metafieldsResponse = response.data.metafields
allMetaFields.push(metafieldsResponse)
})
}
})
.then(function () {
console.log("allProducts: " + allProducts)
console.log("allProducts: " + allMetaFields)
})
.catch((error) => console.log(error))
})
When you hit 429 error, check for Retry-After header and wait for the number of seconds specified there.
You can also use X-Shopify-Shop-Api-Call-Limit header in each response to understand how many requests left until you exceed the bucket size limit.
See more details here: REST Admin API rate limits
By the way, you're using page-based pagination which is deprecated and will become unavailable soon.
Use cursor-based pagination instead.
This is my first project with Firebase, and I've created a relational data structure. Now I see why this isn't the best way to do things!
In this part of my app, users can add multiple items to an outfit - here's a diagram of the data structure/relationship I have in Firebase now.
I've included code from a Redux action creator in my React Native app. When a user edits an outfit - removing some items - this code:
takes an array with the new list of items from the client
compares this array with the current saved items server side
creates a new array of the diff (removed items)
loops through the outfits for each of those items, matching against the outfit being edited
removes references that match
This code works, but is pretty deeply nested and messy:
export const updateTagReferences = (localTags, outfitId) => {
//localTags represents the new set of items from application state
//outfitId is the uid for the outfit where those items appear
const {currentUser} = firebase.auth();
return dispatch => {
firebase
.database()
.ref(`users/${currentUser.uid}/outfits/${outfitId}/taggedItems`)
.once('value')
.then(snapshot => {
var serverTags = snapshot.val();
// Work out the diff (ie, which items have been removed locally)
return _.differenceWith(serverTags, localTags, _.isEqual);
})
.then(toDelete => {
toDelete.map(item => {
firebase
.database()
.ref(`users/${currentUser.uid}/items/${item.item.uid}/outfits`)
.once('value')
.then(snapshot => {
var outfits = snapshot.val();
for (var taggedItem in outfits) {
if (outfits.hasOwnProperty(taggedItem)) {
var i = outfits[taggedItem];
return i.uid === outfitId
? firebase
.database()
.ref(
`users/${currentUser.uid}/items/${item.item.uid}/outfits/${taggedItem}`,
)
.remove()
: null;
}
}
});
});
})
.then(console.log('Done'));
};
};
As you can see, I'm trying to use Firebase promises to loop through the outfits nested in each item server side.
My issues are:
I'm making lots of queries to Firebase, which isn't ideal
console.log('Done') fires before .remove() - I need to follow up with a second action once this action completes
I've tried the following code, based on this answer, but I can't seem to get it to work:
export const updateTagReferences = (localTags, outfitId) => {
const {currentUser} = firebase.auth();
return dispatch => {
// Create a ref for the outfit the user is editing
firebase
.database()
.ref(`users/${currentUser.uid}/outfits/${outfitId}/taggedItems`)
.once('value')
.then(snapshot => {
// Compare local (app state) with remote (Firebase) to work out which items have been removed
var serverTags = snapshot.val();
return _.differenceWith(serverTags, localTags, _.isEqual);
})
.then(toDelete => {
var promises = [];
// Create a ref for each item to delete
toDelete.map(item => {
firebase
.database()
.ref(`users/${currentUser.uid}/items/${item.item.uid}/outfits`)
.once('value')
.then(snapshot => {
var outfits = snapshot.val();
for (var taggedItem in outfits) {
// Loop through the outfits that item appears in
if (outfits.hasOwnProperty(taggedItem)) {
var i = outfits[taggedItem];
// Match against the outfit the user is editing
return i.uid === outfitId
? promises.push(
firebase
.database()
.ref(
`users/${currentUser.uid}/items/${item.item.uid}/outfits/${taggedItem}`,
)
// Remove the reference
.remove(),
)
: null;
}
}
})
// Execute all the promises, removing firebase references for each item removed locally
.then(Promise.all(promises).then(console.log('Done')));
});
});
};
};
I'm trying to make my code more efficient overall, so I can replicate it elsewhere in my project. I'd love an explanation of the best way to move this kind of query server side.
Currently, changing the whole data structure is out of scope (that's for next time!)
Thanks.
I'm trying to build a simple app that lets the user type a name of a movie in a search bar, and get a list of all the movies related to that name (from an external public API).
I have a problem with the actual state updating.
If a user will type "Star", the list will show just movies with "Sta". So if the user would like to see the actual list of "Star" movies, he'd need to type "Star " (with an extra char to update the previous state).
In other words, the search query is one char behind the State.
How should it be written in React Native?
state = {
query: "",
data: []
};
searchUpdate = e => {
let query = this.state.query;
this.setState({ query: e }, () => {
if (query.length > 2) {
this.searchQuery(query.toLowerCase());
}
});
};
searchQuery = async query => {
try {
const get = await fetch(`${API.URL}/?s=${query}&${API.KEY}`);
const get2 = await get.json();
const data = get2.Search; // .Search is to get the actual array from the json
this.setState({ data });
} catch (err) {
console.log(err);
}
};
You don't have to rely on state for the query, just get the value from the event in the change handler
searchUpdate = e => {
if(e.target.value.length > 2) {
this.searchQuery(e.target.value)
}
};
You could keep state updated as well if you need to in order to maintain the value of the input correctly, but you don't need it for the search.
However, to answer what you're problem is, you are getting the value of state.query from the previous state. The first line of your searchUpdate function is getting the value of your query from the current state, which doesn't yet contain the updated value that triggered the searchUpdate function.
I don't prefer to send api call every change of letters. You should send API just when user stop typing and this can achieved by debounce function from lodash
debounce-lodash
this is the best practise and best for user and server instead of sending 10 requests in long phases
the next thing You get the value from previous state you should do API call after changing state as
const changeStateQuery = query => {
this.setState({query}, () => {
//call api call after already changing state
})
}
I see that someone has given me a minus 1. I am a 55 year old mother who has no experience. I have many skills but this is not one of them. I am absolutely desperate and have bust myself to get this far. If you cannot help, I accept that, but please do not be negative towards me. I am now crying. Some encouragement would be much appreciated.
I have a page which displays items from a database on a repeater. The code searches the items using several drop down filters, which are populated from the database. Intermittently, seemingly randomly (no pattern is emerging despite extensive testing) the code is failing to populate random drop down filters (one or more of the drop down filters show the default settings rather than those self populated from the database). I discovered this by either repeatedly visiting the page or by repeatedly refreshing the page. Often the code works, then every 3 or 4 times, one or more of the drop down filters shows its default settings rather than those self populated from the database (then the next time it goes wrong, it might be the same or a different one or set of filters which do not work)
This is the code. On this page, there are 3 drop down filters but I have several pages like this, each displaying and searching a different database, with up to 10 drop down filters on each page, and they all have this intermittent problem...
import wixData from "wix-data";
$w.onReady(function () {
$w('#iTitle')
$w('#iCounty')
$w('#iGeog')
$w('#dataset1')
$w('#text102')
});
let lastFilterTitle;
let lastFilterCounty;
let lastFilterGeog;
export function iTitle_change(event, $w) {
filter($w('#iTitle').value, lastFilterCounty, lastFilterGeog);
}
export function iCounty_change(event, $w) {
filter(lastFilterTitle, $w('#iCounty').value, lastFilterGeog);
}
export function iGeog_change(event, $w) {
filter(lastFilterTitle, lastFilterCounty, $w('#iGeog').value);
}
function filter(title, county, geog) {
if (lastFilterTitle !== title || lastFilterCounty !== county || lastFilterGeog !== geog) {
let newFilter = wixData.filter();
if (title)
newFilter = newFilter.eq('title', title);
if (county)
newFilter = newFilter.eq('county', county);
if (geog)
newFilter = newFilter.eq('geog', geog);
$w('#dataset1').setFilter(newFilter)
.then(() => {
if ($w('#dataset1').getTotalCount() ===0) {
$w('#text102').show();
}
else {
$w('#text102').hide();
}
})
.catch((err) => {
console.log(err);
});
lastFilterTitle = title;
lastFilterCounty = county;
lastFilterGeog = geog;
}
}
// Run a query that returns all the items in the collection
wixData.query("Psychologists")
// Get the max possible results from the query
.limit(1000)
.ascending("title")
.distinct("title")
.then(results => {
let distinctList = buildOptions(results.items);
// unshift() is like push(), but it prepends an item at the beginning of an array
distinctList.unshift({ "value": '', "label": 'All Psychologists'});
//Call the function that builds the options list from the unique titles
$w("#iTitle").options = distinctList
});
function buildOptions(items) {
return items.map(curr => {
//Use the map method to build the options list in the format {label:uniqueTitle, valueuniqueTitle}
return { label: curr, value: curr };
})
}
// Run a query that returns all the items in the collection
wixData.query("Psychologists")
// Get the max possible results from the query
.limit(1000)
.ascending("county")
.distinct("county")
.then(results => {
let distinctList = buildOptions(results.items);
// unshift() is like push(), but it prepends an item at the beginning of an array
distinctList.unshift({ "value": '', "label": 'All Counties'});
//Call the function that builds the options list from the unique titles
$w("#iCounty").options = distinctList
});
function buildOptions1(items) {
return items.map(curr => {
//Use the map method to build the options list in the format {label:uniqueTitle1, valueuniqueTitle1}
return { label: curr, value: curr };
})
}
// Run a query that returns all the items in the collection
wixData.query("Psychologists")
// Get the max possible results from the query
.limit(1000)
.ascending("geog")
.distinct("geog")
.then(results => {
let distinctList = buildOptions(results.items);
// unshift() is like push(), but it prepends an item at the beginning of an array
distinctList.unshift({ "value": '', "label": 'All Regions'});
//Call the function that builds the options list from the unique titles
$w("#iGeog").options = distinctList
});
function buildOptions2(items) {
return items.map(curr => {
//Use the map method to build the options list in the format {label:uniqueTitle2, valueuniqueTitle2}
return { label: curr, value: curr };
})
}
export function button45_click(event, $w) {
//Add your code for this event here:
filter($w('#iTitle').value='', $w('#iCounty').value='', $w('#iGeog').value='');
}
My experience and knowledge is very limited, so the answer may well be very simple. Any help would be much appreciated as I will have to abandon my project if I can't find a solution.Thank you