update React state after fetching referenced document - reactjs

I have a simple React App using Firestore.
I have a document in Firestore:
{
date: November 20, 2022 at 11:24:44 AM UTC+1,
description: "...",
title: "Dummy title",
type: "student",
userRef: /users/0AjB4yFFcIS6VMQMi7rUnF3eJXk2
}
Now I have a custom hook, that fetches the data:
export const useAnnouncements = () => {
const [announcements, setAnnouncements] = useState([]);
useEffect(() => {
getAnnouncements().then((documents) => {
const documentsList = [];
documents.forEach((doc) => {
const document = { id: doc.id, ...doc.data() };
getUser(document.userRef).then((u) => {
document.user = u.data(); // <-- HERE is problem
});
documentsList.push(document);
setAnnouncements(documentsList);
});
});
}, []);
return [announcements];
};
Problem is that I have a REFERENCE field type, and it has to be fetched separately. Result? My list is populated, but first without user. Later, when the users' data is fetched, the state is not being updated.
How to deal with React + Firestore's reference field?

Array.prototype.forEach is not designed for asynchronous code. (It was not suitable for promises, and it is not suitable for async-await.) instead you can use map.
useEffect(() => {
getAnnouncements().then((documents) => {
const promises = documents.map((doc) => {
return getUser(doc.userRef).then((u) => {
const document = { id: doc.id, user: u.data(), ...doc.data() };
return document;
});
});
Promise.all(promises).then((documentsList) => {
setAnnouncements(documentsList);
});
});
}, []);

