I recently ran into an issue with firebase (onSnapshot) realtime updates. The problem is that onSnapshot updates the state whenever a document is (created, deleted, updated) which overrides the state.
In other words, let's say I have a variable called state.
let state = null;
// And when I visit (**/homepage**) ,,,, onSnapshot runs.
firebase.collection(someCollection).onSnapshot((snapshot) => {
state = (we save documents we got from collection in state variable);
})
// I display these documents on the /homepage.
// Now, I click and call a function (orderByTitle)
// this function gets docs from firebase ... ordered by title.
async function orderByTitle(){
let docs = await firebase.collection(someCollection).orderBy("title").get();
state = docs; // (it overrides the "state" with docs oredered by title & display on page.
}
// Now, I delete one of the docs.
// The problem starts here as it triggers (onSnapshot) again
// and my ("state" variable) gets override with "unordered docs" again.
So, my question is how you prevent (onSnapshot) from overriding your current state or do you manage two different states? And if you manage two different states then how you remove the current elements from the DOM which are using old state and you force them to use other state.
The .onSnapshot function will update the data each time there's a change in the database which is why state keeps changing.
It appears that you want to read the data once. From the docs
You can listen to a document with the onSnapshot() method. An initial
call using the callback you provide creates a document snapshot
immediately with the current contents of the single document. Then,
each time the contents change, another call updates the document
snapshot
on the other hand, if you want to read data once and not have it continually update use the .get function. Here's an example of getting all documents from a collection once, with no further notifications.
db.collection(someCollection).get().then(function(querySnapshot) {
querySnapshot.forEach(function(doc) {
// doc.data() is never undefined for query doc snapshots
console.log(doc.id, " => ", doc.data());
});
});
More reading can be found in the documentation Get Data Once
Related
I am creating an easy chat app, with different text channels. I am facing an infinite loop issue when using the useEffect hook to update the messagesList on real time. You can see the code below but this is what I am trying to achieve:
First useEffect is for the chat window to scroll to the last message every time there is a change in the messagesList array. This means: I am in the middle of the messages window, I write a new message and it takes me to the bottom. This is working fine.
Second useEffect is for the messagesList to be rendered whenever the channel is changed or there is any change in the messagesList. Adding the messagesList as a dependency is causing the infinite loop... but I think I need it cause otherwise the following happens: user1 is inside the chat channel and user2 writes a new message. User1 wont see the new message displayed as his chat is not being re-rendered. How would you make it for the new message to be displayed for user1?
Sorry for the confusing question and thanks a lot in advance!
useEffect(() => {
anchor.current.scrollIntoView(false);
}, [messagesList]);
useEffect(() => {
const unsubscribe = onSnapshot(
collection(firestore, `channels/${activChannel}/messages`),
(snapshot) => {
console.log(snapshot.docs);
getMessagesList();
}
);
return () => unsubscribe();
}, [activChannel, messagesList]);
I am not familiar with firestore, but perhaps you could tie the updating of the messages to the event that an user submits his message or use useSyncExternalStore. This piece of documentation on useEffect use cases might help you.
an excerpt from the docs:
Here, the component subscribes to an external data store (in this
case, the browser navigator.onLine API). Since this API does not exist
on the server (so it can’t be used to generate the initial HTML),
initially the state is set to true. Whenever the value of that data
store changes in the browser, the component updates its state.
Although it’s common to use Effects for this, React has a
purpose-built Hook for subscribing to an external store that is
preferred instead. Delete the Effect and replace it with a call to
useSyncExternalStore:
I am trying to write a simple multiplayer game involving a board of tiles. I need real time updates so that all players see all tile updates when they happen. This is my first time using Firestore or building a game with real time updates. I am using React with React Hooks to build my game.
const [tiles, setTiles] = useState([]);
Tiles is an array of 25 objects. The logic for starting a new game and initial setting of new batch of tiles is elsewhere, and I don't think relevant for the problem.
On Firestore, I have a games collection. Each games document includes the tiles array.
When first loaded, we use useEffect with an empty dependency array to subscribe to the onSnapshot listener for the game, and update our local state when snapshots are received.
I used the example from the documentation to attempt to only update the local state when the updates are from the server: https://firebase.google.com/docs/firestore/query-data/listen?authuser=0#events-local-changes
useEffect(() => {
return db
.collection('games')
.doc(code)
.onSnapshot((doc) => {
var source = doc.metadata.hasPendingWrites ? 'Local' : 'Server';
if (source !== 'Local') {
console.log('Game snapshot!');
// get the tiles from the database and update local state to reflect
setTiles(doc.data().tiles);
}
})
}, []
)
When turns happen, tiles are clicked and then new state of the tiles is set after the click:
const onTileClick = (tile, index) => {
// ...
// perform some logic, calculate value of new_tiles
// ...
// Then update tiles in local state
setTiles(new_tiles);
}
And whenever the tiles change, we again use useEffect() to monitor for tile changes and update the Firestore game.
useEffect(() => {
return db
.collection('games')
.doc(code)
// send the new tiles to the server
.update({tiles: tiles})
.then((res) => {
console.log('Tiles updated!');
})
.catch((res) => {
console.log('Error updating tiles!', res);
});
}, [tiles]);
The problem, as you can probably see, is that whenever I click a tile, it starts an infinite loop of setting tiles, triggering a snapshot, setting the tiles again.. etc...
This is only happening when I have two browser windows open (one in incognito mode to mimic two players), I think what's happening is, when the tiles are set from one player's window, it triggers the snapshot for the other player, which sets their tiles, which triggers a snapshot for the other player, setting their tiles ...etc...
Clearly I am not using React Hooks in the best way here. Does anyone have any better suggestions of how to manage React state with a Firestore snapshot? Or do I just need to be more granular in the onSnapshot listener to check for actual differences before updating React state? The problem is, there are going to be multiple fields once I've got a bit further, not just 'tiles'... so more differences to check for.
Thanks in advance.
Consider implementing some way to determine if setTiles should actually be called in your snapshot callback. Right now you are checking doc.metadata.hasPendingWrites, which doesn't tell you for certain if something actually changed. It just tells you if the server sent you an update. Consider writing some logic to compare the current and prior contents of the document to see if something actually changed that needs rendering, and only call setTiles if that's the case. That might stop the ping-ponging of data between the users.
I'm currently creating a to-do list within React which retrieves tasks from Firestore and stores them locally within an array using state hooks: const [todoList, setTodoList] = useState([]). I've run into some roadblocks while coding this mainly because Firestore's onSnapshot function doesn't seem to play properly with React. The snapshot code is called on load (to retrieve existing tasks) and when a new task is created. The snapshot code for appending changes to an array is:
todoReference.onSnapshot(colSnapshot => {
colSnapshot.docChanges().forEach(change => {
if (change.type === 'added') {
const taskData = change.doc.data();
todoList.push(taskData);
}
});
setTodoList(todoList); // "update state" using new array
});
There are a few issues which pop-up when I try different variations of this (pushing to empty array and then concatenating the two arrays together, etc.):
The todo list state doesn't persist on new snapshot. For example, creating a new task task2 updates the todoList to [task2], but creating another task task3 after that makes the first task disappear and updates the array to [task3] instead of [task2, task3].
onSnapshot keeps retrieving the same tasks despite them being previously retrieved. For example, on load the initial todoList is updated to [task1, task2, task3]. When creating a new task and calling the snapshot function again, I expect the todoList to be updated to [task1, task2, task3, task4]. However, instead I'm returned some variation of [task1, task2, task3, task1, task2, task3, task4] which compounds whenever the snapshot function is called again.
This issue seems to only happen in React and not native JavaScript (the tasks are created and retrieved just fine without any duplicates). Some solutions I have tried is wrapping everything within a onEffect (which I believe gave me the first problem if I didn't pass todoList as a dependency, and if I did would infinitely loop) and calling the snapshot function via unsubscribe() (which still gave me the second problem).
Solved! I nested everything within a useEffect with no dependencies and managed to resolve the first bullet-point regarding states not updating properly. Instead of setting state normally using setTodoList(todoList.concat(newTasks)), I functionally set the state using setTodoList(currentList => currentList.concat(newTasks)) (something about setState being asynchronous about useState, async isn't my specicalty). Find the answer here: https://stackoverflow.com/a/56655187/9477642.
Here's the final snapshot update code (I somehow also resolved onSnapshot returning the entire collection instead of just changes each time but I forgot how, I'll update this if I remember why):
useEffect(() => {
let unsubscribe = todoReference.onSnapshot(colSnapshot => {
console.log(colSnapshot.docChanges());
let newTasks = [];
colSnapshot.docChanges().forEach(change => {
if (change.type === 'added') {
const taskData = change.doc.data();
newTasks.push(taskData);
}
});
setTodoList(currentList => currentList.concat(newTasks));
});
return () => unsubscribe();
}, []);
Scenario
I have documents stored for each user at path documents/${documentId}
Goal
I want to parse them and update the index for that document when it changes
Code
import Functions from 'firebase-functions'
export writeTrigger = Functions
.database
.ref('/document/{documentId}')
.onWrite(
async event => {
const data = event.data.val()
const { documentId } = event.params
// assume that updateIndex function exists
updateIndex(documentId, data)
}
)
Problem
This function gets called for every single letter being typed into the document
TLDR
What is the best way to throttle/debounce firebase cloud functions (database.onWrite) so that it isn't fired on each and every change?
Your function will get invoked for each and every change at or under the path you specify. There's currently no way to prevent this.
Instead of writing each and every change to the database, instead try batching up changes on the client and writing them out in bulk, or saving state periodically.
Alternatively, give the client some other way to indicate that it's time for the function to do work, maybe some field in the document and listen to only that field's changes. Here's one that just triggers when a field done is changed:
export writeTrigger = Functions
.database
.ref('/document/{documentId}/done')
.onWrite(...)
Just be sure to unset that value so that the client can indicate another set of changes should be processed.
Is it possible to register a Firebase listener function without calling it when you register it?
For example:
this.gamestateURL.on('value', function(snapshot){
self.GameStateChangeEvent(snapshot);
});
GameStateChangeEvent function fires immediately upon setting up the listener.
Thank you.
Unfortunately, no. The docs specifically state:
This event will trigger once with the initial data stored at this location, and then trigger again each time the data changes. The DataSnapshot passed to the callback will be for the location at which on() was called. It won't trigger until the entire contents has been synchronized. If the location has no data, it will be triggered with an empty DataSnapshot (val() will return null).
You could, however do something like this:
var ref = this.gamestateURL // or however you create a ref
function doSomethingWithAddedOrChangedSnapshot(snapshot) {
// this function is called whenever a child is added
// or changed
}
// assuming you have "timestamp" property on these objects
// it will not be called back with any snapshots on initialization
// because the timestamp of existing snapshots will not be greater
// than the current time
ref.orderByChild('timestamp')
.startAt(new Date().getTime())
.on('child_added', doSomethingWithAddedOrChangedSnapshot);
ref.on('child_changed', doSomethingWithAddedOrChangedSnapshot);
ref.once('value', function(snapshot){
// get the initial state once
// this snapshot represents all the items on this ref
// this will only fire once on initialization
initializeGameData(snapshot.val());
});
In English:
Create one function that handles the updated/added child
start listening to the child_added event for all children added after the current timestamp (unix time since epoch). This also assumes you're storing the timestamp on these children.
start listening to the child_changed event for any child that is changed.
grab all the values of the ref once to initialize your data.
not sure if your use case needs to handle 'child_removed' or 'child_moved'