react useState enters infinite loop even thou variable is the same - reactjs

I have the following code:
const [ ddFilterData, setddFilterData ] = useState('');
useEffect(() => {
getDropdownData();
}, [ddFilterData]);
const getDropdownData = async () => {
if(optionDetails) {
let filteredData = Promise.all(
optionDetails.map(async (item, i) => {
const fltData = await filterData(item, props.items);
return fltData
})
)
filteredData.then(returnedData => {
setddFilterData(returnedData);
})
}
}
What I need is for useEffect to execute eah time ddFilerData changes with NEW or DIFFERENT data.
From my understanding it should only update or run when teh ddFilterData is different no?
Currently it runs on each change. The code above enters into an infinite loop even thou filteredData isn't different. Any ideas what I'm doing wrong?

Your returnedData is an array. So when you do setddFilterData(returnedData) you're setting a new value for ddFilterData. Because React uses Object.is for comparison, even if the array elements are the same as previously, it is still a different object and will trigger useEffect again, causing the infinite loop.

your getDropdownData method is updating ddFilterData which causes re-render. And on re-render you getDropdownData is called which updated ddFilterData due to this cyclic behavior your are getting infinte loop.
Modify your code like this:
const [ ddFilterData, setddFilterData ] = useState('');
useEffect(() => {
getDropdownData();
}, []);
useEffect(() => {
// put your code here if you want to do something on change of ddFilterData
}, [getDropdownData]);
const getDropdownData = async () => {
if(optionDetails) {
let filteredData = Promise.all(
optionDetails.map(async (item, i) => {
const fltData = await filterData(item, props.items);
return fltData
})
)
filteredData.then(returnedData => {
setddFilterData(returnedData);
})
}
}

Related

State not updating inside async function in useEffect

Expected:
Following useEffect fetches a list of pool addresses when first rendered and assigns it to getPoolsList, which should then be used to set poolsList state.
const [poolsList, setPoolsList] = useState([]);
useEffect(() => {
async function fetchPools() {
const getPoolsList = await discountmain.methods.allPool().call();
console.log(getPoolsList); //returns ['0x...']
setPoolsList(getPoolsList);
}
fetchPools();
}, []);
The following code I added to check the value of poolsList whenever its value changes.
useEffect(() => {
console.log("useeffect", poolsList); // returns []
}, [poolsList]);
However, poolsList is showing [].
The file is https://codeshare.io/j0wBLD.
What am I doing wrong? I'm a newbie in React.
Your code looks fine, but React can be finicky with arrays in the dependency array
First thing is to actually make sure that the data changes and the useEffect hook is not being called
Here are few things you can try:
Probably the best solution: useReducer
Use the array length and watch the length
const [poolsList, setPoolsList] = useState([]);
const [poolsListLength, setPoolListLength] = useState(0);
useEffect(() => {
async function fetchPools() {
const getPoolsList = await discountmain.methods.allPool().call();
console.log(getPoolsList); //returns ['0x...']
setPoolsList(getPoolsList);
setPoolListLength(getPoolList.length)
}
fetchPools();
}, []);
then
useEffect(() => {
console.log("useeffect", poolsList); // returns []
}, [poolsListLength]);
Use Set
Not my favorite, but you can Stringify the array
useEffect(() => {
console.log(outcomes)
}, [JSON.stringify(outcomes)])

React useState is not updating when set method is called in an interval

