In useEffect, I retrieve the data from the server and store it in the "products" array:
const { url } = props;
const [products, setProducts] = useState([]);
useEffect(() => {
const fetchProducts = async () => {
setLoadingSpinner(true);
const response = await fetch(url);
const responseData = await response.json();
setLoadingSpinner(false);
const loadedProducts = [];
for (const key in responseData) {
loadedProducts.push({
id: key,
name: responseData[key].name,
description: responseData[key].description,
price: responseData[key].price,
image: responseData[key].image,
processor: responseData[key].processor,
});
}
setProducts(loadedProducts);
setIsDataLoaded(true);
};
fetchProducts();
}, [url, isDataLoaded]);
I pass it to the ProductDetail component:
<ProductDetail products={products}></ProductDetail>
I always get a null value in the ProductDetail component:
function ProductDetail(props) {
const params = useParams();
const [product, setProduct] = useState(null);
useEffect(() => {
if (props.products && params.productId) {
const product = props.products.find(product => product.id === params.productId);
setProduct(product);
}
}, [props.products, params.productId]);
console.log(product)
I realized that when useEffect is run for the first time, my "products" array is still empty and it sends it to the component, so it's not good. But I don't know how to fix this.
There are a couple of things wrong here. I'm going to start with something that seems tangential at first but it will lead to a better answer.
In ProductDetail you introduce a new state called product. You then, in a useEffect, find the product from the list & product id in props and set this back into the state item. This would be unnecessary state duplication (even if not copying a prop verbatim into state, a value derived directly from props still counts) and is going to increase the surface areas for bugs in your application (this is probably the most common beginner's error).
You actually don't need that state item, you just need useMemo:
function ProductDetail(props) {
const params = useParams();
const product = useMemo(() => {
if (props.products && params.productId) {
return props.products.find(product => product.id === params.productId);
}
}, [props.products, params.productId]);
console.log(product)
To solve the issue with the product not being found briefly whilst then products load you can either (a) conditionally render the component so this code doesn't even run in the first place when products isn't fetched yet. Or (b) Change the ProductDetail so that it effectively does nothing until the product is found.
Solution A
const [products, setProducts] = useState(null); // We change the default to null to be able to distinguish empty list case from loading case.
// ...
{products !== null && <ProductDetail products={products}></ProductDetail>}
Solution B
function ProductDetail(props) {
const params = useParams();
const product = useMemo(() => {
if (props.products && params.productId) {
return props.products.find(product => product.id === params.productId);
}
}, [props.products, params.productId]);
if (!product) return null // Return early (do nothing) whilst product is not found
// ... your original render contents
Related
Working on a forum and I need my topics to start not erasing upon refresh so that I can start checking whether or not the messages display properly.
I have it so each time I make a topic, the topic is stored as an object inside an array. Each object stores different types of information like the title of the topic, the message, the author and the date. The array is then sorted and mapped and displays all the information on the page.
The addTopic function is used as an onClick for a form that pops up.
I have my localStorage set up using a useEffect like it was suggested however every time I make a topic and refresh the page, the array still erases itself and I'm back to the original state. Please advice.
const [topic, setTopic] = useState([]);
const [title, setTitle] = useState();
const [message, setMessage] = useState();
const addTopic = (e) => {
e.preventDefault();
const updatedTopic = [
...topic,
{
title: title,
message,
author: "Dagger",
date: new Date(),
},
];
setTopic(updatedTopic);
};
useEffect(() => {
localStorage.setItem("topic", JSON.stringify(topic));
}, [topic]);
useEffect(() => {
const topics = JSON.parse(localStorage.getItem("topic"));
if (topics) {
setTopic(topic);
}
}, []);
Effects run in order, so when you refresh the page the code you have here is going to setItem before you getItem.
An alternative to your second useEffect there is to initialise your useState hook directly from the localStorage:
const [topic, setTopic] = useState(
() => {
const topicJson = localStorage.getItem("topic");
return topicJson ? JSON.parse(topicJson) : [];
});
Yes the reason is JSON.parse(localStorage.getItem("topic")) gives you the string "undefined" instead of the object undefined.
Instead try doing
useEffect(() => {
let topics = localStorage.getItem("topic");
if (topics) {
topics = JSON.parse(topics)
setTopic(topics);
}
}, []);
For my project, I want to fetch data from all documents in a subcollection. And there are multiple documents with this subcollection.
To clarify, this is how my firestore is strctured:
I have an events collection which contains multiple documents with doc.id being the event name itself. Each event document has several fields and an attendee subcollection. In the attendee subcollection, each document contains details about the attendee.
I want to map through all documents in the events collection and fetch data about attendees from all of them.
And I want to display this data when the component first renders. So I'm calling the function inside useEffect. Here's what I have tried:
const [attendeeInfo, setAttendeeInfo] = useState({});
const [events, setEvents] = useState([]);
const getEventsData = async () => {
// first of all, we need the name of the user's org
// fetch it from users collection by using the uid from auth
const orgRef = doc(db, "users", auth["user"].uid);
const orgSnap = await getDoc(orgRef);
// now that we have the org name, use that to search events...
// ...created by that org in events collection
const eventsRef = collection(db, "events");
const eventsQuery = query(eventsRef, where("createdBy", "==", orgSnap.data().orgName));
const eventsQuerySnapshot = await getDocs(eventsQuery);
let eventsInfo = [];
eventsQuerySnapshot.forEach((doc) => {
eventsInfo.push(doc.id);
})
setOrg(orgSnap.data().orgName);
setEvents(eventsInfo);
}
const getAttendeesData = (events) => {
console.log(events);
let attendeeInformation = [];
events.forEach(async (event) => {
const attendeesRef = collection(db, "events", event, "attendees");
const attendeesSnap = await getDocs(attendeesRef);
attendeesSnap.forEach((doc) => {
const isItMentor = doc.data().isMentor ? "Yes" : "No";
const isItMentee = doc.data().isMentee ? "Yes" : "No";
const attendeeData = {
name: doc.id,
mentor: isItMentor,
mentee: isItMentee,
};
attendeeInformation.push(attendeeData);
})
})
// console.log(attendeeInformation);
setAttendeeInfo(attendeeInformation);
}
useEffect(() => {
getEventsData();
// console.log(attendeeInfo);
getAttendeesData(events);
}, []);
However, when I console log the events inside my attendeesData function, I get an empty array which means that the events state variable hasn't been updated from previous function.
Can anyone help me solve this?
This is a timing issue. On first render you start fetching the list of events, but you aren't waiting for them to be retrieved before using them. Furthermore, because you only run this code on mount, when events is eventually updated, getAttendeesData won't be invoked with the updated array.
useEffect(() => {
getEventsData(); // <-- queues and starts "fetch event IDs" action
getAttendeesData(events); // <-- starts fetching attendees for `events`, which will still be an empty array
}, []); // <-- [] means run this code once, only when mounted
The solution to this is to split up the useEffect so each part is handled properly.
useEffect(() => {
getEventsData(); // <-- queues and starts "fetch event IDs" action
}, []); // <-- [] means run this code once, only when mounted
useEffect(() => {
getAttendeesData(events); // initially fetches attendees for an empty array, but is then called again when `events` is updated with data
}, [events]); // <-- [events] means run this code, when mounted or when `events` changes
Next, you need to fix up getAttendeesData as it has a similar issue where it will end up calling setAttendeeInfo() at the end of it with another empty array (attendeeInformation) because you aren't waiting for it to be filled with data first. While this array will eventually fill with data correctly, when it does, it won't trigger a rerender to actually show that data.
const [attendeeInfo, setAttendeeInfo] = useState([]); // <-- should be an array not an object?
const [events, setEvents] = useState([]);
const getAttendeesData = async (events) => {
console.log(events);
const fetchAttendeesPromises = events.map(async (event) => {
const attendeesRef = collection(db, "events", event, "attendees");
const attendeesSnap = await getDocs(attendeesRef);
const attendeeInformation = [];
attendeesSnap.forEach((doc) => {
const isItMentor = doc.data().isMentor ? "Yes" : "No";
const isItMentee = doc.data().isMentee ? "Yes" : "No";
const attendeeData = {
name: doc.id,
mentor: isItMentor,
mentee: isItMentee,
};
attendeeInformation.push(attendeeData);
})
return attendeeInformation; // also consider { event, attendeeInformation }
})
// wait for all attendees to be fetched first!
const attendeesForAllEvents = await Promises.all(fetchAttendeesPromises)
.then(attendeeGroups => attendeeGroups.flat()); // and flatten to one array
// console.log(attendeesForAllEvents);
setAttendeeInfo(attendeesForAllEvents);
}
Applying these changes in a basic and incomplete (see below) way, gives:
// place these outside your component, they don't need to be recreated on each render
const getEventsData = async () => { /* ... */ }
const getAttendeesData = async (events) => { /* ... */ }
export const YourComponent = (props) => {
const [attendeeInfo, setAttendeeInfo] = useState(null); // use null to signal "not yet loaded"
const [events, setEvents] = useState(null); // use null to signal "not yet loaded"
const loading = events === null || attendeeInfo === null;
useEffect(() => {
getEventsData();
}, []);
useEffect(() => {
if (events !== null) // only call when data available
getAttendeesData(events);
}, [events]);
// hide component until ready
// consider rendering a spinner/throbber here while loading
if (loading)
return null;
return (
/* render content here */
)
}
Because getEventsData() and getAttendeesData() are Promises, you should make use of useAsyncEffect implmentations like #react-hook/async and use-async-effect so you can handle any intermediate states like loading, improper authentication, unmounting before finishing, and other errors (which are all not covered in the above snippet). This thread contains more details on this topic.
I am using a context like the following:
const placeCurrentOrder = async () => {
alert(`placing order for ${mealQuantity} and ${drinkQuantity}`)
}
<OrderContext.Provider
value={{
placeCurrentOrder,
setMealQuantity,
setDrinkQuantity,
}}
>
and I'm calling this context deep down with something like this (when the user clicks a button):
const x = () => {
orderContext.setMealQuantity(newMealQuantity)
orderContext.setDrinkQuantity(newDrinkQuantity)
await orderContext.placeCurrentOrder()
}
Sort of like I expect, the state doesn't update in time, and I always get the previous value of the state. I don't want to have a useEffect, because I want control over exactly when I call it (for example, if mealQuantity and drinkQuantity both get new values here, I don't want it being called twice. The real function is far more complex.)
What is the best way to resolve this? I run into issues like this all the time but I haven't really gotten a satisfactory answer yet.
You can set them in a ref. Then use the current value when you want to use it. The easiest way is probably to just create a custom hook something like:
const useStateWithRef = (initialValue) => {
const ref = useRef(initialValue)
const [state, setState] = useState(initialValue)
const updateState = (newState) => {
ref.current = typeof newState === 'function' ? newState(state) : newState
setState(ref.current)
}
return [state, updateState, ref]
}
then in your context provider component you can use it like:
const [mealQuantity, setMealQuantity, mealQuantityRef] = useStateWithRef(0)
const [drinkQuantity, setDrinkQuantity, drinkQuantityRef] = useStateWithRef(0)
const placeOrder = () => {
console.log(mealQuantityRef.current, drinkQuantityRef.current)
}
You can also just add a ref specifically for the order and then just update it with a useEffect hook when a value changes.
const [drinkQuantity, setDrinkQuantity] = useState(0)
const [mealQuantity, setMealQuantity] = useState(0)
const orderRef = useRef({
drinkQuantity,
mealQuantity
})
useEffect(() => {
orderRef.current = {
drinkQuantity,
mealQuantity,
}
}, [drinkQuantity, mealQuantity])
const placeOrder = () => {
console.log(orderRef.current)
}
I am having trouble with a content loading issue with my react/next.js app using hooks. I believe the problem is with my useEffect and the dependency. I would appreciate any unblockin assistance. The situation:
As you will see from the code below:
In the useEffect:
'eventType' object loads from one API endpoint and is added to the corresponding state
if eventType is a movie (has externalData key) then we load the movie object from another API endpoint based on the filmId (in externalData)
so if eventType.externalData is not null then we set the movie state value to the content of the response data
if movie state is true then we render the 'movie' code block
else we render the 'non-movie' code block
The issue:
For some reason the movie information is not coming back fast enough from the API call, so when given the choice of what to render, the app renders the non-movie block
In order to counteract this, I added [movie] to the dependencies for the useEffect. This works, but leads to infinite reload hell.
My question:
How can I make sure that the reload is triggered only once when the movie data loads and not infinitely?
function EventDetailPage(props) {
const router = useRouter();
const slug = router.query.slug;
const cinemaId = router.query.cinemaId;
const date = slug[0];
const eventTypeId = slug[1];
const {locale} = useRouter();
const [eventType, setEventType] = useState(null);
const [movie, setMovie] = useState(null);
const [personData, setPersonData] = useState(null);
const {user} = useUserStore();
const setupEventDetailPageWithData = async () => {
try {
const eventType = await getEventTypeByCinemaIdAndDate(cinemaId, date, eventTypeId);
setEventType(eventType);
console.log('eventType state:', eventType);
if (!user){
const personDataResponse = await fetchPersonData();
setPersonData(personDataResponse);
}
if (eventType.events[0].userId === personData.userId){
console.log("Event initiator id:", eventType.events[0].userId);
setIsInitiator(true);
}
if (eventType.externalData) {
const movie = await getMovieById(eventType.externalData.filmId);
setMovie(movie);
} else {
}
} catch (error) {
// handle error
console.error(error);
}
};
useEffect(() => {
setupEventDetailPageWithData();
}, [movie]);
if (!eventType) {
return (
<>
<LoadingAnimation/>
</>
);
}
if (movie) {
return ( <div> movie code </div> )
} else {
return ( <div> non-movie code </div> )
}
you can do somethings like this
useEffect(() => {
//call api and set your state
},[])
or better practise like this
useEffect(() => {
//call your api and set your state
},[setMovie])
I have a component like this:
const [products, setProducts] = useState([]);
const [store, setStore] = useState([]);
const fetchProducts () => {
... fetch('product_url').then((products) => {
setProducts(products);
}).catch();
}
const fetchStore () => {
... fetch('store_url').then((store) => {
setStore(store);
}).catch();
}
useEffect(() => {
fetchProducts();
fetchStore();
}, []);
const handleLogicAfterProductsAndStoresLoadingSuccess = () => {
// products, store
//Some logic here
}
My question is where can I put the handleLogicAfterProductsAndStoresLoadingSuccess()?
Generally, just test for the values inside your render function, and put any processing after the request.
// Testing for length because you are initializing them to empty arrays. You should probably use null instead.
if (!(products.length && store.length)) {
return <LoadingComponent />
}
// Any other processing you need goes here
return <>
{/* Your result goes here */}
</>;
you can create another useEffect to track both changes of products & store:-
// handle what happen after products & store fetched
const handleLogicAfterProductsAndStoresLoadingSuccess = (products, store) => {
// if both successfully fetched data (not empty array)
if(products.length > 0 && store.length > 0) {
// do something
} else {
alert('Data not fetched correctly!')
}
}
// handle changes happen to products & store (note: this useEffect will be fired every time there are changes made to the products & store)
useEffect(() => {
handleLogicAfterProductsAndStoresLoadingSuccess(products, store)
}, [products, store])