I have this Firebase Firestore query set up in my useEffect hook:
const [favorites, setFavorites] = useState([]);
const { user, setUser } = useContext(AuthenticatedUserContext);
const getUserFavorites = async () => {
const favoritesRef = collection(db, "favorites");
const q = query(favoritesRef, where("userId", "==", user.uid));
const querySnapshot = await getDocs(q);
const fetchedFavorites = [];
querySnapshot.forEach(async (document) => {
const docRef = doc(db, "receipes", document.data().recipeId);
const docSnap = await getDoc(docRef);
const data = docSnap.data();
fetchedFavorites.push(data);
});
setFavorites(fetchedFavorites);
console.log("favorites " + favorites);
};
useEffect(() => {
getUserFavorites();
}, []);
Upon first render of the page the favorites will be [] and after a re-render it will be populated. When logging within the forEach I can see that the query is working, so I suspect the async forEach being the culprit here. How can I fix this?
Yes, you should not use async function with a forEach() loop. You can use a for-of loop with the same code or use Promise.all() as shown below:
const getUserFavorites = async () => {
const favoritesRef = collection(db, "favorites");
const q = query(favoritesRef, where("userId", "==", user.uid));
const querySnapshot = await getDocs(q);
const favPromises = querySnapshot.docs.map((d) => getDoc(doc(db, "receipes", d.data().recipeId)))
const fetchedFavs = (await Promise.all(favPromises)).map((fav) => fav.data());
setFavorites(fetchedFavs);
console.log("favorites " + fetchedFavs);
};
Also checkout: Using async/await with a forEach loop for more information.
Related
I am trying to build an app that requires a directory. Every time I update the component, it pushes the data into the array again creating multiple copies. I tried solving that by adding an !arry.includes, but that does not stop it for adding another occurrence of the data.
const Directory = () => {
const [searchResults, setSearchResults] = useState([]);
const [loading, setLoading] = useState(false);
const { currentUser } = useContext(AuthContext);
useEffect(() => {
setLoading(true);
const data = async () => {
const q = query(collection(db, "users"), where("type", "==", "doctor"));
const querySnapshot = await getDocs(q);
querySnapshot.forEach((doc) => {
const data = doc.data();
if (!searchResults.includes(data)) {
searchResults.push(data);
}
});
setLoading(false);
};
data();
},[]);
console.log(searchResults);
It appears that you are trying to compare objects here which doesn't work with the array.includes() method.
In javascript
const user1 = {name : "nerd", org: "dev"};
const user2 = {name : "nerd", org: "dev"};
user1 == user2; // is false
You would need to search for something more specific or try Comparing Objects. Either way what your trying to do is tricky and you may need to explore lodash or something to solve it
I found an easier way!
useEffect(() => {
setLoading(true);
const data = async () => {
const q = query(collection(db, "users"), where("type", "==", "doctor"));
const querySnapshot = await getDocs(q);
querySnapshot.forEach((doc) => {
const data = doc.data();
const notInArray = searchResults.some(u => u.uid === data.uid);
console.log(notInArray);
if (notInArray === false) {
searchResults.push(data)
}
});
setLoading(false);
};
return () => {
data();
};
}, []);
I want to fetch api and set state, then fetch another api with the state that comes from first one. So is that possible ?
Here is my code that i struggle with ;
useEffect(()=>{
const fetchPokemonSpecies = async (pokemon) => {
const data = await axios("https://pokeapi.co/api/v2/pokemon-species/"+pokeName).then( (response) =>
axios(response.data.evolution_chain.url)).then((response) => {setPokemonFirstEvolution(response.data.chain.evolves_to[0].species.name)
setPokemonFirstForm(response.data.chain.species.name)
setPokemonSecondEvolution(response.data.chain.evolves_to[0].evolves_to[0].species.name)})
setPokemonSpecies(data)
setPokemonCategory(data.genera[7].genus)
}
Thanks already !
So i just solve my problem this way ;
const [pokemonCategory, setPokemonCategory] = useState([])
const [pokemonSecondEvolution, setPokemonSecondEvolution] = useState([])
const [pokemonFirstEvolution, setPokemonFirstEvolution] = useState([])
const [pokemonFirstForm, setPokemonFirstForm] = useState([])
const [pokemonFirstFormIcon, setPokemonFirstFormIcon] = useState([])
const [pokemonFirstEvoIcon, setPokemonFirstEvoIcon] = useState([])
const [pokemonSecondEvoIcon, setPokemonSecondFormIcon] = useState([])
useEffect(()=>{
const fetchPokemonSpecies = async (pokemon) => {
const data = await axios("https://pokeapi.co/api/v2/pokemon-species/"+pokeName).then( (response) => response.data)
setPokemonSpecies(data)
setPokemonCategory(data.genera[7].genus)
const data2 = await axios(data.evolution_chain.url).then((response) => axios("https://pokeapi.co/api/v2/pokemon/"+response.data.chain.evolves_to[0].species.name))
setPokemonFirstEvoIcon(data2.data.sprites.other.dream_world.front_default)
const data3 = await axios(data.evolution_chain.url).then((response) => axios("https://pokeapi.co/api/v2/pokemon/"+response.data.chain.evolves_to[0].evolves_to[0].species.name))
setPokemonSecondFormIcon(data3.data.sprites.other.dream_world.front_default)
const data4 = await axios(data.evolution_chain.url).then((response) => axios("https://pokeapi.co/api/v2/pokemon/"+response.data.chain.species.name))
setPokemonFirstFormIcon(data4.data.sprites.other.dream_world.front_default)
}
fetchPokemonSpecies()
},[])
useEffect(()=>{
const fetchPokemonEvoChain = async (pokemon) => {
const data = await axios("https://pokeapi.co/api/v2/pokemon-species/"+pokeName).then( (response) =>
axios(response.data.evolution_chain.url)).then((response) => {setPokemonFirstEvolution(response.data.chain.evolves_to[0].species.name)
setPokemonFirstForm(response.data.chain.species.name)
setPokemonSecondEvolution(response.data.chain.evolves_to[0].evolves_to[0].species.name)})
}
fetchPokemonEvoChain()
},[])
idk if it is the optimal way but it works
I'm fetching an api and want to change useState when the api is returned. However, it simply isn't updating.
Any suggestions?
const fictionApi = "http://localhost:3000/fiction"
const nonFictionApi = "http://localhost:3000/non_fiction"
const [fictionData, setFictionData] = useState(null)
const db = async (url) => {
const res = await fetch(url)
const data = await res.json()
setFictionData(data)
console.log(data.genre)
}
useEffect(() => {
const db = async (url) => {
const res = await fetch(url)
const data = await res.json()
setFictionData(data)
console.log(fictionData)
}
db(fictionApi)
}, [])
I think there is something strange with your syntax.
Something like this should work :
const fictionApi = "http://localhost:3000/fiction"
const nonFictionApi = "http://localhost:3000/non_fiction"
export default function Page () {
const [fictionData, setFictionData] = useState(null);
const [url, setUrl] = useState(fictionApi); // ';' very important
useEffect(() => {
(async () => {
const res = await fetch(url)
const data = await res.json()
setFictionData(data)
})() //Self calling async function
}, [])
}
Moreover, setState is an async process so :
const [fictionData, setFictionData] = useState(null);
setFictionData(true)
console.log(fictionData) //null
So you can use a useEffect to check state :
const [fictionData, setFictionData] = useState(null);
useEffect(()=>{
console.log(fictionData) //true
},[fictionData])
setFictionData(true)
I'm at a loss here. I feel like I've been trying everything, and using the exact methods explained in other posts/tutorials everywhere. I understand that you need to use a cursor and set the first and last visible document so that you can start after the last, in the case of moving forward, and start BEFORE the first, in the case of moving backwards.
In my implementation, going forwards works fine. However, when I utilize the previousPage function, it returns me to the first page, despite setting the 'first visible' document. It returns to the first page even if I've already moved 3 'pages' forward.
Clearly there is something I'm not understanding here..
const PAGE_SIZE = 6;
const [posts, setPosts] = useState([]);
const [lastVisible, setLastVisible] = useState(null);
const [firstVisible, setFirstVisible] = useState(null);
const [loading, setLoading] = useState(false);
// Initial read to get first set of posts.
useEffect(() => {
const q = query(
collectionGroup(db, "bulletins"),
orderBy("createdAt", "desc"),
limit(PAGE_SIZE)
);
const unsubscribe = onSnapshot(q, (documents) => {
const tempPosts = [];
documents.forEach((document) => {
tempPosts.push({
id: document.id,
...document.data(),
});
});
setPosts(tempPosts);
setLastVisible(documents.docs[documents.docs.length - 1]);
setFirstVisible(documents.docs[0]);
});
return () => unsubscribe();
}, []);
const nextPage = async () => {
const postsRef = collectionGroup(db, "bulletins");
const q = query(
postsRef,
orderBy("createdAt", "desc"),
startAfter(lastVisible),
limit(PAGE_SIZE)
);
const documents = await getDocs(q);
updateState(documents);
};
const previousPage = async () => {
const postsRef = collectionGroup(db, "bulletins");
const q = query(
postsRef,
orderBy("createdAt", "desc"),
endBefore(firstVisible),
limit(PAGE_SIZE)
);
const documents = await getDocs(q);
updateState(documents);
};
const updateState = (documents) => {
if (!documents.empty) {
const tempPosts = [];
documents.forEach((document) => {
tempPosts.push({
id: document.id,
...document.data(),
});
});
setPosts(tempPosts);
}
if (documents?.docs[0]) {
setFirstVisible(documents.docs[0]);
}
if (documents?.docs[documents.docs.length - 1]) {
setLastVisible(documents.docs[documents.docs.length - 1]);
}
};
You should use endAt() instead of endBefore() and also, you should pass the order reference which is the createdAt to the endAt() method. See code below:
const PAGE_SIZE = 6;
const [posts, setPosts] = useState([]);
const [lastVisible, setLastVisible] = useState(null);
const [firstVisible, setFirstVisible] = useState(null);
const [loading, setLoading] = useState(false);
useEffect(() => {
const q = query(
collectionGroup(db, "bulletins"),
orderBy("createdAt", "desc"),
limit(PAGE_SIZE)
);
const unsubscribe = onSnapshot(q, (documents) => {
const tempPosts = [];
documents.forEach((document) => {
tempPosts.push({
id: document.id,
...document.data(),
});
});
setPosts(tempPosts);
setLastVisible(documents.docs[documents.docs.length - 1]);
setFirstVisible(documents.docs[0]);
});
return () => unsubscribe();
}, []);
const nextPage = async () => {
const postsRef = collectionGroup(db, "bulletins");
const q = query(
postsRef,
orderBy("createdAt", "desc"),
startAfter(lastVisible.data().createdAt), // Pass the reference
limit(PAGE_SIZE)
);
const documents = await getDocs(q);
updateState(documents);
};
const previousPage = async () => {
const postsRef = collection(db, "bulletins");
const q = query(
postsRef,
orderBy("createdAt", "desc"),
endAt(firstVisible.data().createdAt), // Use `endAt()` method and pass the reference
limitToLast(PAGE_SIZE)
);
const documents = await getDocs(q);
updateState(documents);
};
const updateState = (documents) => {
if (!documents.empty) {
const tempPosts = [];
documents.forEach((document) => {
tempPosts.push({
id: document.id,
...document.data(),
});
});
setPosts(tempPosts);
}
if (documents?.docs[0]) {
setFirstVisible(documents.docs[0]);
}
if (documents?.docs[documents.docs.length - 1]) {
setLastVisible(documents.docs[documents.docs.length - 1]);
}
};
For more information, See Add a simple cursor to a query.
I have to functions getDataOne and getDataTwo. How do I combine below into one function, using fetch(), useState and useEffect?
const MyComponent = () => {
const [loading, setLoading] = useState(false);
const [dataOne, setDataOne] = useState<Data[]>([]);
const [dataTwo, setDataTwo] = useState<Data[]>([]);
const getDataOne = async () => {
setLoading(true);
const result = await fetch(
"https://my-api-link-one"
);
const jsonResult = await result.json();
setLoading(false);
setDataOne(jsonResult);
};
const getDataTwo = async () => {
setLoading(true);
const result = await fetch(
"https://my-api-link-two"
);
const jsonResult = await result.json();
setLoading(false);
setDataTwo(jsonResult);
};
useEffect(() => {
getDataOne();
getDataTwo();
}, []);
Update:
I set it up using Promise.all
const [loading, setLoading] = useState(false);
const [dataOne, setDataOne] = useState<DataOne[]>([]);
const [dataTwo, setDataTwo] = useState<DataTwo[]>([]);
const [data, setData] = useState<DataOne[] & DataTwo>([]);
const urls = [
"https://url-one", "https://url-two",
];
const getData = async () => {
setLoading(true);
const results = await Promise.all(
urls.map((url) => fetch(url).then((res) => res.json()))
);
setLoading(false);
setData(results);
console.log(data);
};
This is not totally working yet. How do I use useState now correctly (and handle both data from urls)? In the end I want to have one data variable so I can map over this variable:
{data.map((item) => {
return (
// etc
So, Promise.all() accepts an array of promises, so naturally Promise.all() returns an array only. So even though your results variable still is an array I would recommend destructuring it because in this case there are only two API fetches involved. Looking at your update, I think there's only small modifications left which are as follows :
const urls = ["https://url-one", "https://url-two",];
const getData = async () => {
setLoading(true);
const [result1, result2] = await Promise.all(
urls.map((url) => fetch(url).then((res) => res.json()))
);
setLoading(false);
setDataOne(result1);
setDataTwo(result2);
console.log(data);
};
You can use Promise.all. Read more here https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/all.
const getData = () => {
setLoading(true);
Promise.all([fetch('api-1'), fetch('api-2')]).then(results => {
setDataOne(results[0]);
setDataTwo(results[1]);
}).finally(() => setLoading(false));
}
Utilize .flat() to reformat the data array returned from the Promise.all() into your state which holds the response obj/array,
Promise.all(
urls.map(url =>
fetch(url).then(e => e.json())
)
).then(data => {
finalResultState = data.flat();
});