VueJS and Firestore: Updating a document property happens twice - javascript

I got a click handler that, when clicked, for some reason is updating my value twice (incrementing or decrementing twice). It doesn't happen all the time, but I would say 90% of the time. I want to prevent that and only update the value once.
Let me explain.
First, here's my button template code:
<b-button
variant="link"
class="mb-2"
#click="clueHandler(comment)"
>
Here is the script code for the clueHandler:
async clueHandler(comment) {
const inClues =
this.clues.findIndex((clue) => clue.id === comment.id) > -1
if (this.loggedIn) {
if (inClues) {
await this.$store.dispatch('removeCommentFromCluesFeed', comment)
console.log('dispatched from clueHandler')
} else if (!inClues) {
await this.$store.dispatch('addCommentToCluesFeed', comment)
console.log('dispatched from clueHandler')
}
} else {
this.$router.replace('/login')
}
},
Here's the logic in Vuex for updating the Firestore document property:
async addCommentToCluesFeed({ state }, comment) {
try {
const cluesFeedDoc = this.$fireStore
.collection(`users/${state.userProfile.uid}/clues`)
.doc(comment.id)
await cluesFeedDoc.set(comment)
console.log('clue added from addCommentToCluesFeed action')
await this.$fireStore
.collection('comments')
.doc(comment.id)
.update({
clueVotes: parseInt(comment.clueVotes) + 1 // <<<<------------HERE !!
})
console.log('clue vote increased from addCommentToCluesFeed action')
} catch (error) {
console.error(
'error adding clue from addCommentToCluesFeed action',
error
)
}
},
Here's a screenshot of the area the handler is updating:
What happens is that when you click ONCE on the template button with the clueHandler, the document property clueVotes is incremented twice (or decremented twice depending on whether the clue was already set by the user previously).
Here is an example of what the database shows when a user clicks the button for the first time:
It should only be clueVotes: 1.
Anyone have any advice or thoughts on what is happening or how I can fix? I am assuming this is going to be related to not fully realizing the promise/race completion, etc. Thanks!

I found an excellent explanation of why I was having this issue. Here it is:
https://fireship.io/snippets/firestore-increment-tips/
Basically, I needed to return a special value that can be used with set() or update() that tells the server to increment the field's current value by the given value.
Here's my code update:
.update({
clueVotes: this.$fireStoreObj.FieldValue.increment(1)
})

Related

SolidJS: How to trigger refetch of createResource?

I have a createResource which is working:
const fetchJokes = async (programmingOnly) => {
alert(programmingOnly);
return (await fetch(`https://official-joke-api.appspot.com/jokes/${programmingOnly?'programming/':''}ten`)).json();
}
//...
const [jokes, { mutate, refetch }] = createResource(programmingOnly(), fetchJokes);
Now I want to change the programmingOnly boolean via it's signal:
const [programmingOnly, setProgrammingOnly] = createSignal(true);
//...
Programming Only: <input type="checkbox" checked={programmingOnly()}
onInput={()=>{setProgrammingOnly(!programmingOnly());refetch();}}> </input>
But this does not work, the alert fires upon subsequent attempts, but with undefined as arg, and nothing happens with the data.
What is the SolidJS way of approaching this?
I believe the problem here is the signal is getting set with false every time. Since programmingOnly is a signal it should be accessed as a function to retrieve its value.
Ie..
setProgrammingOnly(!programmingOnly)
// should be
setProgrammingOnly(!programmingOnly())
You should not need to call refetch or do anything else for the example to work.

Function A depends on previous function to update the state, but Function A still tries to render before the update

