Firebase 'child_changed' listener understanding - javascript

I am trying to grab real-time changes from Firebase on the status of a question in the lifecycle of my system. My child_changed event is interacting differently than I was expecting, so I am not sure if I am using it right.
I want to grab the new change to the questionStatus variable, so that if another user on the system changes the variable on the database, the updates will be seen real time for both users. My Firebase DB is setup like so:
-/questions
-/ID1234567890
-/title
-/messages
-/ID1234567890
-/ID2345678901
'child_changed' call:
fire.database().ref('/questions/' + this.state.questionId + '/questionStatus').on('child_changed', snap => {
this.setState({
questionStatus: (snap.val().questionStatus),
})
console.log("CHANGED!")
})
The above code is how I think it should work, but I can never get the console.log to display as intended. By just removing questionStatus from the ref() (as outlined in the code below), the event triggers whenever I add a new message to the question (for chat), and seems to be causing a loading error. Am I misunderstanding firebase or is deeper in my code? Maybe this has to with lifecycle of component? I have tried placing it everywhere to no avail, but currently have it in componentWillMount(). Let me know what you think, thanks!
fire.database().ref('/questions/' + this.state.questionId).on('child_changed', snap => {
this.setState({
questionStatus: (snap.val().questionStatus),
})
console.log("CHANGED!")
})

Based on my research, I didn't full understand setState() fully, not knowing that the state can only be updated async, not sync. Knowing this, I just implemented a feature informing the user that the status has changed and to click a button to reload. Not ideal, but it will work for now.
I also tested to exclude children with a specific key (messages).
So my new code looks like this:
fire.database().ref('/questions/' + this.state.questionId).on('child_changed', snap => {
// Ignore if messages are being added to the ticket
if(snap.key !== 'messages'){
this.setState({
questionStatus: (snap.val().questionStatus),
questionTags: (snap.val().questionTags),
question
})
}
})

Related

useEffect for real time data update

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:

PrimeVue Datable not updating

Im pretty new to Vue and JS, so Im having a hard time understanding how it all works. Im coming from Swift and maybe that is the problem.
Im trying to get a Primevue datable to update its values when changing out the values in a ref connected to it. But nothing happens on screen, however the console.log file shows the new data.
//Vue composition api setup()
const { flights } = getCollection("Jobs", shift.value.email, fullDay + platform.value.name)// Firebase Listener
//This function is triggered by some select buttons which changes the shift.value and platform.value so to listen for data in a different collection
const loadFlights = () => {
let {flights: fl} = getCollection("Jobs", shift.value.email, fullDay + platform.value.name)
flights.value = fl.value
console.log('flights ref:', fl, ) // shows new the data, but the datable does not.
}
If Im not clicking to change to a different collection the datable updates the current collection fine. Is there some sort of "tableview.reload()" fuctionality here Im missing?
Solved it. I used watch from vue instead which triggers everytime the select value changes.
I then stored the response in a reactive parameter instead of a ref. And now it works like it should. Cant tell you why.
https://youtu.be/Qypxpm-llnQ
Program with Erik most likely can.

Memory leak/infinite loop issue with multiplayer game: React State and Firestore snapshot listener

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.

Can Relay work properly with Next.js SSR?

I have started learning Relay by writing a new Next.js application. I have so far been following the with-relay-modern example in the Next.js repo, and it has been working just fine for fetching data from the server. However, I have now moved beyond that example by adding a mutation, and things immediately stopped working.
The mutation updater looks like this:
function updateStore(
store: RecordSourceSelectorProxy,
formInstanceUuid: string,
) {
const mutation = store.getRootField("updateFormValue");
const newFormValue = mutation?.getLinkedRecord("formValue");
if (!newFormValue) {
throw "Expected new form value from server";
}
const localFormInstance = store.get(formInstanceUuid)
const localValueRecords = localFormInstance?.getLinkedRecords("values") || [];
for (const record of localValueRecords) {
if (record.getValue("partUuid") == newFormValue.getValue("partUuid")) {
console.log("Copying fields from server provided value to local value");
record.copyFieldsFrom(newFormValue);
}
}
console.debug("New state of store:", initEnvironment().getStore().getSource())
}
All it does is inject a "value" object returned from the server into a list of value objects in a local "form" object. As you can see I am dumping the state of the store on the last line, so I can confirm that the mutation worked as expected, and the local value was modified as expected.
However, the UI doesn't refresh. I have to reload the window to see the new state.
I can't for the life of me figure out what I've done wrong, so I'm starting to wonder if the example I was following only works for fetching data. I assume it's the QueryRenderer object that is responsible for refreshing the UI when the underlying store changes, and the example doesn't use one. I also can't imagine how a QueryRenderer could be added to the example without ruining SSR.
TL;DR
Does the "with-relay-modern" example work when adding mutations, or is my issue somewhere else?
I also started a Discussion on the Next.js GitHub page about this

React state gets "corrupted"

I am working on a simple React.JS frontend piece.
I essentially have a browsing SPA for historical data. The design has a bunch of filters that I need to populate one at a time, starting the top one in my logic hierarchy.
I do something like:
componentWillMount() {
if (!this.props.advertisers) {
this.props.loadAdvertisers();
}
}
Once advertisers array has been loaded, I map it to options of a select component (using react-select), set the selection to the first item in the list and load the next list - campaigns.
As far as I understand, the best way to do this is still componentWillReceiveProps() and I am a little perplexed how this should be done differently, given that componentWillReceiveProps is being phased out.
My code looks like:
componentWillReceiveProps(nextProps) {
if (nextProps.advertisers && !this.props.advertisers) {
const advertiser = nextProps.advertisers[0];
this.setState({
advertiser: {
value: advertiser['id'],
label: advertiser['name']
}
});
nextProps.loadCampaigns(advertiser['id']);
}
// if campaigns list changed, reload the next filtered array
if (nextProps.campaigns && this.props.campaigns !== nextProps.campaigns) {
...
}
This worked fine, until I decided to add a loading indicator. I mapped state's loading property e.g. for campaigns it gets exposed via this.props.campaignsLoading then do:
return (this.props.campaignsLoading || this.props...Loading || ...) ?
<ProgressIndicator> : <MainContentPanel>
The problem is now, my state does not get set correctly inside componentWillReceiveProps().
The project is using #rematch and I initially tried this with #rematch/loading plugin and when the problem happened, thought the plugin does it wrong, somehow. Then, I mapped loading properties manually, and just added two more dispatches to manually set the loading flag.
All the props are being set/unset correctly, but my state is not being set and nothing works. Any suggestions?
When you do
if (nextProps.advertisers && !this.props.advertisers) {
You are not comparing the next and the previous props. "this.props.advertisers" is probably already set so you never go into the setState line. Although using componentWillReceiveProps is no longer the recommended way to go (You Probably Don't Need Derived State), what you probably want to do roughly is:
if (nextProps.advertisers && nextProps.advertisers !== !this.props.advertisers) {

Categories