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);
Related
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")
I'm trying to set a variable with a simple GET database call. The database call is returning the data correctly, but the variable remains undefined after every re-render. Code is below... the getMyThing() function in the useState() function is working correctly and returning the data I want.
import { getMyThing } from '../../utils/databaseCalls'
const MyComponent: React.FC = () => {
const { id } = useParams();
const [myThing, setMyThing] = useState(getMyThing(id));
useEffect(() => {
setMyThing(myThing)
}, [myThing]);
}
My thinking here was to use useState() to set the initial state of the myThing variable with the data returned from my database. I assume it's not immediately working since a database call is asynchronous, so I thought I could use useEffect() to update the myThing variable after the response of the database call completes, since that would trigger the useEffect() function because I have the myThing variable included as a dependency.
What am I missing here? Thanks!
EDIT: Thanks for the answers everyone, but I still can't get it to work by calling the getMyThing function asynchronously inside useEffect(). Is something wrong with my database call function? I guess it's not set up to a return a promise? Here's what that looks like:
export const getMyThing = (id) => {
axios.get('http://localhost:4000/thing/' + id)
.then(response => {
return(response.data);
})
.catch(function (error){
console.log(error);
})
}
You should do all your side effects(fetching data, subscriptions and such) in useEffect hooks and event handlers. Don't execute async logic in useState as you just assign the promise itself to the variable and not the result of it. In any case, it is a bad practice and it won't work. You should either:
import { getMyThing } from '../../utils/databaseCalls'
const MyComponent: React.FC = () => {
const { id } = useParams();
const [myThing, setMyThing] = useState(null);
useEffect(() => {
const fetchData = async () => {
const result = await getMyThing(id);
setMyThing(result);
};
fetchData();
}, [id, getMyThing]);
}
Or if you don't want to introduce an async function:
import { getMyThing } from '../../utils/databaseCalls'
const MyComponent: React.FC = () => {
const { id } = useParams();
const [myThing, setMyThing] = useState(null);
useEffect(() => {
getMyThing()
.then(result => setMyThing(result));
}, [id, getMyThing]);
}
Also, take note of the [id, getMyThing] part as it is important. This is a dependency array determining when your useEffect hooks are gonna execute/re-execute.
If getMyThing returns a Promise, the myThing will be set to that Promise on the first render, and then myThing will stay referring to that Promise. setMyThing(myThing) just sets the state to the Promise again - it's superfluous.
Call the asynchronous method inside the effect hook instead:
const [myThing, setMyThing] = useState();
useEffect(() => {
getMyThing(id)
.then(setMyThing);
}, []);
Here, myThing will start out undefined, and will be then set to the result of the async call as soon as it resolves.
You can't set the initial state with a value obtained asynchronously because you can't have the value in time.
myThing cannot both return the value you want and be asynchronous. Maybe it returns a promise that resolves to what you want.
Set an initial value with some default data. This might be null data (and later when you return some JSX from your component you can special case myThing === null by, for example, returning a Loading message).
const [myThing, setMyThing] = useState(null);
Trigger the asynchronous call in useEffect, much like you are doing now, but:
Make it rerun when the data it depends on changes, not when the data it sets changes.
Deal with whatever asynchronous mechanism your code uses. In this example I'll assume it returns a promise.
Thus:
useEffect(async () => {
const myNewThing = await getMyThing(id);
setMyThing(myNewThing)
}, [id]);
When I try to proces data come from api then use it to render, but I always go to a problem with async because the process function doesn't wait for my fetching functions.
const [fetchData1, setData1] = useState([]);
const [fetchData1, setData2] = useState([]);
const [processedData, setProcessedData] = useState([]);
useEffect(() => {
const getData1 = async () => {
//get data1 using axios
//setData1(response)
}
const getData2 = async () => {
//get data2 using axios
//setData2(response)
}
getData1();
getData2();
setProcessedData(processData(fetchData1, fetchData2));
}, [])
const processData = (data1, data2) => {
//process two data
//return data;
}
Even when I try to wrap two fetching functions and the process function in an async function but the problem remains the same.
(async () => {
await getData1();
await getData2();
setProcessedData(processData(fetchData1, fetchData2));
})
Reading your question, as far as I can tell you don't need fetchData1 and fetchData2, you just want the processedData. The problem with your current code is that it's using the default values of fetchData1 and fetchData2 when calling setProcessedData, it's not using the results form axios.
Wait for both promises to settle and use their results. See comments:
const [processedData, setProcessedData] = useState([]);
useEffect(() => {
const getData1 = async () => {
//get data1 using axios
//setData1(response)
};
const getData2 = async () => {
//get data2 using axios
//setData2(response)
};
// *** Wait for both promises to be fulfilled
Promise.all(
getData1(),
getData2()
).then([data1, data2]) => { // Get those results into parameters
// *** Use the parameter values
setProcessedData(processData(data1, data2));
}).catch(error => {
// handle/report error
});
}, []);
// *** render using the current values in `processedData`
Note that since you're only do this when the component is first created, you don't need to worry about cancelling it, etc., when other state in the component changes (if it has other state). If the calls depended on other state data you were listing in the dependency array, you might need to handle disregarding earlier results if that other data changed during the calls to axios. But again, not with what you're doing here.
Promise.all is for handling multiple asnyc operations:
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/all
Here is more examples:
https://www.taniarascia.com/promise-all-with-async-await/
I have a screen in a React-Native project which essentially just renders a loading icon whilst fetching data from the server, before then taking the user to the main screen. The first function getPrivateKey() will return the private key and store it using redux in the state, and the next function connectWithKey() will then use that key to connect.
The issue I'm facing is that when connectWithkey() runs, it's using the initial, empty value of the private key, not the updated value. Here's the code, and apologies if I'm being stupid it's been a long day :(
export default DataLoader = props => {
//private key - this should, in theory, update after getPrivateKey()
const privateKey = useSelector(({ main }) => main.privateKey);
const dispatch = useDispatch();
useEffect(() => {
const configure = async () => {
//this will update the private key
await getPrivateKey();
//this should use the new private key from useSelector, but instead is using the initialised empty object
await connectWithKey();
props.navigation.navigate('MainScreen');
};
configure();
}, []);
//.... more code below....
I've tried adding privateKey into the array dependencies which just caused an infinite loop, and I've checked that the value has updated in the redux store - so I'm a bit lost! In essence, it appears that the useSelector hook isn't getting a fresh value. Any help would be very much appreciated 😊 Thanks!
EDIT - added more code upon request 😊
const getPrivateKey = async () => {
const privKey = await fetchKeyFromServer();
dispatch({
type: 'UPDATE',
value: privKey
});
};
const connectWithkey = async () => {
//the privateKey here should be the updated value from useSelector
await connectToServer(privateKey)
};
Looks like your getPrivateKey function is a thunk, but you are not dispatching it ? And there is nothing stopping you from returning values from thunks.
const getPrivateKey = async (dispatch) => {
const privKey = await fetchKeyFromServer();
dispatch({
type: 'UPDATE',
value: privKey
});
return privKey // return the key here to whoever wants to use the value immediately.
};
Then in your useEffect in the component you can use the return value easily :)
useEffect(() => {
const configure = async () => {
//make sure you 'dispatch' this thunk
const key = await dispatch(getPrivateKey());
// pass the key
await dispatch(connectWithKey(key));
...
};
....
}, []);
The code above assumes that the connectWithKey is a thunk too. If so, you can design the thunk in a way that it either uses the passed value or reads it from the redux store.
const connectWithkey = (privateKey: passedPrivateKey) = async (dispatch, getState) => {
const state = getState();
let privateKey = state.whatever.the.path.is.to.privateKey;
// use the passed private key if it is present.
if (passedPrivateKey) {
privateKey = passedPrivateKey;
}
await connectToServer(privateKey)
};
I have used this approach several times in my app. This way you do not need to rely on the state in the selector. And should you choose to rely on that state, the dependencies of your useEffect should update accordingly. Right now it is an empty array, and that is why the effect doesn't run again on any state changes (it is acting like the componentDidMount lifecycle function).
const privateKey = useSelector(({ main }) => main.privateKey);
useEffect(() => {
await getPrivateKey();
if (privateKey) {
await connectWithKey();
}
}, [privateKey]);
This way your hook re-runs everytime privateKey state changes. You might need to have some sort of condition for your connectWithKey thunk though, so that it doesn't run if the key is null.
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);