How to get all documents in a collection in firestore? - javascript

I am trying to get all documents in my collection but the log return an empty array and i am having an error message that says cannot read property of undefined reading forEach. i have followed the documentation but can't find where the issue is. Can someone help, please?
The code snippet below is a custom hook i am using it in my index.js as per following. this log return an empty array.
const { docs } = useFireStore('barbers')
console.log('docs',docs)
import { useState, useEffect } from "react";
import { collection, getDocs, querySnapshot } from "firebase/firestore";
import {db} from '../../../base'
export const useFireStore = (mycollection) => {
const [docs, setdocs] = useState([])
useEffect(() => {
const unsub = async () => {
await getDocs(collection(db,mycollection))
querySnapshot.forEach((doc) => {
let document =[];
// doc.data() is never undefined for query doc snapshots
document.push({...doc.data() ,id: doc.id});
//console.log(doc.id, " => ", doc.data());
});
setdocs(document);
}
return () => unsub();
}, [mycollection])
return { docs };
}

The query snapshot, if I'm not mistaken, is the return value you waited for when you called getDocs. You are also redeclaring the document array each time in the forEach callback, it should declared outside the loop, along with the setDocs state updater function.
export const useFireStore = (mycollection) => {
const [docs, setDocs] = useState([]);
useEffect(() => {\
const unsubscribe = async () => {
const querySnapshot = await getDocs(collection(db,mycollection));
const document =[];
querySnapshot.forEach((doc) => {
document.push({
...doc.data(),
id: doc.id
});
});
setdocs(document);
}
return unsubscribe;
}, [mycollection]);
return { docs };
}

Drew's answer gets you the documents once, so 🔼.
If you want to listen for updates to the documents however, and show those in your UI, use onSnapshot instead of getDocs:
export const useFireStore = (mycollection) => {
const [docs, setdocs] = useState([])
useEffect(() => {
const unsub = onSnapshot(collection(db, mycollection), (querySnapshot) => {
const documents = querySnapshot.docs.map((doc) => {
return {
...doc.data(),
id: doc.id
}
});
setdocs(documents);
});
return () => unsub();
}, [mycollection])
}
This:
Uses onSnapshot instead of getDocs so that you also listen for updates to the data.
No longer returns the docs state variable, as that seems error prone.
Now correctly returns a function that unsubscribes the onSnapshot listener.

Related

Getting Error: Rendered more hooks than during the previous render