I have a websocket server that sends an object containing some hashes every 15 seconds. When the client receives a hash, I want to check with my current hash. If they differ, I want to make a call to an API to fetch new data.
The socket is working and sending the hash correctly. If the data updates on the server I get a different hash. My problem is that the hash variable I use to store the current hash is not updated correctly.
I have disabled the socket listening in my component, just to make sure that that is not the problem. Instead I have added a setInterval to mimik the socket update.
This is my code (socked code disabled but left as a comment):
import { useCallback, useEffect, useState } from "react";
import { useAuth, useSocket } from "../utils/hooks";
const Admin = () => {
const [ questionLists, setQuestionLists ] = useState<QuestionListModel[]>([]);
const { user } = useAuth();
const { socket } = useSocket();
const [ hash, setHash ] = useState<Hash>({questionList: ""});
const fetchHash = useCallback(async () => {
setHash({questionList: "sdhfubvwuedfhvfeuvyqhwvfeuq"});
}, []);
const fetchQuestionLists = useCallback(async () => {
console.log("fetching new question lists");
const response: ApiResponse | boolean = await getQuestionLists(user?.token);
if (typeof response !== "boolean" && response.data) {
setQuestionLists(response.data);
}
}, [hash]);
useEffect(() => {
fetchHash();
fetchQuestionLists();
}, []);
const update = useCallback((newHash: Hash) => {
console.log("called update");
let shouldUpdate = false;
let originalHash = { ...hash };
let updatedHash = { ...newHash };
console.log("new: ", newHash);
console.log("stored: ", originalHash);
if (hash.questionList !== newHash.questionList) {
console.log("was not equal");
updatedHash = { ...updatedHash, questionList: newHash.questionList}
shouldUpdate = true;
}
if (shouldUpdate) {
console.log("trying to set new hash: ", updatedHash);
setHash(updatedHash);
fetchQuestionLists();
}
}, [hash]);
/*useEffect(() => {
socket?.on('aHash', (fetchedHash) => update(fetchedHash));
}, []);*/
useEffect(() => {
setInterval(() => {
update({questionList: "sdhfubvwuedfhvfeuvyqhwvfeuq"});
}, 15000)
}, []);
return (
<>
... Things here later ...
</>
);
};
export default Admin;
After the initial render, and waiting two interval cycles, this is what I see in the console:
fetching new question lists
called update
new: {questionList: 'sdhfubvwuedfhvfeuvyqhwvfeuq'}
stored: {questionList: ''}
was not equal
trying to set new hash: {questionList: 'sdhfubvwuedfhvfeuvyqhwvfeuq'}
fetching new question lists
called update
new: {questionList: 'sdhfubvwuedfhvfeuvyqhwvfeuq'}
stored: {questionList: ''}
was not equal
trying to set new hash: {questionList: 'sdhfubvwuedfhvfeuvyqhwvfeuq'}
fetching new question lists
You can see that stored is empty. That leads me to believe that setHash(updatedHash); never runs for some reason. Why is that?
Having hacked about with this in codepen here: https://codesandbox.io/s/as-prop-base-forked-l3ncvo?file=/src/Application.tsx
This seems to me to be a closure issue as opposed to a React issue. If you have a look in the dev tools, you'll see the state of the component is doing what you're expecting it to. The issue is that the console log is not.
useEffect is only ever going to use an old version of update, so the console won't log what you're expecting. If you add update to the dependency array (and add a clean up so we don't end up with tonnes of intervals) you'll get what you're looking for. Can be seen in the linked codepen.
I think the issue in on this line :
socket?.on('aHash', (hash) => update(hash));
maybe when you register a listener, it keeps the first value of update only,
can you please share useSocket?
const [ hash, setHash ] = useState<Hash>({questionList: ""});
const fetchHash = useCallback(async () => {
setHash({questionList: "sdhfubvwuedfhvfeuvyqhwvfeuq"});
}, []);
Include setHash in your dependency list et voilà
EDIT: Or well, you should include these dependencies in all your useCallback/useEffect hooks since the reference will be lost whenever the component updates. You always have to include all dependencies in the dependency list not to get unpredictable behavior.
use setState(prevValue => {}) to get the the preferred effect. Also, if you running in a Strict mode this will fire the setState twice.
Here is how the code should look like:
import { useCallback, useEffect, useState } from "react";
import { faker } from '#faker-js/faker';
const Admin = () => {
const [ questionLists, setQuestionLists ] = useState([]);
const [ hash, setHash ] = useState({questionList: ""});
const fetchHash = useCallback(async () => {
setHash({questionList: "sdhfubvwuedfhvfeuvyqhwvfeuq"});
}, []);
const fetchQuestionLists = useCallback(async () => {
console.log("fetching new question lists");
const response = {data: {hash: 'asdf-1234'}}
setQuestionLists(response.data);
}, [hash]);
useEffect(() => {
fetchHash();
fetchQuestionLists();
}, []);
const update = (newHash) => {
console.log("called update");
setHash(oldHash => {
console.log('old hash: ', oldHash);
console.log('new hash', newHash);
if (JSON.stringify(oldHash) !== JSON.stringify(newHash)) {
return newHash
}
})
};
/*useEffect(() => {
socket?.on('aHash', (fetchedHash) => update(fetchedHash));
}, []);*/
useEffect(() => {
setInterval(() => {
update({questionList: faker.random.numeric(36)});
}, 15000)
}, []);
return (
<>
<h2>Hash</h2>
{JSON.stringify(hash)}
</>
);
};
export default Admin;
In both cases (socket & interval) the issue is that you need to re-define the callback functions with the new context of the variables in the scope, whenever something changes. In this case you will probably need to put "update" (and whatever other variable you need to "watch") inside the dependancy array of the useEffect.
Ive had a similar issues. Here is how I ended up defining socket callback that updates correctly. Notice that I added the save function (just a function that saves the state into the useState). Also, you need to return a clean up function to turn the socket callback off when the component unmounts. This way every time anything changes in the dependancy array, the hook re-runs and recreates that callback with the new information.
React.useEffect(() => {
socketRef?.current?.on(
'private_message_sent_to_client',
(data: IMessageResult) => {
savePrivateMessages(data);
}
);
return () => {
socketRef?.current?.off('private_message_sent_to_client');
};
}, [meta, selectedChatId, savePrivateMessages]);
And here is an example for you
React.useEffect(() => {
socket?.on('aHash', (hash) => update(hash));
return () => {
socket?.off('aHash')
};
}, [update, hash]);

Can't run functions in .then() Axios React

I have a webpage where I fetch the data with async axios and then make calculations with them.
Here is the code snippet:
const FetchData = async () =>{
console.log("FETCH CALLED");
await Axios.get(`http://localhost:8080/stock/getquote/${props.API}`)
.then(resp => {
setStockData(resp.data);
calculateTrend();
calculateTrendDirection();
})
}
Here, I get the error at calculateTrend() function. My question is, that this .then() should run when the response has arrived, but it seems that it runs before. Because both calculateTrend and calculateTrendDirection works with this fetched data
Edit: The error I am getting is Cannot read property 'previousClosePrice' of undefined. I am sure this exist in the object so mispelling is not a problem
Edit2: I edited my Component according to your solutions and one happens to work, the only thing is that the fetching gets to an infinite loop and fetches multiple times a second. My suspect is the dependencies in useEffect, but I am not sure what to set there.
Here is my full component:
function StockCard(props) {
const [FetchInterval, setFetchInterval] = useState(300000);
const [StockData, setStockData] = useState({});
const [TrendDirection, setTrendDirection] = useState(0);
const [Trend, setTrend] = useState(0);
const FetchData = async () =>{
console.log("FETCH CALLED");
const resp = await Axios.get(`http://localhost:8080/stock/getquote/${props.API}`)
setStockData(resp.data);
}
const calculateTrendDirection = () => {
console.log(StockData.lastPrice);
if(StockData.lastPrice.currentPrice > StockData.lastPrice.previousClosePrice){
setTrendDirection(1);
} else if (StockData.lastPrice.currentPrice < StockData.lastPrice.previousClosePrice){
setTrendDirection(-1);
} else {
setTrendDirection(0);
}
}
const calculateTrend = () => {
console.log(StockData.lastPrice);
var result = 100 * Math.abs( ( StockData.lastPrice.previousClosePrice - StockData.lastPrice.currentPrice ) / ( (StockData.lastPrice.previousClosePrice + StockData.lastPrice.currentPrice)/2 ) );
setTrend(result.toFixed(2));
}
useEffect(() => {
FetchData();
if(StockData.lastPrice){
console.log("LÉTEZIK A LAST PRICE")
calculateTrend();
calculateTrendDirection();
}
const interval = setInterval(() => {
FetchData();
}, FetchInterval)
return() => clearInterval(interval);
},[StockData, FetchData, FetchInterval, calculateTrend, calculateTrendDirection]);
return(
<div>
<CryptoCard
currencyName={StockData.lastPrice? StockData.name : "Name"}
currencyPrice={StockData.lastPrice? `$ ${StockData.lastPrice.currentPrice}` : 0}
icon={<img src="https://upload.wikimedia.org/wikipedia/commons/thumb/4/46/Bitcoin.svg/2000px-Bitcoin.svg.png"/>}
currencyShortName={StockData.lastPrice? StockData.symbol : "Symbol"}
trend={StockData.lastPrice? `${Trend} %` : 0}
trendDirection={StockData.lastPrice? TrendDirection : 0}
chartData={[9200, 5720, 8100, 6734, 7054, 7832, 6421, 7383, 8697, 8850]}
/>
</div>
)
The then block is called only after the promise is fulfilled, so the data is available at that point.
From what I can see, the problem is setStockData tries to set the stockData state variable with the response, but calculateTrend and calculateTrendDirection are called before the state is set because updating state values is batched.
There are several solutions to the problem.
Solution 1:
You can call the two functions after the state is set:
setStockData(resp.data, () => {
calculateTrend();
calculateTrendDirection();
});
Solution 2:
You can use useEffect to call the functions again after the state is updated:
useEffect(() => {
if (stockData) { // or whatever validation needed
calculateTrend();
calculateTrendDirection();
}
}, [stockData]);
Solution 3:
You can pass the parameters to the method:
calculateTrend(resp.data);
calculateTrendDirection(resp.data);
The best option? I think #2, because it also makes sure that the trend and trend direction are re-calculated whenever stock data is updated (from whatever other causes).
I guess in calculateTrend you are using the data which setStockData sets to the state, if that is the case
setState is not happening right after you call the setState, if you want something to execute after correctly update the State then should look at something like this
setStockData(resp.data, () => {
calculateTrend();// this will call once the state gets changed
});
or you could use useEffect
useEffect(() => {
calculateTrend(); // this will call every time when stockData gets changed
}, [stockData])
If you are using stockData inside calculateTrend function and setStockData is an async function, move calculateTrend function to useEffect using stockData as dependency, so every time stockData is updated, calculateTrend and calculateTrendDirection will be called:
useEffect(() => {
const interval = setInterval(() => {
FetchData();
}, FetchInterval);
return() => clearInterval(interval);
}, [FetchInterval]);
useEffect(() => {
if(StockData.lastPrice){
console.log("LÉTEZIK A LAST PRICE")
calculateTrend();
calculateTrendDirection();
}
}, [StockData]);
const FetchData = async () =>{
console.log("FETCH CALLED");
const res = await Axios.get(`http://localhost:8080/stock/getquote/${props.API}`);
setStockData(resp.data);
}

React useEffect causing infinite loop even when dependencies are listed (with Firebase realtime database)

I've been trying to solve this but no matter what solution I do, it is still stuck in a infinite loop.
Here is the code
const [carr, setCarr] = useState({})
useEffect(() => {
sortedRosterCollection.once('value', (snap) => {
snap.forEach((doc) =>{
if (doc.key==="Carr Intermediate"){
var school = doc.key;
var mentorList = doc.val();
var schoolMentor = {school:school, mentors: mentorList};
setCarr(schoolMentor)
console.log(carr)
}
});
});
},[carr]);
No matter what I do "console.log(carr)" is fired endlessly.
If you want to inspect the value of carr whenever it's changed, put it into another use effect:
const [carr, setCarr] = useState({})
useEffect(() => {
sortedRosterCollection.once('value', (snap) => {
snap.forEach((doc) => {
if (doc.key === "Carr Intermediate") {
var school = doc.key;
var mentorList = doc.val();
var schoolMentor = {
school: school,
mentors: mentorList
};
setCarr(schoolMentor)
}
});
});
}, []);
useEffect(() => {
console.log(carr)
}, [carr])
You don't forget cleanup function in useEffect hook like this:
useEffect(() => {
effect
return () => {
cleanup
}
}, [input])
I used to make this problem like you because useEffect can compare two object.
You can make the reference to the link: https://medium.com/javascript-in-plain-english/comparing-objects-in-javascript-ce2dc1f3de7f. And I check changed by using below code:
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
});
return ref.current;
}

