Can't delete document in Firestore from React/Redux - javascript

I'm building a CRUD application using redux toolkit and firestore, and I cannot figure out how to delete an item from firestore, and I really don't know why the following code isn't working. I've tried this in several different ways, and the current error that I'm getting is:
"Cannot use 'in' operator to search for '_delegate' in undefined"
Here's the relevant code from the slice:
export const recipeSlice = createSlice({
name: 'recipesSlice',
initialState: {
recipes: []
},
reducers: {
ADD_RECIPE: (state, action) => {
state.recipes.push(action.payload)
},
DELETE_RECIPE: (state, action) => {
state.recipes = state.recipes.filter((recipe) => recipe.recipeId !== action.payload.recipeId)
}
And here is the thunk that I cannot, for the life of me make work:
export const deleteRecipe = ({recipeId}) => {
return async (dispatch) => {
const q = query(collection(db, "recipes"), where("recipeId", "==", `${recipeId}`));
const querySnapshot = await getDocs(q);
querySnapshot.forEach(async(doc) => {
console.log(doc.id, " => ", doc.data())
await deleteDoc(doc)
});
dispatch(DELETE_RECIPE({recipeId}))
}
}
I didn't use createAsyncThunk because it didn't seem to be a good use case, but I could be wrong.
I've tried firing this function with hard-coded dummy data, and that doesn't help. I have also tried running this code without the 'DELETE_RECIPE' reducer but that doesn't make a difference. I thought that using async/await within the forEach loop on the querySnapshot would work because it's not a typical forEach loop but rather a method in Firestore to iterate over querysnapshot.

The deleteDoc() functions takes DocumentReference of the document and not the snapshot. Also try using Promise.all() or Batched Writes to delete the documents at once instead of using a forEach() loop. Try refactoring the code as shown below:
const querySnapshot = await getDocs(q);
const deletePromises = querySnapshot.docs.map((d) => deleteDoc(d.ref))
await Promise.all(deletePromises)
console.log("Documents deleted")

Related

react useState only stores the last document

I am working on a react Netflix clone web app and I have stored some data on Firestore for making a favorite list. When I fetched data from Firestore and try to store in a state, some error occured.
There were about four documents in firebase but I only got the last one every time I try
I have included key to the map and try to give the state empty string as initial value, I tried spread operator but none of them worked
firestore is working well as i see those documents on console
const [movie, setMovie] = useState([])
useEffect(() => {
async function fetchData() {
const q = query(collection(db, "movie"));
const querySnapshot = await getDocs(q);
querySnapshot.forEach((doc) => {
setMovie([...movie, doc.data().Details])
console.log(doc.data().Details)
console.log(movie)
});
}
fetchData()
Your fetchData function closes over movie, which means it only sees the movie that existed when the function was created (you haven't shown enough to be sure, but I'm guessing you have an empty dependencies array on that useEffect, so that would be an always-empty movie). Doing setMovie([...movie, doc.data().Details]) just spreads that empty movie and then adds the one final document.
Instead, two things:
Whenever updating state based on existing state, it's best to use the callback form of the setter so that you get the up-to-date version of the state you're updating.
Collect the documents, then do just one setter call.
const [movies, setMovies] = useState([])
useEffect(() => {
async function fetchData() {
const q = query(collection(db, "movie"));
const querySnapshot = await getDocs(q);
setMovies(previousMovies => [
...previousMovies,
...querySnapshot.map((doc) => doc.data().Details)
]);
}
fetchData();
}, []); // <== I've assumed this
(Note I've made it a plural, since there's more than one movie.)
(I don't use MongoDB, but I've assumed from the forEach that querySnapshot is an array and so it has map. If not, it's easy enough to create the array, use forEach to push to it, and then do the setMovies call.)
But there's another thing: You should allow for the possibility your component is unmounted before the query completes. If your getDocs has a way to cancel its operation, you'll want to use that. For instance, if it accepted an AbortSignal, it might look like:
const [movies, setMovies] = useState([])
useEffect(() => {
async function fetchData() {
const controller = new AbortController();
const { signal } = controller;
const q = query(collection(db, "movie"));
const querySnapshot = await getDocs(q, signal);
if (!signal.aborted) {
setMovies(previousMovies => [
...previousMovies,
...querySnapshot.map((doc) => doc.data().Details)
]);
}
}
fetchData();
return () => {
controller.abort();
};
}, []);
But if there's some other cancellation mechanism, naturally use that.
If there's no cancellation mechanism, you might use a flag so you don't try to use the result when it won't be used for anything, but that may be overkill. (React has backed off complaining about it when you do a state update after the component is unmounted, as it was usually a benign thing to do.)
Don't know if it will solve the problem, but when you set a state value base on the previous value (here setMovie([...movie, doc.data().Details])) you should use a callback function: https://en.reactjs.org/docs/hooks-reference.html#functional-updates
Like this:
setMovie(oldMovies => ([...oldMovies, doc.data().Details]))
If it still doesn't work try to create a new tab you're filling during the forEach, and at the end, set the state:
const q = query(collection(db, "movie"));
const querySnapshot = await getDocs(q);
const newMovies = []
querySnapshot.forEach((doc) => {
newMovies.push(doc.data().Details)
});
setMovie([...movie, ...newMovies])
for a start, I suggest you to do something like the following:
const [movie, setMovie] = useState([])
useEffect(() => {
async function fetchData() {
const q = query(collection(db, "movie"));
const querySnapshot = await getDocs(q);
setMovie(querySnapshot.flatMap(elt=>elt.data().Details));
}
fetchData()
It's because you update your state on forEach loop, you start from first doc until the last one, in the end you set the last movie, that's why you have everytime the last doc on your state.
First of all don't update state too frequently, it creates app performance problems.
To fix it (based on your example):
let movies=[]
querySnapshot.forEach((doc) => {
movies([...movies, doc.data().Details]);
//or movies.push(doc.data().Details);
});
setMovie(movies);

Direct access to a doc in an documen query array returns undefined outside of the onSnapshot() block

I am trying to pass the result of the queried documents (which is an array) outside of the function. However, accessing this array outside of the onSnapshot function yields undefined.
I have spent a whole day on this trying various things but can't figure it out.
export const getComments = (taskId) => {
const comments = [] // i want to return this array once it is filled wit documents
const dbRef = db.collection('tasks').doc(taskId).collection('comments') // comments is a subcollection of each task document
dbRef.onSnapshot(
(querySnapshot) => {
querySnapshot.forEach((doc) => {
comments.push(doc.data()) //each doc.data() is an object that gets pushed to the comments array. Lets say that the 1st doc.data() is {userId: 'xyz', user: 'Kiu', comment: 'this is my 1st stack overflow question'}
})
console.log('value for comments[0] inside onSnapshot')
console.log(comments[0]) //sucessfully display the 1st object in the array as {userId: 'xyz', user: 'Kiu', comment: 'this is my 1st stack overflow question'}
},
(error) => {
throw new Error(error.message)
}
)
console.log('value for comments[0] outside onSnapshot')
console.log(comments[0]) // this is undefined for some reason????
console.log('value for the whole comments array outside onSnapshot')
console.log(comments) // this does actually show the whole array, however any method used here (i.e. forEach(), map() ) returns undefined. ????
return comments
}
When calling getComments(taskId) in App.js the console.log() result is not what I expect. Can somebody please help this newbie?
--update#1 : I read elsewhere on stack overflow that onSnapshot is to activate a listener. Listeners are not to be used in an asynchronous fashion , as listeners stay 'on' until unsubscribed. So i don't think using async and await will work for this one. I think this is where I read it. See the bottom of this discussion: What is the right way to cancel all async/await tasks within an useEffect hook to prevent memory leaks in react?
I think your problem is that you are returning (and accessing) the comments array outside of the onSnapshot() call.
According to the docs for listening to collections, you should be listening to/modifying/returning/etc. the comments array from within the onSnapshot() block:
export const getComments = (taskId) => {
const comments = [];
const dbRef = db.collection("tasks").doc(taskId).collection("comments");
dbRef.onSnapshot(
(querySnapshot) => {
querySnapshot.forEach((doc) => {
comments.push(doc.data());
});
console.log("value for comments[0] inside onSnapshot");
console.log(comments[0]);
return comments; // or comments.map(), comments.forEach(), etc.
},
(error) => {
throw new Error(error.message);
}
);
};
Your useEffect hook in App.js should follow the unsubscribe pattern as described in the React docs for Effects with Cleanup.
After reading a bunch of forums, and the response above from #kamillamagna, I proceeded to use useState and useEffect in conjunction with onSnapshot. The typical way of doing this seems to use onSnapshot() with useState and useEffect in the component, OR to use it in a custom hook. I rewrote my code to the following and it works as expected:
import {useState, useEffect} from 'react'
import firebase from 'firebase/app'
/*
useComments is a custom React Hook.
Below is taken from the react website: https://reactjs.org/docs/hooks-custom.html
To share stateful logic between two or more JavaScript functions, we extract it to a third function. Both components and Hooks are functions, so this works for them too!
A custom Hook is a JavaScript function whose name starts with ”use” and that may call other Hooks.
*/
// -- returns an array of comments for a given taskId
// -- using the prefix of 'use' in front of the function name is important to identify it as a custom hook
export default function useComments(taskId) {
const [comments, setComments] = useState([])
useEffect(() => {
const commentsRef = firebase
.firestore()
.collection('tasks')
.doc(taskId)
.collection('comments') // comments is a subcollection of each task document
const unsubscribe = commentsRef.onSnapshot(
(snapshot) => {
const commentsArray = snapshot.docs.map((doc) => ({
id: doc.id,
...doc.data(),
}))
setComments(commentsArray)
},
(error) => {
throw new Error('Error: ' + error.message)
}
)
return () => unsubscribe()
// -- the return function activates when the componentWillUnmount, which is to clean up (unsubscribe) to the onSnapshot listener
}, [])
return comments // -- an array of comment objects with all the key value pairs, including the doc id. The doc.id will be used as the element's unique key when rendering the React component
}
I have been focusing on this issue in the React and Firestore ecosystem. I am not sure how to achieve the same effect if i was not using React (i.e. plain vanilla js). Maybe returning the snapshot result inside the next() function inside onSnapshot() is sufficient (as #kamillamagna suggested)

Returning array of results from Firebase query

I'm using the #react-native-firebase wrapper to interact with Firebase's Firestore. I have a function which performs some querying to one of my collections and the intended response should be an Array object containing each found document.
My function currently looks like:
export const backendCall = async => {
let response = []
firestore()
.collection('collection')
.where('id', '==', 1)
.onSnapshot(documentSnapshot => {
documentSnapshot.forEach(x => response.push(x.data()))
})
return response
On my UI, I use a react-native Button component which calls this function onPress:
<Button
title="get"
onPress={() => {
backendCall().then(response => console.log(response))
}}
/>
Using console.log I am able to observe the expected Array object (but with a "value below was evaluated just now" icon. If however, I change the onPress to console.log(JSON.stringify(response)), the Array object is empty.
I'm assuming this has something to do with the async calls but I can't quite figure out how to fix it. Would appreciate some pointers.
You're returning the response without waiting for the result from your firebase query. To wait for the response to arrive before returning, you can use Promise.
export const backendCall = () => {
return new Promise(resolve => {
firestore()
.collection('collection')
.where('id', '==', 1)
.onSnapshot(documentSnapshot => {
const response = []
documentSnapshot.forEach(x => response.push(x.data()))
resolve(response)
})
})
}
You can use Array.map to make the for loop looks nicer.
const response = documentSnapshot.map(x => x.data())
resolve(response)
You can also read docs from a QuerySnapshot:
export const backendCall = async () => {
const qs = firestore().collection('collection').where('id', '==', 1).get()
return qs.docs.map(x => x.data())
}

Create query snapshot to listen live changes

I created a function which gets data from Firestore using if condition. I have achieved it with .get() method but it does not get updated in real time, I mean if any record is updated in the server, it doesn't reflect here.
Here is my function
async getMyPosts() {
this.query = firebase.firestore().collection('complaints')
if (this.complaint_status_filter_active==true) { // you decide
this.query = this.query
.where('complaint_status', '==', this.complaint_status_filter_value)
.limit(5)
}
const questions = await this.query.get()
.then(snapshot => {
snapshot.forEach(doc => {
console.log(doc.data())
})
})
.catch(catchError)
}
What should be used instead of get() to have real time update
Edit 1
In below example I am able to use snapshot changes using Angular Firestore but here I am not able to use if condition
How do I add where condition with if clause here? Code:
this.firestore.collection('complaints' , ref => ref
.where('complaint_status', '==', this.complaint_status_filter_value)
.limit(5)
.orderBy("complaint_date","desc")
)
.snapshotChanges()
.subscribe(response => {
Use onSnapshot() to get realtime updates, as illustrated in the documentation.
this.query.onSnapshot(doc => {
// ...
})

React Apollo Hooks - Chain mutations

I have two mutations:
const [createRecord, {data}] = useMutation(createRecordQuery); which returns the ID of the newly created record
const [saveValue, {data: dataSave}] = useMutation(saveValueQuery); which save some values on a record
My saveValue mutation requires a record ID. If I open my form on a new record, I don't have this ID yet, so on submit I need to call createRecord first to retrieve the ID and then saveValue to save values on my record.
This simple approach doesn't work:
const onSave = async values => {
if (!recordId) {
// Call createRecord to retrieve recordId (code is simplified here)
const recordId = await createRecord();
}
// Save my values
return saveValue({variables: {recordId, values});
}
But I don't really know how should I deal with the loading and data of the first mutation and wait for it to run the second mutation.
Thanks!
The mutate function returns a promise that resolves to the mutation response data, so you should simply be able to use to achieve what you want.
From the source code:
If you're interested in performing some action after a mutation has
completed, and you don't need to update the store, use the Promise
returned from client.mutate
I'm not sure why this didn't work in your initial tests, but I tried it locally and it works as expected. You should essentially be able to do what you wrote in your question.
I'm not sure if there is a way to postpone execution(as well as we cannot pause <Mutation>). So how about moving second part into separate useEffect?
const [recordId, setRecordId] = useState(null);
const [values, setValues] = useState({});
const onSave = async _values => {
if (!recordId) {
// Call createRecord to retrieve recordId (code is simplified here)
setRecordId(await createRecord());
}
setValues(_values);
}
useEffect(() => {
saveValue({variables: {recordId, values});
}, [recordId, _values]);
Another workaround is utilizing withApollo HOC:
function YourComponent({ client: { mutate } }) {
onSave = async values => {
let recordId;
if (!recordId) {
recordId = await mutate(createRecordQuery);
}
await mutate(saveValueQuery, values);
// do something to let user know saving is done
};
export withApollo(YourComponent);

Categories