When I load my Nextjs page, I get this error message: "Error: Rendered more hooks than during the previous render."
If I add that if (!router.isReady) return null after the useEffect code, the page does not have access to the solutionId on the initial load, causing an error for the useDocument hook, which requires the solutionId to fetch the document from the database.
Therefore, this thread does not address my issue.
Anyone, please help me with this issue!
My code:
const SolutionEditForm = () => {
const [formData, setFormData] = useState(INITIAL_STATE)
const router = useRouter()
const { solutionId } = router.query
if (!router.isReady) return null
const { document } = useDocument("solutions", solutionId)
const { updateDocument, response } = useFirestore("solutions")
useEffect(() => {
if (document) {
setFormData(document)
}
}, [document])
return (
<div>
// JSX code
</div>
)
}
useDocument hook:
export const useDocument = (c, id) => {
const [document, setDocument] = useState(null)
const [isLoading, setIsLoading] = useState(true)
const [error, setError] = useState(null)
useEffect(() => {
const ref = doc(db, c, id)
const unsubscribe = onSnapshot(
ref,
(snapshot) => {
setIsLoading(false)
if (snapshot.data()) {
setDocument({ ...snapshot.data(), id: snapshot.id })
setError(null)
} else {
setError("No such document exists")
}
},
(err) => {
console.log(err.message)
setIsLoading(false)
setError("failed to get document")
}
)
return () => unsubscribe()
}, [c, id])
return { document, isLoading, error }
}
You cannot call a hook, useEffect, your custom useDocument, or any other after a condition. The condition in your case is this early return if (!router.isReady) returns null. As you can read on Rules of Hooks:
Don’t call Hooks inside loops, conditions, or nested functions. Instead, always use Hooks at the top level of your React function, before any early returns...
Just remove that if (!router.isReady) returns null from SolutionEditForm and change useDocument as below.
export const useDocument = (c, id) => {
const [document, setDocument] = useState(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
if (!id) return; // if there is no id, do nothing 👈🏽
const ref = doc(db, c, id);
const unsubscribe = onSnapshot(
ref,
(snapshot) => {
setIsLoading(false);
if (snapshot.data()) {
setDocument({ ...snapshot.data(), id: snapshot.id });
setError(null);
} else {
setError("No such document exists");
}
},
(err) => {
console.log(err.message);
setIsLoading(false);
setError("failed to get document");
}
);
return () => unsubscribe();
}, [c, id]);
return { document, isLoading, error };
};
The if (!router.isReady) return null statement caused the function to end early, and subsequent hooks are not executed.
You need to restructure your hooks such that none of them are conditional:
const [formData, setFormData] = useState(INITIAL_STATE)
const router = useRouter()
const { solutionId } = router.query
const { document } = useDocument("solutions", solutionId, router.isReady) // pass a flag to disable until ready
const { updateDocument, response } = useFirestore("solutions")
useEffect(() => {
if (document) {
setFormData(document)
}
}, [document])
// Move this to after the hooks.
if (!router.isReady) return null
and then to make useDocument avoid sending extra calls:
export const useDocument = (c, id, enabled) => {
and updated the effect with a check:
useEffect(() => {
if (!enabled) return;
const ref = doc(db, c, id)
const unsubscribe = onSnapshot(
ref,
(snapshot) => {
setIsLoading(false)
if (snapshot.data()) {
setDocument({ ...snapshot.data(), id: snapshot.id })
setError(null)
} else {
setError("No such document exists")
}
},
(err) => {
console.log(err.message)
setIsLoading(false)
setError("failed to get document")
}
)
return () => unsubscribe()
}, [c, id, enabled])
UseEffect cannot be called conditionally
UseEffect is called only on the client side.
If you make minimal representation, possible to try fix this error

How to detect transaction confirmation with web3.js and re-render with React

Context: The app is a simple on-chain todo list that allows you to see a list of todos and create new todos.
Problem: When I create new todos and send it onchain using createTask(), I am struggling to check for transaction confirmation, which then allows me to re-render the todo list to display the new input submitted and confirmed onchain.
Tech stack: Web3.js + React
For your reference: I have been following this tutorial: https://www.dappuniversity.com/articles/ethereum-dapp-react-tutorial.
import './App.css';
import Web3 from 'web3'
import { useEffect, useState } from 'react'
import { TODOLIST_ABI, TODOLIST_ADDRESS } from './config'
import Tasks from './Tasks'
import CreateTasks from './CreateTasks'
const App = () => {
const [acct, setAcct] = useState('')
const [contract, setContract] = useState([])
const [task, setTask] = useState([])
const [taskCount, setTaskCount] = useState(0)
const [loading, setLoading] = useState(false)
const loadBlockchainData = async () => {
setLoading(true)
const provider = window.ethereum
try {
const web3 = await new Web3(provider)
const acc = await (await web3.eth.requestAccounts())[0]
setAcct(acc)
const todo_list = new web3.eth.Contract(TODOLIST_ABI, TODOLIST_ADDRESS)
setContract(todo_list)
const taskCount = await todo_list.methods.taskCount().call()
setTaskCount(taskCount)
for (var i = 1; i <= taskCount; i++) {
// methods.mymethod.call - call constant method sithout sending any transaction
const temp_task = await todo_list.methods.tasks(i).call()
setTask(t => {return [...t, temp_task]})
}
setLoading(false)
} catch (error) {
console.log(`Load Blockchain Data Error: ${error}`)
}
}
const loadTasks = async () => {
const taskCount = await contract.methods.taskCount().call()
setTaskCount(taskCount)
setTask(() => [])
for (var i = 1; i <= taskCount; i++) {
// methods.mymethod.call - call constant method sithout sending any transaction
const temp_task = await contract.methods.tasks(i).call()
setTask(t => {return [...t, temp_task]})
}
}
const createTask = async (text) => {
setLoading(true)
console.log(`onsubmit: ${text}`)
await contract.methods.createTask(text).send({from: acct}).once('sent', r => {
console.log(`Transaction Hash: ${r['transactionHash']}`)
loadTasks()
})
setLoading(false)
}
useEffect(() => {
loadBlockchainData()
}, [])
return (
<>
<h1>Hello</h1>
<p>Your account: { acct }</p>
{loading? (<p>loading...</p>) : (<Tasks task={ task }/>)}
<CreateTasks contract={ contract } account={ acct } createTask={ createTask }/>
</>
)
}
export default App;
const taskCount = await todo_list.methods.taskCount().call()
in case taskCount=undefined, it will be good practice to run the for-loop inside if statement
if(taskCount){//for-loop here}
since you are calling the contract method multiple times in a sequence, you are are getting promises each time and one of those promises might get rejected. Imagine the scenario your taskCount is 10 but when i=5 your promise rejected and you get out of loop and catch block runs. In this case you would have only previous tasks captured. To prevent this, you should implement either all promises resolved or none resolves. (atomic transaction)
In this case you should be using Promise.all
if(taskCount){
Promise.all(
// because taskCount is string.convert it to number
Array(parseInt(taskCount))
.fill() // if taskCount=2, so far we got [undefined,undefined]
.map((element,index)=>{
const temp_task = await todo_list.methods.tasks(index).call()
setTask(t => {return [...t, temp_task]})
})
)
}

UseEffect hook with firebase query only render the initial empty array even after the array is not empty anymore

const ID = useSelector((state) => state.userDetail.userID);
const [postItem, setpostItem] = useState([]);
useEffect(() => {
const q = query(
collection(firestore, "latestPost"),
where("userID", "==", ID)
);
onSnapshot(q, (querySnapshot) => {
querySnapshot.forEach((doc) => {
postItem.push({ ...doc.data(), id: doc.id });
});
});
},
[]);
I wanted to read data from firebase and set my useState with an empty array and even after the data is in the array, the screen still renders an empty array, I need to manually refresh the screen to see the change.
You shouldn’t be pushing directly to postItem. You should be using setPostItem. Also I think you need to add postItem to the end of useEffect in the square brackets.
So maybe something like
useEffect(()=> {
const q = query(
collection(firestone, "latestPost"),
where("userId", "==", ID)
);
onSnapshot(q, (querySnapshot) => {
querySnapShot.forEach((doc) => {
setPostItem([...postItem, {...doc.data(), id:doc.id}])
});
});
}, [postItem]);
Basically that means useEffect will run again once you've setPostItem and also because useState has changed it will refresh the page.
I think that was how it is designed. However someone please correct me if my logic is incorrect!
The only way that react knows that it needs to render again is if you setState. Mutating the existing array will not cause a rerender. Also, don't forget to return the unsubscribe function so your snapshot listener can be torn down when the component unmounts:
useEffect(() => {
const q = query(
collection(firestone, "latestPost"),
where("userId", "==", ID)
);
const unsubscribe = onSnapshot(q, (querySnapshot) => {
const newItems = querySnapshot.docs.map(doc => ({
...doc.data(),
id: doc.id
});
setpostItem(newItems);
});
return unsubscribe;
}, []);
I'm going to fix the updates don't rendering when passing an array in setState, see the comment line:
useEffect(() => {
const q = query(
collection(firestone, "latestPost"),
where("userId", "==", ID)
);
const unsubscribe = onSnapshot(q, (querySnapshot) => {
const newItems = querySnapshot.docs.map(doc => ({
...doc.data(),
id: doc.id
});
setpostItem([...newItems]); //this now re render the page
});
return unsubscribe;
}, []);

How do I make useState hook work with my function?

I am trying to execute a function to update a setState but it as well needs other state to load first.
const [chatsIds, setChatsIds] = useState([]);
const [chats, setChats] = useState([]);
useEffect(() => {
getChatsIds();
}, []);
useEffect(() => {
getChats();
}, [chats]);
the "getChats" needs the value from "chatsIds" but when the screen is loaded the value isn't , only when i reload the app again it gets the value.
Here are the functions :
const getChatsIds = async () => {
const ids = await getDoc(userRef, "chats");
setChatsIds(ids);
}
const getChats = async () => {
const chatsArr = [];
chatsIds.forEach(async (id) => {
const chat = await getDoc(doc(db, "Chats", id));
chatsArr.push(chat);
console.log(chatsArr);
});
setChats(chatsArr);
}
I've tried with the useEffect and useLayoutEffect hooks, with promises and async functions, but i haven't found what i'm doing wrong :(
The problem is in your useEffect hook dependency. It should depends on chatsIds not chats.
useEffect(() => {
getChats();
}, [chatsIds]);
Which mean fetching chatsIds should depend on first mount and fetching chats should depend on if chatsIds is chnaged.
You simply change the useEffect hook to like below.
useEffect(() => {
getChatsIds();
}, [chatsIds]);
I Think getChat() is depend on chatIds...
so you use useEffect with chatIds on dependency
const [chatsIds, setChatsIds] = useState([]);
const [chats, setChats] = useState([]);
useEffect(() => {
getChatsIds();
}, []);
useEffect(() => {
getChats(chatsIds);
}, [chatsIds]);
const getChatsIds = async () => {
const ids = await getDoc(userRef, "chats");
setChatsIds(ids);
}
const getChats = async (chatsIds) => {
const chatsArr = [];
chatsIds.forEach(async (id) => {
const chat = await getDoc(doc(db, "Chats", id));
chatsArr.push(chat);
console.log(chatsArr);
});
setChats(chatsArr);
}

chaining promise where 2nd promise is dependent on the result of the first

I am having a lot of trouble chaining two promises together in my react native app. The first promise successfully gets a value in Async storage and sets it to the state. The second promise gets the data I have from Firebase, but is dependent on the state that I set from the first promise. Any help would be greatly appreciated.
import React, { useState, useEffect } from "react";
import { Text, StyleSheet, View } from "react-native";
import firebase from "../../firebase/fbConfig";
import AsyncStorage from "#react-native-async-storage/async-storage";
let DB = firebase.firestore();
function Questions(props) {
const [productId, setProductId] = useState("");
const [question, setQuestion] = useState("");
const [reward, setReward] = useState("");
const getAsyncData = () => {
AsyncStorage.getItem("key").then((value) => setProductId(value))
// works fine //
};
const getDataFromFirebase = (productId) => {
DB.collection("ads")
.where("productId", "==", productId)
.get()
.then(function (querySnapshot) {
querySnapshot.forEach(function (doc) {
setQuestion(doc.data().question);
setReward(doc.data().reward);
});
})
.catch(function (error) {
console.log("Error getting documents: ", error);
});
// works fine //
};
useEffect(() => {
getAsyncData().then((productId) => getDataFromFirebase(productId));
// does not work //
});
return (
<>
<View style={styles.container}>
</View>
</>
);
}
export default Questions;
Try this way
useEffect(() => {
getAsyncData();
}, []);
const getAsyncData = async () => {
try {
const productId = await AsyncStorage.getItem("key");
getDataFromFirebase(productId);
} catch (error) {
console.log(error);
}
};

Categories