React useEffect dependency of useCallback always triggers render

I have a mystery. Consider the following custom React hook that fetches data by time period and stores the results in a Map:
export function useDataByPeriod(dateRanges: PeriodFilter[]) {
const isMounted = useMountedState();
const [data, setData] = useState(
new Map(
dateRanges.map(dateRange => [
dateRange,
makeAsyncIsLoading({ isLoading: false }) as AsyncState<MyData[]>
])
)
);
const updateData = useCallback(
(period: PeriodFilter, asyncState: AsyncState<MyData[]>) => {
const isSafeToSetData = isMounted === undefined || (isMounted !== undefined && isMounted());
if (isSafeToSetData) {
setData(new Map(data.set(period, asyncState)));
}
},
[setData, data, isMounted]
);
useEffect(() => {
if (dateRanges.length === 0) {
return;
}
const loadData = () => {
const client = makeClient();
dateRanges.map(dateRange => {
updateData(dateRange, makeAsyncIsLoading({ isLoading: true }));
return client
.getData(dateRange.dateFrom, dateRange.dateTo)
.then(periodData => {
updateData(dateRange, makeAsyncData(periodData));
})
.catch(error => {
const errorString = `Problem fetching ${dateRange.displayPeriod} (${dateRange.dateFrom} - ${dateRange.dateTo})`;
console.error(errorString, error);
updateData(dateRange, makeAsyncError(errorString));
});
});
};
loadData();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [dateRanges /*, updateData - for some reason when included this triggers infinite renders */]);
return data;
}
The useEffect is being repeatedly triggered when updateData is added as a dependency. If I exclude it as a dependency then everything works / behaves as expected but eslint complains I'm violating react-hooks/exhaustive-deps.
Given updateData has been useCallback-ed I'm at a loss to understand why it should repeatedly trigger renders. Can anyone shed any light please?
The problem lies in the useCallback/useEffect used in combination. One has to be careful with dependency arrays in both useCallback and useEffect, as the change in  the useCallback dependency array will trigger the useEffect to run. 
The “data” variable is used inside useCallback dependency array, and when the setData is called react will rerun function component with new value for data variable and that triggers a chain of calls. 
Call stack would look something like this:
useEffect run
updateData called
setState called
component re-renders with new state data
new value for data triggers useCallback
updateData changed
triggers useEffect again
To solve the problem you would need to remove the “data” variable from the useCallback dependency array. I find it to be a good practice to not include a component state in the dependency arrays whenever possible.
If you need to change component state from the useEffect or useCallback and the new state is a function of the previous state, you can pass the function that receives a current state as parameter and returns a new state.
const updateData = useCallback(
(period: PeriodFilter, asyncState: AsyncState<MyData[]>) => {
const isSafeToSetData = isMounted === undefined || (isMounted !== undefined && isMounted());
if (isSafeToSetData) {
setData(existingData => new Map(existingData.set(period, asyncState)));
}
},
[setData, isMounted]
);
In your example you need the current state only to calculate next state so that should work.
This is what I now have based on #jure's comment above:
I think the problem is that the "data" variable is included in the dependency array of useCallback. Every time you setData, the data variable is changed that triggers useCallback to provide new updateData and that triggers useEffect. Try to implement updateData without a dependecy on the data variable. you can do something like setData(d=>new Map(d.set(period, asyncState)) to avoid passing "data" variable to useCallback
I adjusted my code in the manners suggested and it worked. Thanks!
export function useDataByPeriod(dateRanges: PeriodFilter[]) {
const isMounted = useMountedState();
const [data, setData] = useState(
new Map(
dateRanges.map(dateRange => [
dateRange,
makeAsyncIsLoading({ isLoading: false }) as AsyncState<MyData[]>
])
)
);
const updateData = useCallback(
(period: PeriodFilter, asyncState: AsyncState<MyData[]>) => {
const isSafeToSetData = isMounted === undefined || (isMounted !== undefined && isMounted());
if (isSafeToSetData) {
setData(existingData => new Map(existingData.set(period, asyncState)));
}
},
[setData, isMounted]
);
useEffect(() => {
if (dateRanges.length === 0) {
return;
}
const loadData = () => {
const client = makeClient();
dateRanges.map(dateRange => {
updateData(dateRange, makeAsyncIsLoading({ isLoading: true }));
return client
.getData(dateRange.dateFrom, dateRange.dateTo)
.then(traffic => {
updateData(dateRange, makeAsyncData(traffic));
})
.catch(error => {
const errorString = `Problem fetching ${dateRange.displayPeriod} (${dateRange.dateFrom} - ${dateRange.dateTo})`;
console.error(errorString, error);
updateData(dateRange, makeAsyncError(errorString));
});
});
};
loadData();
}, [dateRanges , updateData]);
return data;
}

Resources