please help with useCallback, how to call it so that useEffect does not send endless requests to the server? Now it sends endless requests, I know that useCallback is needed for this, but I don’t understand how to use it
// Get data
useEffect(() => {
api.getTestLeagues()
.then(data => setLeagues(data));
api.getTestCountries()
.then(data => setCountries(data));
if(sidebars === null && leagues !== null && countries !== null) {
onTest()
}
}, [api, leagues, sidebars, countries, onTest]);
// The 'onTest' function makes the dependencies of useEffect Hook (at line 29) change on every render. Move it inside the useEffect callback. Alternatively, wrap the 'onTest' definition into its own useCallback() Hook
// test
const onTest = () => {
const updateObj = {
leagues: {
title: "Мои лиги",
items: leagues
},
countries: {
title: "Страны",
items: countries
}
};
setSidebars(updateObj);
};
Req:
I'm assuming you want to load data once, normally done with componentDidMount.
2 useEffect fired only once, running in parralel
useEffect(() => {
api.getTestLeagues()
.then(data => setSidebarItemsLeagues(data)); // for items 1 (not null)
}, []);
useEffect(() => {
api.getTestCountries()
.then(data => setSidebarItemsCountries(data)); // for items 2 (not null)
}, [])
useEffect running only on data changes
useEffect(() => {
if(sidebars === null && leagues !== null && countries !== null) {
const updateObj = {
leagues: {
title: "Мои лиги",
items: leagues
},
countries: {
title: "Страны",
items: countries
}
};
setSidebars(updateObj);
}
}, [leagues, countries]);
Update
api defined earlier ...
const api = new Services();
of course changes on every render but should not be taken as dependency as it's always uses the same urls - they are not parametrized. No meaningful (affecting data) changes - don't care.
If api is not passed to rendered components (ref cange could force rerenderings) then there is no need to take care about it.
You can get rid of this definition by
useEffect(() => {
new Service().getTestLeagues()
.then(data => setSidebarItemsLeagues(data)); // for items 1 (not null)
}, []);
useEffect(() => {
new Service().getTestCountries()
.then(data => setSidebarItemsCountries(data)); // for items 2 (not null)
}, [])
Even if it looks as unnecessary duplicating instances it can be more optimal when component is frequently rerendered - like in open-close view changes.
You're in an infinite loop because your effect depends on values you change in it: each time you fetch some data from the api you change leagues and countries states which leads to running the effect again.
Split the effects so that the requests effect does not depend on the values it changes:
useEffect(() => {
api.getTestLeagues().then(data => setLeagues(data));
api.getTestCountries().then(data => setCountries(data));
}, [api]);
useEffect(() => {
if(sidebars === null && leagues !== null && countries !== null) {
onTest();
}
}, [leagues, sidebars, countries, onTest]);
Also:
Define api outside of your component (above) so the value stays stable.
You can wrap onTest in a useCallback before your effects, like #Ramesh suggested.
Try wrapping onTest with useCallback like this:
const onTest = useCallback(() => {
const updateObj = {
leagues: {
title: "Мои лиги",
items: leagues
},
countries: {
title: "Страны",
items: countries
}
};
setSidebars(updateObj);
},[]);
The second argument to useCallback is an array of dependencies similar to useEffect.
BTW this will memorize the function so that a new function doesn't get created on every render.
update
It is better to split the useEffect so that each useEffect just focuses on executing something when their dependencies are changed.
useEffect(() => {
api.getTestLeagues()
.then(data => setLeagues(data));
api.getTestCountries()
.then(data => setCountries(data));
}, [api]);
useEffect(() => {
if(sidebars === null && leagues !== null && countries !== null) {
onTest()
}
},[onTest, sidebars, leagues, countries])
When you send a request that changes the state and also use the same state as a dependency that'll again send a request(because the value of the state changes) it'll cause an infinite loop.
update 2
The issue might be with the api class instance because on every new render a new instance is created.
You said you were doing something like this const api = new Services(); so wrap that with useMemo like this:
const api = useMemo(() => {
return new Services();
})
Related
I'm trying to do something that in my mind is very simple.
I have an array of documents (firebase firestore) and I have a method to fetch From Document with timeStamp A to docuemnt with timeStamp B
In the fetch function that tries to see if the ref id has already been fetched, but the messages inside the fetchUpTo function never updates. While the one I log in the effect hook, updates as expected.
The top Log is the Inside fetchUpTo and the bottom one is the effect one.
The logs are from trying to refetch one of the documents present in the bottom log.
const fetchUpTo = (ref: any) => {
if (isFetching || isAtEnd) {
return
};
if (!messagesSnapShot) {
return;
}
if (messagesSnapShot.size < queryLimit) {
return;
}
let index = messages.findIndex(d => d.id === ref.id)
if (index !== -1) {
if (messageRefs.current[index] !== null) {
scrollToMessage(messageRefs.current[index])
return;
}
}
setIsFetching(true)
const lastVisible = messages[0]
const cQuery = query(collection(fireStore, "scab-chat", chatId, "messages"), orderBy('time', 'desc'), startAfter(lastVisible.data.time), endAt(ref.data.time));
getDocs(cQuery).then(newDocs => {
if (newDocs.size < queryLimit) {
setIsAtEnd(true)
}
const newD = newDocs.docs.map(doc => ({
data: doc.data(),
id: doc.id,
ref: doc
}));
setMessages(s => {
const f = newD.filter(doc => s.findIndex(d => d.id === doc.id) === -1)
return [...s, ...f]
})
})
}
After doing this, I "wait" for the state to update with an Effect hook
useEffect(() => {
if (messages) {
setIsFetching(false)
}
}, [messages])
The problem is I have this small part of the code
let index = messages.findIndex(d => d.id === ref.id)
if (index !== -1) {
if (messageRefs.current[index] !== null) {
scrollToMessage(messageRefs.current[index])
return;
}
}
React state will only rerender you app when the function finishes, so you will only check for the updated messages when you call fetchUpTo again. If you need the updated value on the same function call, try using flushSync.
There is a nice video with Dan Abramov trying to achieve the same as you, I will leave it here for reference: https://youtu.be/uqII0AOW1NM?t=2102
Okay so I fixed it kinda, I had to render a diferent component while the isFetchingState was true so
if(isFetching){return <Loader/>}
and then it worked. I still don't really understand why It didn't work in the first place.
I am having a problem assigning data to useState by fetching data using reference type value from firebase.
const [preOil, setPreOil] = useState([]);
const [oilChange, setOilChange] = useState([]);
useEffect(() => {
getDocs(query(collection(db, "oil"), orderBy("timestamp"))).then(
(snapshot) => {
setPreOil(
snapshot.docs.map((doc) => ({
id: doc.id,
...doc.data(),
}))
);
}
);
}, []);
useEffect(() => {
let current = preOil.length > 0 ? [...preOil] : [];
current.map((_oil, i) => {
getDoc(_oil.oilReference).then((oilRef) => {
current[i].oilReference = oilRef.data();
});
});
setOilChange(current);
}, [preOil]);
In the first useEffect, the data is fetched successfully in this form,
preOil = {
id:"RxbAOAIs8d3kGOHNskJ4",
oilReference: Ta {converter: null, _key: ut, type: 'document', firestore: Na},
customerName: "USAMA",
}
In the second useEffect based on [preOil], I need to reassign the oilReference with the data fetched from firestorm through its reference(oilReference), The data is successfully fetched from the database and assigned to current but The main problem is when I set to state setOilChange(current) it updates my oilChange state when I inspect in inspect tools in chrome but in JSX the changes don't reflect
I am updating the state in useEffect
I am having desired data assigned in a local variable and assign that variable to the state
Then What am I missing?
In your second useEffect(), more specifically, in
current.map((_oil, i) => {
getDoc(_oil.oilReference).then((oilRef) => {
current[i].oilReference = oilRef.data();
});
});
setOilChange(current);
You are mutating the content of the current variable. This mutation, because it is async, will happen after the setOilChange call. Such mutation will thus not trigger a re-render.
What you need is to instead first wait for all the docs to be loaded and only after that set the state. Example:
const _docs = current.map((_oil, i) => {
return getDoc(_oil.oilReference).then((oilRef) => { // changed here
return { // changed here
...current[i], // changed here
oilReference: oilRef.data() // changed here
} // changed here
}); // changed here
});
Promise.all(_docs).then(() => {
setOilChange(_docs);
});
Also notice I didn't mutate current, rather I returned a new object. This is not mandatory but just a best practice overall.
I have my useeffect defined as follows- I am trying to get some values
from backend based on the dependencies changes. However it's creating
an infinite loop.
useEffect(() => {
if (
model &&
model.mortgage &&
model.mortgage.borrowingsStartDate &&
model.mortgage.borrowingsRepaymentFrequency &&
model.mortgage.borrowingsAnnualInterestRate &&
model.mortgage.borrowingsPrinciple &&
model.mortgage.borrowingsTermInYears
) {
loanRepaymentAndInterest();
} else {
setRepaymentAmount(0);
}
}, [
model.mortgage.borrowingsStartDate,
model.mortgage.borrowingsRepaymentFrequency,
model.mortgage.borrowingsAnnualInterestRate,
model.mortgage.borrowingsPrinciple,
model.mortgage.borrowingsTermInYears
]);
I have declared my function as-
const [repaymentAmount, setRepaymentAmount] = useState(0);
const loanRepaymentAndInterest = async () => {
const payload = {
amount: {
currency: model.mortgage.borrowingsCurrency,
value: model.mortgage.borrowingsPrinciple
},
freq: model.mortgage.borrowingsRepaymentFrequency,
interestRate: model.mortgage.borrowingsAnnualInterestRate,
startDate: model.mortgage.borrowingsStartDate,
term: model.mortgage.borrowingsTermInYears
};
await api
.post(`/loan/repayment`, {
...payload
})
.then(data => {
setRepaymentAmount(data.data.repaymentAmount.value);
})
.catch(error => console.log(error));
};
The forms has been managed by react final forms. The function will run inside useeffect whenever the dependencies change. The dependencies are just the form field values which are stored inside model object. I am trying to get some values from backend based on those changed dependencies values. Eventhough the dependencies do not change useeffect is running for infinite time. The api is called infinitely.
So after the content is loaded in the website I'm trying to call a function to fill [fields] (State) with the events but something Isn't working.
Here is what I mean:
const [events, setEvents] = useState([]);
useEffect(() => {
if (orders.orders !== null && orders.isLoading !== true)
orders.orders.map((order) =>
setEvents([
...events,
{
title: `Meeting with ${order.customer.firstName}`,
date: `${order.appointment}`,
},
])
);
setLoading(false);
}, [orders]);
So when I console log the events, I get [] (empty), but if i console.log the object it works.
I'm not sure what you are trying to achieve with the code but looks like your useEffect has a missing dependency of events. Since you are also updating events with setEvents, you should update with
setEvents(prev=>[
...prev,
{
title: `Meeting with ${order.customer.firstName}`,
date: `${order.appointment}`,
}
])
to avoid an infinite loop.
You are directly updating state so it is shown in console but in react rerender should be done to update state so pass parameter for setEvents and spread parameter but not directly events because you cannot directly update state.
const [events, setEvents] = useState([]);
useEffect(() => {
if (orders.orders !== null && orders.isLoading !== true)
orders.orders.map((order) =>
setEvents(prev => [
...prev,
{
title: `Meeting with ${order.customer.firstName}`,
date: `${order.appointment}`,
},
])
);
setLoading(false);
}, [orders]);
useEffect(() => {
if (!stop) {
// get current user profile
db.collection('events').get(eventId).then((doc) => {
doc.forEach((doc) => {
if (doc.exists) {
let temp = doc.data()
let tempDivisions = []
temp["id"] = doc.ref.id
doc.ref.collection('divisions').get().then((docs) => {
docs.forEach(doc => {
let temp = doc.data()
temp["ref"] = doc.ref.path
tempDivisions.push(temp)
});
})
temp['divisions'] = tempDivisions
setEvent(temp)
setStop(true)
// setLoading(false);
} else {
// doc.data() will be undefined in this case
console.log("No such document!");
<Redirect to="/page-not-found" />
}
})
})
}
}, [stop, eventId]);
I am curious if this is the properly way to extract nested data from Cloud Firestore.
Data model:
Collection(Events) -> Doc(A) -> Collection(Divisions) -> Docs(B, C, D, ...)
Pretty much I'm looking to get metadata from Doc(A), then get all the sub-collections which contain Docs(B, C, D, ...)
Current Problem: I am able to get meta data for Doc(A) and its subcollections(Divisions), but the front-end on renders metadata of Doc(A). Front-End doesn't RE-RENDER the sub-collections even though. However, react devtools show that subcollections(Divisions) are available in the state.
EDIT 2:
const [entries, setEntries] = useState([])
useEffect(() => {
let active = true
let temp = []
if (active) {
divisions.forEach((division) => {
let teams = []
let tempDivision = division
db.collection(`${division.ref}/teams`).get().then((docs) => {
docs.forEach((doc, index) => {
teams.push(doc.data())
})
tempDivision['teams'] = teams
})
setEntries(oldArray => [...oldArray, temp])
})
}
return () => {
active = false;
};
}, [divisions]);
is there any reason why this is not detecting new array and trigger a new state and render? From what I can see here, it should be updating and re-render.
Your inner query doc.ref.collection('divisions').get() doesn't do anything to force the current component to re-render. Simply pushing elements into an array isn't going to tell the component that it needs to render what's in that array.
You're going to have to use a state hook to tell the component to render again with new data, similar to what you're already doing with setEvent() and setStop().