An Example I have linked below, that shows the problem I have.
My Problem
I have these two functions
const updatedDoc = checkForHeadings(stoneCtx, documentCtx); // returns object
documentCtx.setUserDocument(updatedDoc); // uses object to update state
and
convertUserDocument(stoneCtx, documentCtx.userDocument);
// uses State for further usage
The Problem I have is, that convertUserDocument runs with an empty state and throws an error and then runs again with the updated state. Since it already throws an error, I cannot continue to work with it.
I have tried several different approaches.
What I tried
In the beginning my code looked like this
checkForHeadings(stoneCtx, documentCtx);
// updated the state witch each new key:value inside the function
convertUserDocument(stoneCtx, documentCtx.userDocument);
// then this function was run; Error
Then I tried the version I had above, to first put everything into an object and update the state only once.
HavingconvertUserDocument be a callback inside of checkForHeadings, but that ran it that many times a matching key was found.
My current try was to put the both functions in seperate useEffects, one for inital render and one for the next render.
const isFirstRender = useRef(true);
let init = 0;
useEffect(() => {
init++;
console.log('Initial Render Number ' + init);
console.log(documentCtx);
const updatedDoc = checkForHeadings(stoneCtx.stoneContext, documentCtx);
documentCtx.setUserDocument(updatedDoc);
console.log(updatedDoc);
console.log(documentCtx);
isFirstRender.current = false; // toggle flag after first render/mounting
console.log('Initial End Render Number ' + init);
}, []);
let update = 0;
useEffect(() => {
update++;
console.log('Update Render Number ' + update);
if (!isFirstRender.current) {
console.log('First Render has happened.');
convertUserDocument(stoneCtx.stoneContext, documentCtx.userDocument);
}
console.log('Update End Render Number ' + update);
}, [documentCtx]);
The interesting part with this was to see the difference between Codesandbox and my local development.
On Codesandbox Intial Render was called twice, but each time the counter didn't go up, it stayed at 1. On the other hand, on my local dev server, Initial Render was called only once.
On both version the second useEffect was called twice, but here also the counter didn't go up to 2, and stayed at 1.
Codesandbox:
Local Dev Server:
Short example of that:
let counter = 0;
useEffect(()=> {
counter++;
// this should only run once, but it does twice in the sandbox.
// but the counter is not going up to 2, but stays at 1
},[])
The same happens with the second useEffect, but on the second I get different results, but the counter stays at 1.
I was told this is due to a Stale Cloruse, but doesn't explain why the important bits don't work properly.
I got inspiration from here, to skip the initial render: https://stackoverflow.com/a/61612292/14103981
Code
Here is the Sandbox with the Problem displayed: https://codesandbox.io/s/nameless-wood-34ni5?file=/src/TextEditor.js
I have also create it on Stackblitz: https://react-v6wzqv.stackblitz.io
The error happens in this function:
function orderDocument(structure, doc, ordered) {
structure.forEach((el) => {
console.log(el.id);
console.log(doc);
// ordered.push(doc[el.id].headingHtml);
// if (el.children?.length) {
// orderDocument(el.children, doc, ordered);
// }
});
return ordered;
}
The commented out code throws the error. I am console.loggin el.id and doc, and in the console you can see, that doc is empty and thus cannot find doc[el.id].
Someone gave me this simple example to my problem, which sums it up pretty good.
useEffect(() => {
documentCtx.setUserDocument('ANYTHING');
console.log(documentCtx.userDocument);
});
The Console:
{}
ANYTHING
You can view it here: https://stackblitz.com/edit/react-f1hwky?file=src%2FTextEditor.js
I have come to a solution to my problem.
const isFirstRender = useRef(true);
useEffect(() => {
const updatedDoc = checkForHeadings(stoneCtx.stoneContext, documentCtx);
documentCtx.setUserDocument(updatedDoc);
}, []);
useEffect(() => {
if (!isFirstRender.current) {
convertUserDocument(stoneCtx.stoneContext, documentCtx.userDocument);
} else {
isFirstRender.current = false;
}
}, [documentCtx]);
Moving isFirstRender.current = false; to an else statement actually gives me the proper results I want.
Is this the best way of achieving it, or are there better ways?

Why can't my function read the actual value of my component's state?