I think you need to wait for all the data to be fetched
export const useAnnouncements = () => {
const [announcements, setAnnouncements] = useState([]);
useEffect(() => {
let isValidScope = true;
const fetchData = async () => {
const documents = await getAnnouncements();
if (!isValidScope) { return; }
const allPromises = documents?.map(doc => {
return getUser(doc.userRef)
.then(user => {
return {
id: doc.id,
...doc.data(),
user: user.data()
}
})
}
const documentsList = await Promise.all(allPromises);
if (!isValidScope) { return; }
setAnnouncements(documentsList);
}
fetchData()
return () => { isValidScope = false }
}, []);
return [announcements];
};
Hope it helps in some way

Related

How to update state variable in multiple api fetch?

I am trying to update state variable between multiple API call but problem I am facing is variable stores only last updated value not all the value.
const [dataSet, setDataSet] = useState({});
const onDataSetChange = () => {
console.log(dataSet)
};
useEffect(onDataSetChange, [dataSet]);
const initializeData = () => {
getData();
};
useEffect(initializeData, []);
const getData = () => {
dispatch(
fetchData("123", (response) => {
response.data.forEach((dataItem) => {
getDataDetail(dataItem);
});
})
);
};
const getDataDetail = (dataItem) => {
dispatch(
fetchDataDetail(dataItem.id, (response) => {
const modifiedDataSet = { ...dataSet };
modifiedDataSet[dataItem.id] = {
label: dataItem.name,
data: response.data,
};
setDataSet(modifiedDataSet);
})
);
};
In console log I am getting
{}
{"data1":{label:"dataItem1",data:{}}
{"data2":{label:"dataItem2",data:{}}
And what I am expecting is
{}
{"data1":{label:"dataItem1",data:{}}
{
"data1":{label:"dataItem1",data:{}
"data2":{label:"dataItem2",data:{}
}

Hi, i'm retrieving data from firestore, and checking whether to direct the user to index page or to enter details for a new user But not able to do so

React code
import React, { useEffect, useState } from "react";
import { getDocs, collection } from "firebase/firestore";
import { auth, db } from "../firebase-config";
import { useNavigate } from "react-router-dom";
function Load() {
const navigate = useNavigate();
const [accountList, setAccountList] = useState([]);
const [hasEmail, setHasEmail] = useState(false);
const accountRef = collection(db, "accounts");
Am i using useEffect correctly?
useEffect(() => {
const getAccounts = async () => {
const data = await getDocs(accountRef);
setAccountList(
data.docs.map((doc) => ({
...doc.data(),
id: doc.id,
}))
);
};
getAccounts();
emailCheck();
direct();
}, []);
checking whether email exists
const emailCheck = () => {
if (accountList.filter((e) => e.email === auth.currentUser.email)) {
setHasEmail(true);
} else {
setHasEmail(false);
}
};
Redirecting based on current user
const direct = () => {
if (hasEmail) {
navigate("/index");
} else {
navigate("/enterdetails");
}
};
return <div></div>;
}
The code compiles but doesn't redirect properly to any of the pages.
What changes should I make?
First question posted excuse me if format is wrong.
There are two problems here:
useEffect(() => {
const getAccounts = async () => {
const data = await getDocs(accountRef);
setAccountList(
data.docs.map((doc) => ({
...doc.data(),
id: doc.id,
}))
);
};
getAccounts();
emailCheck();
direct();
}, []);
In order:
Since getAccounts is asynchronous, you need to use await when calling it.
But even then, setting state is an asynchronous operation too, so the account list won't be updated immediately after getAccounts completes - even when you use await when calling it.
If you don't use the accountList for rendering UI, you should probably get rid of it as a useState hook altogether, and just use regular JavaScript variables to pass the value around.
But even if you use it in the UI, you'll need to use different logic to check its results. For example, you could run the extra checks inside the getAccounts function and have them use the same results as a regular variable:
useEffect(() => {
const getAccounts = async () => {
const data = await getDocs(accountRef);
const result = data.docs.map((doc) => ({
...doc.data(),
id: doc.id,
}));
setAccountList(result);
emailCheck(result);
direct();
};
getAccounts();
}, []);
const emailCheck = (accounts) => {
setHasEmail(accounts.some((e) => e.email === auth.currentUser.email));
};
Alternatively, you can use a second effect that depends on the accountList state variable to perform the check and redirect:
useEffect(() => {
const getAccounts = async () => {
const data = await getDocs(accountRef);
setAccountList(
data.docs.map((doc) => ({
...doc.data(),
id: doc.id,
}))
);
};
getAccounts();
});
useEffect(() => {
emailCheck();
direct();
}, [accountList]);
Now the second effect will be triggered each time the accountList is updated in the state.

How to prevent object undefined in React

I have react app requeting a flask server, I can return the objects but when I assing the state to a new variable it log undefined, even though I am able to log it
const [characters, setCharacters] = useState([]);
useEffect(() => {
const getData = async () => {
await fetch("http://127.0.0.1:6789/api/load_img_data")
.then((res) => res.json())
.then(
(result) => {
const arrayOfObj = Object.entries(result.imgData).map((e) => e[1]);
setCharacters(arrayOfObj);
},
(error) => {}
);
};
getData();
}, []);
console.log(characters); ## it works fine and log the object on the console
const columnsFromBackend = {
["1"]: {
name: "Terminator Group",
items: characters, ## doesn't work when I map over it as it will be always empty
}
}
so my question what it the best way to assign a state to variable? thanks
You can declare your columnsFromBacked and initialize it as empty object.After you data from api is stored in the hook, then you can assign the appropriate values to columnsFromBacked
Solution 1
let columnsFromBackend= {}
const [characters, setCharacters] = useState([]);
useEffect(() => {
const getData = async () => {
await fetch("http://127.0.0.1:6789/api/load_img_data")
.then((res) => res.json())
.then(
(result) => {
const arrayOfObj = Object.entries(result.imgData).map((e) => e[1]);
setCharacters(arrayOfObj);
columnsFromBackend = {
["1"]: {
name: "Terminator Group",
items: characters
}
}
},
(error) => {}
);
};
getData();
}, []);
}
Solution 2
You can implement useEffect hook with dependency on character hook.
sample code
useEffect(()=>{
columnsFromBackend = {...} //your code
}, [character]);

Cannot use fetched data from Firestore 9, inside useEffect using Context and useReducer

I'm trying to build a react app using Firestore 9 and Context API with useReducer.
I'm stuck in a point where I'm trying to fetch data from Firestore inside the Context.js, and even though I know that the data is there and getting stored inside the state when I try to access it from another component, I'm getting an empty array.
Here's the useEffect inside Context.js
useEffect(() => {
const categoryList = [];
const fetchTasks = async () => {
console.log("Fetching taks..");
try {
const categories = [];
categoryList.forEach(async (category) => {
const ref = collection(db, "categories", category.id, "tasks");
const snapshot = await getDocs(ref);
const tasks = [];
snapshot.docs.forEach((doc) => {
tasks.push({
...doc.data(),
id: doc.id,
});
});
categories.push({ ...category, tasks: tasks });
});
dispatch({ type: "SET_CATEGORIES", payload: categories });
} catch (err) {
console.log(err);
}
};
const fetchData = async () => {
try {
const ref = collection(db, "categories");
const snapshot = await getDocs(ref);
snapshot.docs.forEach((doc) => {
categoryList.push({
...doc.data(),
id: doc.id,
});
});
} catch (err) {
console.log(err);
}
};
fetchData().then(() => fetchTasks());
}, []);
Then I'm trying to load categories inside Main.jsx component like this:
function Main() {
const { loading, categories } = useContext(TasksContext);
if (loading) {
return <h3>Loading...</h3>;
} else {
return (
<div className="flex gap-10 flex-col px-4">
{categories.map((category) => {
return <TasksList key={category.id} category={category} />;
})}
</div>
);
}
}
And here's the TasksReducer :
const tasksReducer = (state, action) => {
switch (action.type) {
case "SET_CATEGORIES": {
return {
...state,
loading: false,
categories: action.payload,
};
default:
return state;
}
};
I don't know what is the best practice to fetch a collections with their sub-collections and then merge them together.
I know it's easier to fetch data inside individual components, but I need the data in multiple places.

Rerendering a component - ReactJS, Axios

After calling canBookSlot I want to update the slotsList I figure i have to make a new Axios request, can i reuse the useEffect whitin the then() method to rerender the component with updated properties or is there any other smart way of doing it without rewriting the Axios request?
useEffect(() => {
Axios.post("http://localhost:3001/api/get/week1/ex").then((response) => {
setSlotsList(response.data);
});
}, []);
let userDetailsString = localStorage.getItem("userDetails");
const userDetailsObj = JSON.parse(userDetailsString);
const canBookSlot = (id) => {
if (userDetailsObj.canBook != 0) {
Axios.post("http://localhost:3001/api/book/week1/ex", {
room: userDetailsObj.room,
id: id.id + 1,
}).then(); // update the slotsList
}
};
EDIT:
The userDetailsObj is an object from another component, It isn't the same object as the ones in slotList how do i go about rerendering userDetailsObj
const updateData = () => {
Axios.post("http://localhost:3001/api/get/week1/ex").then((response) => {
setSlotsList(response.data);
});
}
useEffect(() => {
updateData();
}, []);
let userDetailsString = localStorage.getItem("userDetails");
let userDetailsObj = JSON.parse(userDetailsString);
const canBookSlot = (id) => {
if (userDetailsObj.canBook != 0) { // Always true
Axios.post("http://localhost:3001/api/book/week1/ex", {
room: userDetailsObj.room,
id: id.id + 1,
}).then(() => updateData())
}
};
You can create common function and reuse when you want to call that axios api and update the data.
updateData = () => {
Axios.post("http://localhost:3001/api/get/week1/ex").then((response)
=> {
setSlotsList(response.data);
});
}
useEffect(() => {
updatedData();
}, []);
let userDetailsString = localStorage.getItem("userDetails");
const userDetailsObj = JSON.parse(userDetailsString);
const canBookSlot = (id) => {
if (userDetailsObj.canBook != 0) {
Axios.post("http://localhost:3001/api/book/week1/ex", {
room: userDetailsObj.room,
id: id.id + 1,
}).then(() => updateData()); // update the slotsList
}
};

Resources