React useEffect with async/await issue - reactjs

I am trying to add some properties to an array of objects inside useEffect but it renders DOM while those fields are still not present. Is there any reliable way how to wait till those properties are added:
useEffect hook look like this:
useEffect(() => {
onSnapshot(query(collection(db, "conversations"), where('canRead', 'array-contains', user.user.uid), orderBy("lastMsgDate", 'desc')),
async (snapshot) => {
let conversations = snapshot.docs.map(doc => toConversation(doc, user.user.uid));
await conversations.map(async (convo, index) => {
const profile = await getDoc(doc(db, "users", convo.canRead[0]))
conversations[index].picture = profile.data().picture
conversations[index].name = profile.data().name
})
setConversations(conversations)
})
}, []);
This is how I am rendering list of convos:
<IonCard>
{conversations.length > 0 ?
conversations.map((conversation) =>
<IonItem key={conversation.id}>
<IonAvatar slot='start'>
<img src={conversation.picture ? conversation.picture : '/assets/default-profile.svg'} alt={conversation.name} />
</IonAvatar>
<IonLabel>{conversation.name}</IonLabel>
</IonItem>
)
:
<IonCardContent>
no convos
</IonCardContent>
}
</IonCard>
the name and picture does not render even i can see it when log array into console
0:{
canRead: ['iwPmOBesFQV1opgs3HT9rYPF7Sj1'],
id: "W6cefXGoBAZijPof8jVl",
lastMsg: "test",
lastMsgDate: nt {seconds: 1668418292, nanoseconds: 281000000},
lastMsgSender: "Hyw4Argt8rR25mFaFo1Sl4iAWoM2",
name: "Warren"
picture: "https://firebasestorage.googleapis.com/v0/b/..."
users: {
Hyw4Argt8rR25mFaFo1Sl4iAWoM2: true,
iwPmOBesFQV1opgs3HT9rYPF7Sj1: true
}
}
Any help appreciated

You can use a loading message or a gif.
const [loading, setLoading] = useState(true);
useEffect(() => {
onSnapshot(query(collection(db, "conversations"), where('canRead', 'array-contains', user.user.uid), orderBy("lastMsgDate", 'desc')),
async (snapshot) => {
....
setConversations(conversations);
setLoading(false);
})
}, []);
if(loading) return <p>Loading...</p>
return <> your content </>

for some reason it works with setTimeout function which is not the best solution
useEffect(() => {
setLoading({ loading: true, loadingMsg: 'Loading conversations' })
onSnapshot(query(collection(db, "conversations"), where('canRead', 'array-contains', user.user.uid), orderBy("lastMsgDate", 'desc')),
async (snapshot) => {
let convos = snapshot.docs.map(doc => toConversation(doc, user.user.uid));
await convos.map(async (convo, index) => {
const profile = await getDoc(doc(db, "users", convo.canRead[0]))
convos[index].picture = profile.data().picture
convos[index].name = profile.data().name
})
setTimeout(() => {
setConversations(convos)
setLoading({ loading: false, loadingMsg: undefined })
}, 1000);
})
}, [user.user.uid]);

Maybe u can add a loading state? something like
const [loading, setLoading] = useState(false);
const [conversations,setConversations] = useState(null);
const onSnapshot = async () => {
setLoading(true)
onSnapshot(
query(
collection(db, "conversations"),
where("canRead", "array-contains", user.user.uid),
orderBy("lastMsgDate", "desc")
),
async (snapshot) => {
let conversations = snapshot.docs.map((doc) =>
toConversation(doc, user.user.uid)
);
await conversations.map(async (convo, index) => {
const profile = await getDoc(doc(db, "users", convo.canRead[0]));
conversations[index].picture = profile.data().picture;
conversations[index].name = profile.data().name;
});
setConversations(conversations);
}
);
setLoading(false);
};
useEffect(() => {
onSnapshot()
},[])
return loading ? <span>loading...</span> : <div>{conversations.map((con,i) => <span key={i}>con</span>)}</div>

Related

How to automatically update data after a change

