I have a useEffect function that must wait for four values to have their states changed via an API call in a separate useEffect. In essence the tasks must happen synchronously. The values must be pulled from the API and those stateful variables must be set and current before the second useEffect can be called. I am able to get the values to set appropriately and my component to render properly without doing these tasks synchronously, I have a ref which changes from true to false after first render (initRender), however I find the code to be hacky and inefficient due to the fact that the second useEffect still runs four times. Is there a better way to handle this?
//Hook for gathering group data on initial page load
useEffect(() => {
console.log("UseEffect 1 runs once on first render");
(async () => {
const response = await axios.get(`${server}${gPath}/data`);
const parsed = JSON.parse(response.data);
setGroup(parsed.group);
setSites(parsed.sites);
setUsers(parsed.users);
setSiteIDs(parsed.sitesID);
setUserIDs(parsed.usersID);
})();
return function cleanup() {};
}, [gPath]);
//Hook for setting sitesIN and usersIN values after all previous values are set
useEffect(() => {
console.log("This runs 4 times");
if (
!initRender &&
sites?.length &&
users?.length &&
userIDs !== undefined &&
siteIDs !== undefined
) {
console.log("This runs 1 time");
setSitesIN(getSitesInitialState());
setUsersIN(getUsersInitialState());
setLoading(false);
}
}, [sites, siteIDs, users, userIDs]);
EDIT: The code within the second useEffect's if statement now only runs once BUT the effect still runs 4 times, which still means 4 renders. I've updated the code above to reflect the changes I've made.
LAST EDIT: To anyone that sees this in the future and is having a hard time wrapping your head around updates to stateful variables and when those updates occur, there are multiple approaches to dealing with this, if you know the initial state of your variables like I do, you can set your dependency array in a second useEffect and get away with an if statement to check a change, or multiple changes. Alternatively, if you don't know the initial state, but you do know that the state of the dependencies needs to have changed before you can work with the data, you can create refs and track the state that way. Just follow the examples in the posts linked in the comments.
I LIED: This is the last edit! Someone asked, why can't you combine your different stateful variables (sites and sitesIN for instance) into a single stateful variable so that way they get updated at the same time? So I did, because in my use case that was acceptable. So now I don't need the 2nd useEffect. Efficient code is now efficient!
Your sites !== [] ... does not work as you intend. You need to do
sites?.length && users?.length
to check that the arrays are not empty. This will help to prevent the multiple runs.
Related
I'm trying to arrange the data gotten from firebase but after I arrange it the app becomes slow and if I click on a button it gives an error saying "Excessive number of pending callbacks".
useEffect(() => {
if (chats.length && users.length) {
const list = [];
chats.forEach((chat) => {
if (chat.members.includes(userId)) {
chat.members.forEach((y) => {
if (y !== userId) {
console.log("receiver: " + y);
users.forEach((z) => {
if (z.id === y) {
console.log(z);
list.push({
chat: chat,
acc: z,
user: user
});
}
});
console.log(list);
}
});
}
});
setUserChats(list);
}
}, [chats, users]);
users and chats are both states that I got from firebase on snapshot also in useEffect
One guess: Your dependencies don't work in the way you expect them to. chats and users aren't primitives, so depending on how they get created and passed, it's possible useEffect is run on every render, no matter whether chat and users have changed in structure or not. So this is what might happen:
useEffect() will be called on every rerender due to your invalid dependencies.
useEffect() calls setUserChats() on every turn. In theory, setUserChats() will check whether the new state actually differs, but in the same manner as useEffect it "fails" in the comparison and takes every list as a new state.
setState() will trigger a new render. Rinse, repeat with 1)
What you need to understand it that useEffect checks whether dependencies have changed (and setUserChats() does so as well to decide whether new state actually differs from the old one). This check relies on the identity/equal reference, i.e. on oldValue === newValue. For non-primitive values that means: it doesn't matter if oldValue and newValue look alike or are perfect clones even - if they don't share the same address in the memory, they are taken as non-equal.
Check out this thread or this library for solutions. In your case, a simple (but not really nice) solution would be to change your dependencies to [JSON.stringify(chats), JSON.stringify(users)], but there are more elaborate, performant and reliable solutions out there.
(Additionally, you forget to add userId to the dependencies. So something like [userId, JSON.stringify(chats), JSON.stringify(users)] might be more appropriate.)
Another thought though: I don't see why all that logic requires to be put into a useEffect() anyway. Just calculate the list in the component itself and call setUserChats(list). Or does it take too long?
The kinda the same problem as described here
How to compare oldValues and newValues on React Hooks useEffect?
But in my case, usePrevious hook does not help.
Imagine the form with several inputs, selects, and so on. You may want to look at https://app.uniswap.org/#/swap to make a similar visualization. There are several actions and data updates that will be happened on almost any change, which will lead to several re-renders, at least 4. For example.
I have 2 inputs, each represents a token. Base(first one) and Quote(second one).
This is a state for Base
const [base, setBase] = useState({
balance: undefined,
price: undefined,
value: initState?.base?.value,
token: initState?.base?.token,
tokenId: initState?.base?.tokenId,
});
and for Quote
const [quote, setQuote] = useState({
balance: undefined,
price: undefined,
value: initState?.quote?.value,
token: initState?.quote?.token,
tokenId: initState?.quote?.tokenId,
});
They gonna form a pair, like BTC/USD for example.
By changing token (instead of BTC I will choose ETH) in the select menu I will trigger several actions: fetching wallet balance, fetching price, and there are gonna be a few more rerenders with input view update and modal window close. So at least 4 of them are happening right now. I want to be able to compare base.token and basePrv with
const basePrv = usePrevious(base?.token); but on the second re-render base.token and basePrv gonna have the same token property already and it is an issue.
I also have the swap functionality between the inputs where I should change base with quote and quote with base like that
setBase(prevState => ({
...prevState,
base: quote
}));
setQuote(prevState => ({
...prevState,
quote: base
}));
In that case, there are no additional requests that should be triggered.
Right now I have useEffect with token dependency on it. But it will be fired each time when the token gonna be changed which will lead to additional asynchronous calls and 'tail' of requests if you gonna click fast. That's why I need to compare the token property that was before the change to understand should I make additional calls and requests because of the formation of new pair (BTC/USD becomes ETH/USD) or I should ignore that because it was just a "swap" (BTC/USD becomes USD/BTC) and there is no need to make additional calls and fetches. I just had to, well, swap them, not more.
So in my story, usePrevious hook will return the previous token property only once, and at the second and third time, it would be overwritten by multiple re-renders(other properties would be fetched) to the new one. So at the time when useEffect gonna be triggered, I would have no chance to compare the previous token property and the current one, because they will show the same.
I have several thoughts on how to solve it, but I am not sure is it right or wrong, because it seemed to me that the decisions look more imperative than declarative.
I can leave everything as it is (requests would be triggered always on any change no matter what it was. Was it a swap or user changed a pair). I can disable the swap button until all of the requests would be finished. It would solve the problem with requests 'tail'. But it is a kinda hotfix, that gonna be work, but I do not like it, because it would lead to additional unnecessary requests and it would be slow and bad for UX.
I can use a state to keep the previous pair on it right before the update by setBase or setQuote happens. It will allow me to use useEffect and compare previous pair to the current one to understand did the pair was changed, or just swapped and take the decision should I make fetches and calls or not.
I can get rid of useEffect with base.token and quote.token dependencies and handle everything inside of onClick handler. Because of that, the swap functionality would not trigger useEffect, and calls and fetches would be fired only if the user gonna click and choose something different. But as I said this option seemed a little bit odd to me.
I tried to use closure here, to "remember" the previous state of tokens, but it is kinda similar to use the current component state. Also, you have to initialize closure outside of the functional component body, and I do not see a possibility to transfer the init state into it that way, so the code becomes more spaghettified.
So any other ideas guys? I definitely missing something. Maybe that much of re-renders is an antipattern but I am not sure how to avoid that.
There could be multiple solutions to your problem. I would suggest to pick one which is easier to understand.
1. Modify the usePrevious hook
You can modify the usePrevious hook to survive multiple renders.
Tip: use JSON.stringify to compare if you think the value will be a complex object and might change the reference even for same real value.
function usePrevious(value) {
const prevRef = useRef();
const curRef = useRef();
if (value !== curRef.current){
// or, use
// if ( JSON.stringify(value) !== JSON.stringify(curRef.current)){
prevRef.current = curRef.current;
curRef.current = value;
}
return prevRef.current;
}
2. Sort useEffect dependency array
Since you're using tokens(strings) as dependency array of useEffect, and you don't mind their order (swap shouldn't change anything), sort the dependency array like
useEffect(
() => {
// do some effect
},
[base.token, quote.token].sort()
)
3. Store the currently fetched tokens.
While storing the API response data, also store the tokens(part of request) associated with that data. Now, you'll have 2 sets of tokens.
currently selected tokens
currently fetched tokens
You can chose to fetch only when the currently fetched tokens don't fulfil your needs. You can also extend this and store previous API request/responses and pick the result from them if possible.
Verdict
Out of all these, 3rd seems a nice & more standardised approach to me, but an overkill for your need (unless you want to cache previous results).
I would have gone with 2nd because of simplicity and minimalism. However, It still depends on what you find easier at the end.
I have a paginated list of data that I am updating based on a filter and an offset (page)
When the offset is updated (next/prev page) I hit the API and get new data. When the filter is updated I reset the offset to 0.
The problem is, when the filter is updated and the offset is updated it causes the useEffect to be fired twice. Which in turn calls the api twice.
const [filter, setFilter] = useState('All');
const [offset, setOffset] = useState(0);
onFilterChange = (value) => {
setFilter(value);
offset !== 0 && setOffset(0);
}
getDataFromAPI = useCallback(() => {
const endpoint = `https://example.com/data?filter=${filter}&offset=${offset}`;
CallApi(endpoint);
}, [offset, filter])
useEffect(getDataFromAPI, [getDataFromAPI]);
I think the fix would be to get rid of useEffect in such case. Sometimes it is used needlessly. Just call CallApi inside the onFilterChange and onOffsetChange handlers with the new values that were set, and that's it.
Here are some relevant quotes from Dan Abramov:
In hindsight, my personal conviction has become that if some effect
can’t safely over-fire (e.g. fire twice instead of once), there is a
problem. Usually things that can’t over-fire are related to user
actions ("buy", "send", "finish"). Actions start in event handlers.
Use them!
To sum up, if something happens because a user did something,
useEffect might not be the best tool.
On the other hand, if an effect merely synchronizes something (Google
Map coordinates on a widget) to the current state, useEffect is a good
tool. And it can safely over-fire.
PS But just to note I thought in your case react would batch the two different set state calls inside the filter change handler (hence call render once), but it seems it doesn't? In any case you may still prefer the recommendation in the beginning of my answer to remove useEffect.
So basically i started learning ReactJs and am trying to build a chatting app
now here there is section of adding Channels so here is my code
const [Channels, setChannelState] = React.useState([])
let loadedChannels = []
React.useEffect(() => {
setCurrentUser(props.currentUser)
ref.ref('channels').on('child_added', snap =>{
loadedChannels.push(snap.val())
console.log(loadedChannels)
setChannelState(loadedChannels)
})
},[])
so here when i tried to log the loadedChannels Array am getting all the result but when i try to set this array to my state then am getting only one result
how can i resolve this?
You have an empty array [] as a second parameter which runs the function passed to useEffect only once.
You can tell React to skip applying an effect if certain values haven’t changed between re-renders. To do so, pass an array as an optional second argument to useEffect which you have done.
If we pass [] as second parameter this will apply effect only once.
We can pass value inside array, only on change which it will trigger useEffect call.
As per the documentation
If you want to run an effect and clean it up only once (on mount and
unmount), you can pass an empty array ([]) as a second argument. This
tells React that your effect doesn’t depend on any values from props
or state, so it never needs to re-run. This isn’t handled as a special
case — it follows directly from how the dependencies array always
works.
try using something like this:
const [Channels, setChannelState] = React.useState([])
React.useEffect(() => {
let loadedChannels = []
setCurrentUser(props.currentUser)
ref.ref('channels').on('child_added', snap =>{
loadedChannels.push(snap.val())
})
setChannelState([...loadedChannels])
},[])
Hope this helps, Happy coding!!!
The second argument for useEffect is called a dependency array. And when it's empty, it acts like componentDidMount() i.e it runs only once, after the first render.
You also need to be careful about setting state in useEffect, since this might create an infinite loop of re-renders if not done correctly - setCurrentUser. That is not enough code to help any further than that.
This is the perfect time to get into React. Have fun!
Second parameter is passed for conditional rendering. To understand that there are few scenarios which tell us the possible ways to use that.
There are multiple scenarios which will make it easier for you to understand the importance and functionality of the second parameter with the help of the example from demo.
Scenario 1
Second parameter as empty array,
useEffect(() => {
console.log(`You clicked ${count} times`);
},
[ ]) //called only once similar to componentDidMount
If we pass an empty array, it means that we are not interested in monitoring any of the values so that the useEffect won’t be called except on mounting and before un-mounting. This mimic’s the working of componentDidMount and componentWillUnmount, it will only run once.
Scenario 2
Second parameter with value(s),
useEffect(() => {
console.log(`You clicked ${count} times`);
}, [count]); //function inside useEffect will be called when "count" will be updated
The value passed as the second parameter (array) here in our example: count will be responsible for the execution of the function inside the useEffect Hook. If the value inside the array will be updated, then and only then the function will be executed.
What if we have multiple useEffect hooks and we need to update just few of them? Conditional rendering comes in light. Conditional rendering is achieved by passing the second parameter instead of just empty array. The values in the array will be monitored and the useEffect function will only be called when the monitored value(s) is/are updated.
Refer these links for more info
https://stackblitz.com/edit/react-hooks-intro
https://www.c-sharpcorner.com/article/react-hooks-introduction/
locationHistory is always an empty array in the following code:
export function LocationHistoryProvider({ history, children }) {
const [locationHistory, setLocationHistory] = useState([])
useEffect(() => history.listen((location, action) => {
console.log('old state:', locationHistory)
const newLocationHistory = locationHistory ? [...locationHistory, location.pathname] : [location.pathname]
setLocationHistory(newLocationHistory)
}), [history])
return <LocationHistoryContext.Provider value={locationHistory}>{children}</LocationHistoryContext.Provider>
}
console.log always logs []. I have tried doing exactly the same thing in a regular react class and it works fine, which leads me to think I am using hooks wrong.
Any advice would be much appreciated.
UPDATE: Removing the second argument to useEffect ([history]) fixes it. But why? The intention is that this effect will not need to be rerun on every rerender. Becuase it shouldn't need to be. I thought that was the way effects worked.
Adding an empty array also breaks it. It seems [locationHistory] must be added as the 2nd argument to useEffect which stops it from breaking (or no 2nd argument at all). But I am confused why this stops it from breaking? history.listen should run any time the location changes. Why does useEffect need to run again every time locationHistory changes, in order to avoid the aforementioned problem?
P.S. Play around with it here: https://codesandbox.io/s/react-router-ur4d3?fontsize=14 (thanks to lissitz for doing most the leg work there)
You're setting up a listener for the history object, right?
Assuming your history object will remain the same (the very same object reference) across multiple render, this is want you should do:
Set up the listener, after 1st render (i.e: after mounting)
Remove the listener, after unmount
For this you could do it like this:
useEffect(()=>{
history.listen(()=>{//DO WHATEVER});
return () => history.unsubscribe(); // PSEUDO CODE. YOU CAN RETURN A FUNCTION TO CANCEL YOUR LISTENER
},[]); // THIS EMPTY ARRAY MAKES SURE YOUR EFFECT WILL ONLY RUN AFTER 1ST RENDER
But if your history object will change on every render, you'll need to:
cancel the last listener (from the previous render) and
set up a new listener every time your history object changes.
useEffect(()=>{
history.listen(()=>{//DO SOMETHING});
return () => history.unsubscribe(); // PSEUDO CODE. IN THIS CASE, YOU SHOULD RETURN A FUNCTION TO CANCEL YOUR LISTENER
},[history]); // THIS ARRAY MAKES SURE YOUR EFFECT WILL RUN AFTER EVERY RENDER WITH A DIFFERENT `history` OBJECT
NOTE: setState functions are guaranteed to be the same instance across every render. So they don't need to be in the dependency array.
But if you want to access the current state inside of your useEffect. You shouldn't use it directly like you did with the locationHistory (you can, but if you do, you'll need to add it to the dependency array and your effect will run every time it changes). To avoid accessing it directly and adding it to the dependency array, you can do it like this, by using the functional form of the setState method.
setLocationHistory((prevState) => {
if (prevState.length > 0) {
// DO WHATEVER
}
return SOMETHING; // I.E.: SOMETHING WILL BE YOUR NEW STATE
});