For a school project my group is building a table that's filled with city-data via a database-call.
The skeleton of the table-component is as such:
import React, { useState } from 'react'
function Table(props) {
const [ cities, setCities ] = useState([])
const [ pageNum, setPageNum ] = useState(0)
useEffect(()=> { // this sets up the listener for the infinite scroll
document.querySelector(".TableComponent").onscroll = () => {
if (Math.ceil(table.scrollHeight - table.scrollTop) === table.clientHeight) showMoreRows()
}
//initial fetch
fetchData(0)
},[])
async function showMoreRows() {
console.log("Show more rows!")
await fetchData(pageNum)
}
async function fetchData(page) {
// some code, describing fetching
// EDIT2 start
console.log(pageNum)
// EDIT2 end
const jsonResponse = await {}// THE RESPONSE FROM THE FETCH
if(page) {
setCities([...cities, ...jsonResponse])
console.log("page is true", page)
setPageNum(pageNum + 1)
} else {
console.log("page is false", page) // this always runs and prints "page is false 0"
setCities([...cities, ...jsonResponse])
setPageNum(1)
}
}
return <div className="TableComponent"> { pageNum }
<!-- The rest of the component -->
</div>
}
The table features an "infinite-scrolling"-feature, so when you scroll to the bottom of the page it prints "Show more rows!" and runs fetchData(pageNum) to get more data. At this point, after the initial fetch, the pageNum-variable should be 1 or more, but for some reason the function acts as if it is 0. I put the pageNum-variable on display in the JSX, and I can see that it is 1, but it still prints out "page is false 0" when ever I run it.
When I try to google the issue, it seems the only similar thing could be that I try to read a useState-variable too soon after using setPageNum (before the redraw), but that isn't the case here as far as I can see. I give it plenty of time between tries, and it always says pageNum is zero.
Any ideas as to what I am doing wrong, and how this makes sense in any way?
Thanks for any help!
EDIT: Just tried the code I wrote over, and it seemed to work - however the full code I have doesn't work. Anyone have any ideas about problems related to this, even if the above code might work?
EDIT2: I added a console.log(pageNum) to the fetchData-function, and tested a bit, and it seems that whatever I put into the initial value in useState(VALUE) is what is being printed. That makes NO sense to me.
Help.
EDIT3: Added await, already had it in real code
EDIT4: I've tried at this for a while, but realized as I am using react that I could move the scroll-listener I have down to the JSX-area, and then it worked - for some reason. It now works. Can't really mark any answers as the correnct ones, but the problem is somewhat solved.
Thanks all who tried to help, really appreciate it.
Your staleness issues are occurring because React is not aware of your dependencies on the component state.
For example, useEffect ensures that value of showMoreRows from the scope of the initial render will be called on every scroll. This copy of showMoreRows refers to the initial value of pageNum, but that value is "frozen" in a closure along with the function and won't change when the component state does. Hence the scroll listening won't work as it needs to know the current state of pageNum.
You can resolve the issues by using callbacks to "hookify" showMoreRows and fetchData and declare their dependence on the component state. You must then declare the dependence of useEffect on these callbacks and use a clean-up function to handle the effect being invoked more than once.
It would look something like this (I haven't tried running the code):
import React, { useState } from 'react'
function Table(props) {
const [ cities, setCities ] = useState([])
const [ pageNum, setPageNum ] = useState(0)
useEffect(()=> {
// Run this only once by not declaring any dependencies
fetchData(0)
}, [])
useEffect(()=> {
// This will run whenever showMoreRows changes.
const onScroll = () => {
if (Math.ceil(table.scrollHeight - table.scrollTop) === table.clientHeight) showMoreRows()
};
document.querySelector(".TableComponent").onscroll = onScroll;
// Clean-up
return () => document.querySelector(".TableComponent").onscroll = null;
}, [showMoreRows])
const showMoreRows = React.useCallback(async function () {
console.log("Show more rows!")
await fetchData(pageNum)
}, [fetchData, pageNum]);
const fetchData = React.useCallback(async function (page) {
// some code, describing fetching
// EDIT2 start
console.log(pageNum)
// EDIT2 end
const jsonResponse = await {}// THE RESPONSE FROM THE FETCH
if(page) {
setCities([...cities, ...jsonResponse])
console.log("page is true", page)
setPageNum(pageNum + 1)
} else {
console.log("page is false", page) // this always runs and prints "page is false 0"
setCities([...cities, ...jsonResponse])
setPageNum(1)
}
}, [setCities, cities, setPageNum, pageNum]);
return <div className="TableComponent"> { pageNum }
<!-- The rest of the component -->
</div>
}
This might not totally solve the problem (it's hard to tell without more context), but useEffect runs every render, so things like that 'initial' fetchData(0) are going to run every update, which would probably give you the result from page = 0 every time in that conditional in fetchData.
It's hard to say without more context, but I have one guess.
Try using
setCities(value => [...value, ...jsonResponse])
instead of
setCities([...cities, ...jsonResponse])
Also make sure you use await for resolving promises for requests like:
const jsonResponse = await ...
You can console log it to check if they are not pending and that you get the right property if it's a nested object.

How to make this run only once?

I'm trying to get it where If someone clicks the button it will update the database but what happens if I enter 50 then it will keep running it and I have a tracking board that sums everything up so it overloads my server and makes the total in the 1000's when its normally just over 100.
I've tried a document ready function, I've had on and one. ('click') and it keeps running multiple times
$('#update_new_used-counter').one('click', function (event) {
event.preventDefault();
let updated_new_counter = $('#new_sold-update').val().trim();
let updated_used_counter = $('#used_sold-update').val().trim();
trackingBoardRef.on("value", function(snapshot) {
let current_new_counter = snapshot.val().new;
let current_used_counter = snapshot.val().used;
if (updated_new_counter == '') {
trackingBoardRef.update({
new: current_new_counter,
});
} else {
trackingBoardRef.update({
new: updated_new_counter,
})
};
if (updated_used_counter == '') {
trackingBoardRef.update({
used: current_used_counter,
});
} else {
trackingBoardRef.update({
used: updated_used_counter,
})
};
console.log(snapshot.val().new);
console.log(snapshot.val().used);
});
});
That's what I have now and it just keeps running multiple times until firebase says I had to many requests and stops it. I just want it to update once
When you call:
trackingBoardRef.on("value", function(snapshot) {
You attach a listener to the data in trackingBoardRef that will be triggered right away with the current value, and then subsequently whenever the data under trackingBoardRef changes. And since you're changing data under trackingBoardRef in your code, you're creating an infinite loop.
If you only want to read the data once, you can use the aptly named once method:
trackingBoardRef.once("value", function(snapshot) {
...
Note that if you're update the value under trackingBoardRef based on its current value, you really should use a transaction to prevent users overwriting each other's changes.

Angular 2 Observables - Empty response on action still shows data in array

I have this function that aggregates some user data from Firebase in order to build a "friend request" view. On page load, the correct number of requests show up. When I click an "Accept" button, the correct connection request gets updated which then signals to run this function again, since the user is subscribed to it. The only problem is that once all of the friend requests are accepted, the last remaining user stays in the list and won't go away, even though they have already been accepted.
Here is the function I'm using to get the requests:
getConnectionRequests(userId) {
return this._af.database
.object(`/social/user_connection_requests/${userId}`)
// Switch to the joined observable
.switchMap((connections) => {
// Delete the properties that will throw errors when requesting
// the convo keys
delete connections['$key'];
delete connections['$exists'];
// Get an array of keys from the object returned from Firebase
let connectionKeys = Object.keys(connections);
// Iterate through the connection keys and remove
// any that have already been accepted
connectionKeys = connectionKeys.filter(connectionKey => {
if(!connections[connectionKey].accepted) {
return connectionKey;
}
})
return Observable.combineLatest(
connectionKeys.map((connectionKey => {
return this._af.database.object(`/social/users/${connectionKey}`)
}))
);
});
}
And here is the relevant code in my Angular 2 view (using Ionic 2):
ionViewDidLoad() {
// Get current user (via local storage) and get their pending requests
this.storage.get('user').then(user => {
this._connections.getConnectionRequests(user.id).subscribe(requests => {
this.requests = requests;
})
})
}
I feel I'm doing something wrong with my observable and that's why this issue is happening. Can anyone shed some light on this perhaps? Thanks in advance!
I think you nailed it in your comment. If connectionKeys is an empty array calling Observable.combineLatest is not appropriate:
import 'rxjs/add/observable/of';
if (connectionKeys.length === 0) {
return Observable.of([]);
}
return connectionKeyObservable.combineLatest(
connectionKeys.map(connectionKey =>
this._af.database.object(`/social/users/${connectionKey}`)
)
);

Categories

Resources