I am making a small application for writing words, using a Firestore Cloud for data storage. I use onSubmit to send data to Firestore for storage.
Everything works and is saved to the cloud, but the data is not updated on the page, for which you need to reload the page and after that the data will already be updated.
How can I solve this problem, do I need to render a new array every time and get data from the server in order to update the data (fetchProduct), or is it possible in some other way?
function Menu() {
const [data, setData] = useState([]);
const [isLoading, setIsLoading] = useState(true);
const { user } = UserAuth();
const fetchProduct = async () => {
const ref = collection(db, "langcards-db");
const q = query(ref, where("author", "==", user.uid));
const querySnapshot = await getDocs(q);
const arr = [];
querySnapshot.forEach((doc) => {
arr.push({
...doc.data(),
id: doc.id,
});
});
setData(arr);
setIsLoading(false);
};
useEffect(() => {
if (user) {
fetchProduct();
} else {
setData([]);
setIsLoading(false);
}
}, []);
useEffect(() => {
if (user) {
fetchProduct();
} else {
setData([]);
setIsLoading(false);
}
}, [user]);
const onSubmit = async (e) => {
e.preventDefault();
let rw = word.replace(/\s/g, "");
let rt = translate.replace(/\s/g, "");
if (rw.length >= 1 && rt.length >= 1) {
setWarn(false);
await addDoc(collection(db, "langcards-db"), {
word: word,
translate: translate,
note: note,
category: "category",
author: user.uid,
});
setWord("");
setTranslate("");
setNote("");
console.log(data)
} else {
setWarn(true);
}
};
return (
<div className="main-inner">
{isLoading ? (
<Loader />
) : (
<div className="content">
<Routes>
<Route
path="addcard"
element={
<AddCard
onChangeWord={onChangeWord}
onChangeNote={onChangeNote}
onChangeTranslate={onChangeTranslate}
onSubmit={onSubmit}
word={word}
translate={translate}
note={note}
warn={warn}
resetFormAdd={resetFormAdd}
/>
}
/>
<Route
path="saved"
element={<Saved data={data} setData={setData} />}
/>
</Route>
</Routes>
</div>
);
}
export default Menu;
For this problem you should not use getDocs but onSnapshot which listens for changes in the data. Also, this way you can update the state directly and don`t have to create a new array each time. Then you can directly refer to .data when accessing an entry from the data state.
useEffect(() => {
const ref = collection(db, "langcards-db");
const q = query(ref, where("author", "==", user.uid));
const unsubscribe = onSnapshot(q, { includeMetadataChanges: true }, (querySnapshot) => {
setData([]); // Empty state
querySnapshot.forEach((doc) => {
// Do something with your data (append it to the data array like before)
setData(old => {...old, {"id" : doc.id, "data" : doc.data()});
});
});
return () => { unsubscribe(); };
}, [])
In this doc you can read more about how to fetch data from your firestore.

React not updating state?

I´m new to react. I´m trying to fetch an endpoints array. and I want to update the api's status every 15 seconds. I´m trying to do this
export const endpoints: string[] = [
"accounts/health/status",
"assets/health/status",
"customers/health/status",
"datapoints/health/status",
"devices/health/status",
"documents/health/status",
"forms/health/status",
"invites/health/status",
"media/health/status",
"messages/health/status",
"namespaces/health/status",
"orders/health/status",
"patients/health/status",
"relationships/health/status",
"rules/health/status",
"templates/health/status",
"users/health/status",
"workflows/health/status",
];
and I have this proxy in my package.json
"proxy": "https://api.factoryfour.com/",
Here the rest of my code
const [data, setData] = useState<Response[]>([]);
const [loading, setLoading] = useState<boolean>(false);
const [error, setError] = useState<string[] | null[]>([]);
const effectRan = useRef(false);
const fetching = async () => {
setLoading(true);
endpoints.map(async (endpoint) => {
return await axios
.get(endpoint)
.then((res) => {
setData((prev) => [...prev, res.data]);
})
.catch((err) => {
setError([...error, err.message]);
});
});
setLoading(false);
};
useEffect(() => {
if (!effectRan.current) {
fetching();
}
return () => {
effectRan.current = true;
};
});
useEffect(() => {
setTimeout(async () => {
setData([]);
setLoading(true);
setError([]);
await fetching();
}, 15000);
}, []);
but when the seTimeout runs every card duplicates and the state gets more data than before. even though I´m reseting the state to setData([]) I just want to update the api's status. What can i do?
if (loading) return <Spinner />;
return (
<div className="card-container">
{data.length ? (
data.map((item) => {
return (
<Card
key={generateKey()}
hostname={item.hostname}
message={item.message}
success={item.success}
time={item.time}
/>
);
})
) : (
<Spinner />
)}
{error.length
? error.map((err) => (
<ErrorCard key={generateKey()} message={err as string} />
))
: null}
</div>
```
Theres a few things wrong here and one or more probably fixes it:
You keep a ref around to track the first fetch but theres no need as you can do that by virtue of using [] in an effects deps array, which you already have.
The loading state does not wait until all requests in flight finished.
The 15 second interval does not wait until all requests launched are finished.
You dont clear down the timer if the component unmounts and remounts.
The data is not keyed against the endpoint which could land you in trouble if using React strictmode that runs affects twice in dev mode.
Your code, by design it seems, does append data each time one of the requests comes back -- but I think that was intentional?
const [data, setData] = useState<Record<string, Response>>({});
const [loading, setLoading] = useState<boolean>(false);
const [error, setError] = useState<Record<string, string | null>>({});
const fetching = async () => {
setLoading(true);
await Promise.all(
endpoints.map((endpoint) => {
return axios
.get(endpoint)
.then((res) => {
setData((prev) => ({...prev, [endpoint]: res.data}));
})
.catch((err) => {
setError((prev) => ({...prev, [endpoint]: err.message}));
});
})
);
setLoading(false);
};
useEffect(() => {
let timer: number | null = null;
const intervalFetch = async () => {
await fetching();
timer = setTimeout(async () => {
setError({});
setData({});
intervalFetch();
}, 15000);
};
intervalFetch();
return () => timer !== null && clearTimeout(timer);
}, []);
if (loading) return <Spinner />;
return (
<div className="card-container">
{Object.values(data).length ? (
Object.values(data).map((item) => {
return (
<Card
key={generateKey()}
hostname={item.hostname}
message={item.message}
success={item.success}
time={item.time}
/>
);
})
) : (
<Spinner />
)}
{Object.values(error).length
? Object.values(error).map((err) => (
<ErrorCard key={generateKey()} message={err as string} />
))
: null}
</div>)
I think this piece of code might be adding additional data instead of overwriting the existing one. Is that what you're trying to do?
setData((prev) => [...prev, res.data]);

Uncaught TypeError: can't access property "map", sizes is undefined

I am trying to map the prop sizes, that I'm saving in a state when the item has been loaded from the api, but I keep getting this error:
"Uncaught TypeError: can't access property "map", sizes is undefined"
const ItemDetailContainer = () => {
const { id } = useParams()
const [item, setItem] = useState({})
const [related, setRelated] = useState([])
const [sizes, setSizes] = useState([])
const [loading, setLoading] = useState(true)
const getRelated = () => {
const relatedItems = (productList.filter(product => product.category === item.category))
const clearRelated = relatedItems.filter(product => product.id !== item.id)
setRelated(clearRelated)
}
const setsizing = () => {
setSizes(item.sizes)
}
useEffect(() => {
customFetch(3000, productList.find(product => product.id == id))
.then(res => setItem(res))
.catch(error => console.log(error))
.finally(setLoading(false))
}, [])
//This is a solution to get the sizes and related items to load after the item has been loaded
useEffect(() => {
getRelated()
setsizing()
console.log(sizes);
}, [item])
return (
<>
loading ? <Spinner />
:
<ItemDetail
name={item.name}
price={item.price}
img={item.img}
stock={item.stock}
category={item.category}
description={item.description}
sizes={sizes}
related={related}
/>
</>
)
}
There are few mistakes in usage of React hook.
1. You should not access state variable as soon as you set the state. Because value is not reliable at all.
setsizing()
console.log(sizes); // This sizes is not updated value in Reactjs.
2. You should provide correct dependencies in your hooks and can remove unnecessary functions.
In the following code, you need to add productList at least.
useEffect(() => {
customFetch(3000, productList.find(product => product.id == id))
.then(res => setItem(res))
.catch(error => console.log(error))
.finally(setLoading(false))
}, [])
3. You can write one line code to get the related list.
Here is the updated code snippet you can refer to.
const ItemDetailContainer = () => {
const { id } = useParams()
const [item, setItem] = useState({})
const [related, setRelated] = useState([])
const [sizes, setSizes] = useState([])
const [loading, setLoading] = useState(true)
useEffect(() => {
customFetch(3000, productList.find(product => product.id == id))
.then(res => setItem(res))
.catch(error => console.log(error))
.finally(setLoading(false))
}, [productList])
//This is a solution to get the sizes and related items to load after the item has been loaded
useEffect(() => {
if (item && productList) {
const related = (productList.filter(product => product.category === item.category && product.id !== item.id))
setRelated(related);
setSizes(item.sizes);
}
}, [item, productList]);
return (
<>
loading ? <Spinner />
:
(item? <ItemDetail
name={item.name}
price={item.price}
img={item.img}
stock={item.stock}
category={item.category}
description={item.description}
sizes={sizes}
related={related}
/> : <div>Item does not exist!</div>)
</>
)
}

How to only return last api request (ReactJS)

Alternating between the 2 buttons will display first names or last names, but pressing them together really fast will chain requests and will combine the two. How can I make create a check, and only display the names from the button that was pressed last
export default function App() {
const [name, setName] = useState();
return (
<div className="App">
<button onClick={() => setName("first_name")}>1</button>
<button onClick={() => setName("last_name")}>2</button>
<Users name={name} />
</div>
);
}
export default function Users({ name }) {
const [users, setUsers] = useState([]);
useEffect(() => {
setUsers([]);
axios({
method: "GET",
url: `https://reqres.in/api/users?delay=1`
})
.then((res) => {
const allUsers = res.data.data.map((user) => <p>{user[name]}</p>);
setUsers((prev) => [...prev, ...allUsers]);
})
.catch((e) => {
console.log(e);
});
}, [name]);
return <div className="Users">{users}</div>;
}
Here is a great article by Dan Abramov about the useEffect hook in which he also talks about how to handle race cases- https://overreacted.io/a-complete-guide-to-useeffect/#speaking-of-race-conditions
To solve your issue, create a variable like let didCancel = false at the start of useEffect. Then, you have to return a function from useEffect, which automatically runs at the time when the name changes next time. In that function set didCancel to true. Now, you have to handle fetch response only if didCancel is false. This way, you are discarding all fetch responses received from second-last, third-last, etc. button presses, and handling fetch response only from the last button press.
Here is updated useEffect code:-
useEffect(() => {
let didCancel = false;
setUsers([]);
axios({
method: "GET",
url: `https://reqres.in/api/users?delay=1`
})
.then((res) => {
if (!didCancel) {
const allUsers = res.data.data.map((user) => <p>{user[name]}</p>);
setUsers((prev) => [...prev, ...allUsers]);
}
})
.catch((e) => {
console.log(e);
});
return () => {
didCancel = true;
};
}, [name]);
return <div className="Users">{users}</div>;
}
you have to create a loading state, and the user should not be able to send a new request until the data is received... you can create a hook for this or use SWR:
let me give you an example:
function Users(usersList) {
return (
<ul>
{usersList.map((user, key) => (
<li key={key}>{user}</li>
))}
</ul>
);
}
const useFetchUsers = (name) => {
const [isLoading, setIsLoading] = React.useState(true);
const [error, setError] = React.useState(null);
const [data, setData] = React.useState([]);
React.useEffect(() => {
setIsLoading(true);
setError(null);
fetch('https://blahblahblah.com/api/users')
.then((res) => res.json())
.then((response) => setData(response))
.catch((err) => setError(err))
.finally(() => setIsLoading(false));
}, [name]);
return {
isLoading,
error,
data,
};
};
function App() {
const [name, setName] = React.useState('Tom');
const { isLoading, error, data } = useFetchUsers(name);
const handleSubmitName = (name) => {
if (isLoading) alert('wait!');
else setName(name);
};
if (error) return <>an error occured</>;
if (data)
return (
<>
<button onClick={() => handleSubmitName('first_name')}>1</button>
<button onClick={() => handleSubmitName('last_name')}>2</button>
<Users name={name} />
</>
);
}
hint/note: it's just pseudocode and there are some tools to do data fetching + caching.
The problem is in this line setUsers((prev) => [...prev, ...allUsers]);. You are assuming that prev is [], but when the second request is resolve prev has data, that is why you see the request are combined:
I recommend to change your useEffect block to avoid the problem you are facing:
useEffect(() => {
axios({
method: "GET",
url: `https://reqres.in/api/users?delay=1`
})
.then((res) => {
const allUsers = res.data.data.map((user) => <p>{user[name]}</p>);
setUsers(...allUsers); //--> with the last name's value
})
.catch((e) => {
console.log(e);
});
}, [name]);

react hooks useState consuming object

I am not sure how to make it correctly so I can pass object to useState
const App = () => {
const [weatherData, setWeatherData] = useState({data: "", time: ""});
useEffect(() => {
axios.get(apiUrl).then(response => {
setWeatherData({...weatherData, data: response.data, time: timestamp});
});
}, []);
return <div>{weatherData && <Weather data={weatherData.data} />}</div>;
};
when I do the same just with useState() and setWeatherData(response.data) it works fine but I would like to add the time
Have you tried the following:
setWeatherData({
...response.data,
time: timestamp,
});
P.S. Let me know if I understood you correctly.
UPD
Other option, if you need to access the current state:
useEffect(() => {
axios.get(apiUrl).then(response => {
const timestamp = Date.now().timestamp;
setWeatherData((prevWeatherData) => ({
...prevWeatherData,
data: response.data,
time: timestamp,
}));
});
}, []);
Try this:
const App = () => {
const [weatherData, setWeatherData] = useState(null);
useEffect(() => {
async function fetchWeather () {
const response = await axios.get(apiUrl)
setWeatherData({data: response.data, time: new Date().getTime()});
}
fetchWeather()
}, [weatherData]);
return (
<>
{weatherData && <Weather data={weatherData.data} />}
</>
);
};

Resources