Getting frustrated to solve this since I am no JS expert. 😢
I am using Firestore as a database and VuexFire to bind the data to VueX state, like so.
getLeads: firestoreAction(async ({
bindFirestoreRef
}) => {
// return the promise returned by `bindFirestoreRef`
return bindFirestoreRef('leads', db.collection('leads').orderBy('updated.date', 'desc').limit(30))
}),
It gets the first 30 results and then i want to implement an infinite scroll feature to run a function every time the scroll reaches the bottom and fetch more data and bind to the same state. In Firestore pagination require passing a query cursor of the last fetched document as a reference
Below from firebase document, with vanilla JS
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)
.limit(25);
});
since I use VuexFire to bind the data to state, I dont see an option to get the snapshot of the last document fetched by VuexFire (lastVisible from the above code), in order to pass it to the next query.
Any help will be highly appreciated. 🙏🏽
Lets say I have a collection of Customer records and i am displaying the first 5 ordered by last updated.
The query is
getLeads: firestoreAction(({ commit, bindFirestoreRef
}) => {
bindFirestoreRef('Leads', db.collection('leads')
.orderBy('updated.date', 'desc').limit(5)).then(documents => {
commit('POPULATE_TESTLEADS', documents);
commit('LAST_DOC', documents[documents.length - 1]);
});
}),
I am saving both the results and the lastdoc in the state, looping and showing the names, like so:
Nakheel
Emaar Group
Yapi Kredi Inc
Cairo Amman Bank
Arab Jordan Investment Bank LLC
I then call again with the last doc as query cursor and expect the next 5 docs to return from firebase, like so
moreLeadLeads: firestoreAction(({ state, bindFirestoreRef
}) => {
bindFirestoreRef('testLeads', db.collection('leads')
.orderBy('updated.date', 'desc')
.startAfter(state.lastDoc).limit(5))
}),
But I get the same 5 results as above from firestore. What am I doing wrong? :(
Internally VueFire and VuexFire use a serializer function that maps each Document returned by RTDB or Firestore into the data objects that are bound to the final component or Vuex store state.
The default serializer is implemented by the function createSnapshot that is part of the vuefire-core library:
/**
* #param {firebase.firestore.DocumentSnapshot} doc
* #return {DocumentData}
*/
export function createSnapshot (doc) {
// defaults everything to false, so no need to set
return Object.defineProperty(doc.data(), 'id', {
value: doc.id
})
}
As you can see it returns only doc.data() (with id added) and discards the doc object. However when implementing Firestore pagination via query.startAfter(doc) we need the original doc object. The good news is that VueFire and VuexFire APIs allow us to replace the serializer with our own that can preserve the doc object like so:
const serialize = (doc: firestore.DocumentSnapshot) => {
const data = doc.data();
Object.defineProperty(data, 'id', { value: doc.id });
Object.defineProperty(data, '_doc', { value: doc });
return data;
}
We can configure our new VuexFire serializer either globally via plugin options or per binding via binding options.
// Globally defined
Vue.use(firestorePlugin, { serialize });
// OR per binding
bindFirebaseRef('todos', db.ref('todos'), { serialize } )
For VuexFire, we can now get access to the first document as state.todos[0]._doc or last document state.todos[state.todos.length-1]._doc and use them to implement pagination queries for collections or "get next" & "get previous" queries that bind single documents (essential when your base query has multi-field sorting).
NOTE: Because _doc and id are non-enumerable properties, they won't appear on component or store objects in Vue DevTools.
From the VueFire documentation on binding data and using it, the $bind method (which I assume your bindFirestoreRef wraps) returns a promise with the result (as well as binding it to this). From that documentation:
this.$bind('documents', documents.where('creator', '==', this.id)).then(documents => {
// documents will point to the same property declared in data:
// this.documents === documents
})
So you should be able to do the same, and then get the document from the array with something like:
bindFirestoreRef('leads', db.collection('leads').orderBy('updated.date', 'desc').limit(30)).then(documents => {
this.lastDoc = documents[documents.length - 1];
})
Related
I trying to store data content reference type by batch transaction, then I got an exception:
Function WriteBatch.set() called with invalid data. Unsupported field value: a custom object (found in document orders/OC9dZErupEhPsamp8QEd)
Is there a way we can use batch transaction to store reference type?
this is my code:
batch.update(orderRef, {
userId: firestore.doc(userId),
});
Normaly update() use to update existing firestore data. Review firestore docs for the same. In that given example they are updating population by increments of value or with new population number but before passing it in each update function values are stored in one cost value if it is not static value. as Asked by #dharmaraj please edit your questions by posting with full code you can also read given firestore documentation for your own studies.
import firebase from "firebase/app";
const app = firebase.initializeApp({});
const firestore = app.firestore();
const batch = firestore.batch();
const newUserId = firestore.doc(userId);
batch.update(orderRef, {
userId: newUserId,
});
Log newUserId value and see what are you getting into it.
You can't store the reference object that doc() returns, it's an object that may have circular references and functions in it. doc() is not the id of the document. If you want to get the id (which is a string), then:
const newUserId = firestore.doc(userId).ref.id;
batch.update(orderRef, {
userId: newUserId,
});
I don't know why batch validate input should be a pure object. I tried to push reference type id inside nested object then it work well, yeah I know it already is a trick, but it work.
change:
batch.update(docRef, {
user: firestore.collection('users').doc(userId)
})
to:
batch.update(docRef, {
user: {
id: firestore.collection('users').doc(userId)
}
})
I'm trying to fetch real time data from Cloud Firestore using the below code.
export const getRealTimeData = () =>
db
.collection('posts')
.onSnapshot(
(querySnapshot) => {
const posts: any = [];
querySnapshot.forEach((doc) =>
posts.push(Object.assign({
id: doc.id
}, doc.data()))
);
},
);
};
And, I want to use the resultant array to display the data on UI. When I'm doing this, the resultant array is a function but not the actual array of data.
const posts = getRealTimeData();
Here's what I get when I log posts
function () {
i.kT(), o.al(s);
}
Could anyone please point where I went wrong?
Realtime listeners added with onSnapshot() are not compatible with returning values from function calls. That's because they continue to generate new results over time, and would never really "return" anything once. You should abandon the idea of making a synhronous getter type function in this case - they just don't work for what you're trying to do.
Ideally, you would use an architecture like Redux to manage the updates as they become available. Your realtime listener would dispatch query updates to a store, and your component would subscribe to that store that to receive those updates.
If you don't want to use Redux (which is too bad - you really should for this sort of thing), then you should wrap your query inside a useEffect hook, then have your listener set a state hook variable so your component can receive the updates.
Scenario
Using the Express framework to modify a MongoDB I am able to add documents to a collection named faculties. Also, the dropdownbox displaying the property name of the documents in collection faculties, when I can inspect it after the post method is completed. However, when I pass the state, and retrieve the faculties collection from it with: const { faculties } = state; and then inspect the properties name of the array of documents of collection faculties, It doesn't show the most recent addition to the database. (I am looking for the property 'id' of the newly inserted faculty document with name exampleFaculty to implement many-to-many relationships).
Attempts
I tried applying a forceUpdate() to update the state, however from what I understood, this updates the render instead of the state. Nevertheless the render is updated but the state is not. This is the implementation:
this.forceUpdate();
axios.post('http://localhost:3001/api/putFaculty', {name: message});
this.forceUpdate();
ManyToManyDbMain.Main(message,collectionName,this.state) // implement many to many relationships in db
I tried this.setState(this.state); to update the state:
this.forceUpdate();
axios.post('http://localhost:3001/api/putFaculty', {name: message});
this.forceUpdate();
this.setState(this.state);
ManyToManyDbMain.Main(message,collectionName,this.state) // implement many to many relationships in db
I tried directly calling the fetch method to update the state:
this.forceUpdate();
axios.post('http://localhost:3001/api/putFaculty', {name: message});
this.forceUpdate();
this.getFaculties()
this.setState(this.state);
ManyToManyDbMain.Main(message,collectionName,this.state) // implement many to many relationships in db
Where the fetch method consists of:
// read the mongodb collection faculties in database "education"
getFaculties= () => {
fetch('http://localhost:3001/api/getFaculties')
.then((data) => data.json())
//set property "faculties" within the state" (won't throw error if you haven't defined property "faculties" within state".
.then((res) => this.setState({ faculties: res.data }));
};
Using the response of the axiom.post(..) method to read the new faculties from that:
axios.post('http://localhost:3001/api/putFaculty', {name: message})
.then(response => {
const {faculties} = this.state;
alert(response.data)
alert(response.name)
faculties.push(response.data);
this.setState({faculties});
})
But that returns an object Object and I do not yet know how to read the properties of that object, I tried mapping it into a .name property with: response.map((dat) => dat.name) but the response had no function named map.
Since the state gets automatically updated every second with:
this.getFaculties();
if (!this.state.intervalIsSet) {
let interval = setInterval(this.getFaculties, 1000);
this.setState({ intervalIsSet: interval });
}
I tried adding a sleep method of 5 seconds with the false expectation that the state would automatically be updated before the state is passed to the ManyToManyDbMain.Main() method. Instead the website/code is frozen for 7 seconds, (meaning the state does not get updated either), before the code continues.
Question
How can I update the state after the axios.post(), so that the latest document of collection faculties is included in the state that is passed to the ManyToManyDbMain.Main(..)?
Ignored shortcuts/XY-problem solutions:
The following solutions to the xy-problem are acknowledged but not persued.
I could manually generate all the id's for all documents, and just push them to the database, that way I have the Id's in the App.js when I need them. However, the reason to implement many-to-many relationships in the DB, is because it is designed to be a small database used (potentially) by many users, hence I want to reduce computation time (applying search queries to find subset id's) at the cost of a bigger database. That also means I don't want to create a separate system in the website that manages all the created Id's and prevents double Id's when MongoDb already has an optimised Id allocation system.
I could add a second loop every n-milliseconds that checks whether some boolean indicates whether a new faculty is added, and launch a "get Id method" from there. But I think that makes the code unnecessarily complex, difficult to test, potentially unreliable with multiple users, and computationally intensive, which I am trying to avoid.
You can use the axios .then() method to set the component's state.
for example -
axios.get('http://localhost:3001/api/getFaculties')
.then(res => {
/** Console here to get an actual response from server */
console.log(res);
this.setState({
faculties: res.data.results
});
})
.catch(function(error) {
console.log(error);
});
I have a property on my service class as so:
articles: Observable<Article[]>;
It is populated by a getArticles() function using the standard http.get().map() solution.
How can I manually push a new article in to this array; One that is not yet persisted and so not part of the http get?
My scenario is, you create a new Article, and before it is saved I would like the Article[] array to have this new one pushed to it so it shows up in my list of articles.
Further more, This service is shared between 2 components, If component A consumes the service using ng OnInit() and binds the result to a repeating section *ngFor, will updating the service array from component B simultaneously update the results in components A's ngFor section? Or must I update the view manually?
Many Thanks,
Simon
As you said in comments, I'd use a Subject.
The advantage of keeping articles observable rather than storing as an array is that http takes time, so you can subscribe and wait for results. Plus both components get any updates.
// Mock http
const http = {
get: (url) => Rx.Observable.of(['article1', 'article2'])
}
const articles = new Rx.Subject();
const fetch = () => {
return http.get('myUrl').map(x => x).do(data => articles.next(data))
}
const add = (article) => {
articles.take(1).subscribe(current => {
current.push(article);
articles.next(current);
})
}
// Subscribe to
articles.subscribe(console.log)
// Action
fetch().subscribe(
add('article3')
)
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/5.5.2/Rx.js"></script>
Instead of storing the whole observable, you probably want to just store the article array, like
articles: Article[]
fetch() {
this.get(url).map(...).subscribe(articles => this.articles)
}
Then you can manipulate the articles list using standard array manipulation methods.
If you store the observable, it will re-run the http call every time you subscribe to it (or render it using | async) which is definitely not what you want.
But for the sake of completeness: if you do have an Observable of an array you want to add items to, you could use the map operator on it to add a specified item to it, e.g.
observable.map(previousArray => previousArray.concat(itemtToBeAdded))
ex from angular 4 book ng-book
Subject<Array<String>> example = new Subject<Array<String>>();
push(newvalue:String):void
{
example.next((currentarray:String[]) : String[] => {
return currentarray.concat(newValue);
})
}
what the following says in example.next is take the current array value Stored in the observable and concat a new value onto it and emit the new array value to subscribers. It is a lambda expression.I think this only works with subject observables because they hold unto the last value stored in their method subject.getValue();
I am using Firebase and Node with Redux. I am loading all objects from a key as follows.
firebaseDb.child('invites').on('child_added', snapshot => {
})
The idea behind this method is that we get a payload from the database and only use one action to updated my local data stores via the Reducers.
Next, I need to listen for any NEW or UPDATED children of the key invite.
The problem now, however, is that the child_added event triggers for all existing keys, as well as newly added ones. I do not want this behaviour, I only require new keys, as I have the existing data retrieved.
I am aware that child_added is typically used for this type of operation, however, i wish to reduce the number of actions fired, and renders triggered as a result.
What would be the best pattern to achieve this goal?
Thanks,
Although the limit method is pretty good and efficient, but you still need to add a check to the child_added for the last item that will be grabbed. Also I don't know if it's still the case, but you might get "old" events from previously deleted items, so you might need to watch at for this too.
Other solutions would be to either:
Use a boolean that will prevent old added objects to call the callback
let newItems = false
firebaseDb.child('invites').on('child_added', snapshot => {
if (!newItems) { return }
// do
})
firebaseDb.child('invites').once('value', () => {
newItems = true
})
The disadvantage of this method is that it would imply getting events that will do nothing but still if you have a big initial list might be problematic.
Or if you have a timestamp on your invites, do something like
firebaseDb.child('invites')
.orderByChild('timestamp')
.startAt(Date.now())
.on('child_added', snapshot => {
// do
})
I have solved the problem using the following method.
firebaseDb.child('invites').limitToLast(1).on('child_added', cb)
firebaseDb.child('invites').on('child_changed', cb)
limitToLast(1) gets the last child object of invites, and then listens for any new ones, passing a snapshot object to the cb callback.
child_changed listens for any child update to invites, passing a snapshot to the cb
I solved this by ignoring child_added all together, and using just child_changed. The way I did this was to perform an update() on any items i needed to handle after pushing them to the database. This solution will depend on your needs, but one example is to update a timestamp key whenever you want the event triggered. For example:
var newObj = { ... }
// push the new item with no events
fb.push(newObj)
// update a timestamp key on the item to trigger child_changed
fb.update({ updated: yourTimeStamp })
there was also another solution:
get the number of children and extract that value:
and it's working.
var ref = firebaseDb.child('invites')
ref.once('value').then((dataSnapshot) => {
return dataSnapshot.numChildren()
}).then((count) =>{
ref .on('child_added', (child) => {
if(count>0){
count--
return
}
console.log("child really added")
});
});
If your document keys are time based (unix epoch, ISO8601 or the firebase 'push' keys), this approach, similar to the second approach #balthazar proposed, worked well for us:
const maxDataPoints = 100;
const ref = firebase.database().ref("someKey").orderByKey();
// load the initial data, up to whatever max rows we want
const initialData = await ref.limitToLast(maxDataPoints).once("value")
// get the last key of the data we retrieved
const lastDataPoint = initialDataTimebasedKeys.length > 0 ? initialDataTimebasedKeys[initialDataTimebasedKeys.length - 1].toString() : "0"
// start listening for additions past this point...
// this works because we're fetching ordered by key
// and the key is timebased
const subscriptionRef = ref.startAt(lastDataPoint + "0");
const listener = subscriptionRef.on("child_added", async (snapshot) => {
// do something here
});