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);
}
Related
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]);
Here is my code:
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 () =>{
const resp = await Axios.get(`http://localhost:8080/stock/getquote/${props.API}`)
setStockData(resp.data);
}
const calculateTrendDirection = () => {
if(StockData.lastPrice.currentPrice > StockData.lastPrice.previousClosePrice){
setTrendDirection(1);
} else if (StockData.lastPrice.currentPrice < StockData.lastPrice.previousClosePrice){
setTrendDirection(-1);
} else {
setTrendDirection(0);
}
}
const calculateTrend = () => {
var result = 100 * Math.abs( ( StockData.lastPrice.previousClosePrice - StockData.lastPrice.currentPrice ) / ( (StockData.lastPrice.previousClosePrice + StockData.lastPrice.currentPrice)/2 ) );
setTrend(result.toFixed(2));
}
useEffect(() => {
FetchData();
const interval = setInterval(async () => {
await FetchData();
}, FetchInterval)
return() => clearInterval(interval);
},[FetchInterval]);
useEffect(()=>{
if(StockData.lastPrice){
console.log("Trends calculated", StockData.name);
calculateTrend();
calculateTrendDirection();
}
},[StockData])
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>
)
}
export default StockCard;
The basic idea is. I have a backend from which I fetch data let's say every minute(this is why i need setInterval) and I have cards which are showing off the data i fetched. I have an expression so it says generic things like "Name" until the data has arrived, then it should re-render with the real data.
But this doesn't happen. It fetches all the data, I can log it out but it doesn't get updated.
And error number 2 is it says that in the useEffects i should include the functions into dependencies.
So for example in the second useEffect where I call the function calculateTrend() and calculateTrendDirection, it says I should include not only the StockData but the two functions too.
I tried #Ozgur Sar 's fix and it worked, so it turned out the problem was "timing" with my api calls
I'm quite new to React and I don't always understand when I have to use hooks and when I don't need them.
What I understand is that you can get/set a state by using
const [myState, setMyState] = React.useState(myStateValue);
So. My component runs some functions based on the url prop :
const playlist = new PlaylistObj();
React.useEffect(() => {
playlist.loadUrl(props.url).then(function(){
console.log("LOADED!");
})
}, [props.url]);
Inside my PlaylistObj class, I have an async function loadUrl(url) that
sets the apiLoading property of the playlist to true
gets content
sets the apiLoading property of the playlist to false
Now, I want to use that value in my React component, so I can set its classes (i'm using classnames) :
<div
className={classNames({
'api-loading': playlist.apiLoading
})}
>
But it doesn't work; the class is not updated, even if i DO get the "LOADED!" message in the console.
It seems that the playlist object is not "watched" by React. Maybe I should use react state here, but how ?
I tested
const [playlist, setPlaylist] = React.useState(new PlaylistObj());
React.useEffect(() => {
//refresh playlist if its URL is updated
playlist.loadUrl(props.playlistUrl).then(function(){
console.log("LOADED!");
})
}, [props.playlistUrl]);
And this, but it seems more and more unlogical to me, and, well, does not work.
const [playlist, setPlaylist] = React.useState(new PlaylistObj());
React.useEffect(() => {
playlist.loadUrl(props.playlistUrl).then(function(){
console.log("LOADED!");
setPlaylist(playlist); //added this
})
}, [props.playlistUrl]);
I just want my component be up-to-date with the playlist object. How should I handle this ?
I feel like I'm missing something.
Thanks a lot!
I think you are close, but basically this issue is you are not actually updating a state reference to trigger another rerender with the correct loading value.
const [playlist, setPlaylist] = React.useState(new PlaylistObj());
React.useEffect(() => {
playlist.loadUrl(props.playlistUrl).then(function(){
setPlaylist(playlist); // <-- this playlist reference doesn't change
})
}, [props.playlistUrl]);
I think you should introduce a second isLoading state to your component. When the effect is triggered whtn the URL updates, start by setting loading true, and when the Promise resolves update it back to false.
const [playlist] = React.useState(new PlaylistObj());
const [isloading, setIsLoading] = React.useState(false);
React.useEffect(() => {
setIsLoading(true);
playlist.loadUrl(props.playlistUrl).then(function(){
console.log("LOADED!");
setIsLoading(false);
});
}, [props.playlistUrl]);
Use the isLoading state in the render
<div
className={classNames({
'api-loading': isLoading,
})}
>
I also suggest using the finally block of a Promise chain to end the loading in the case that the Promise is rejected your UI doesn't get stuck in the loading "state".
React.useEffect(() => {
setIsLoading(true);
playlist.loadUrl(props.playlistUrl)
.then(function() {
console.log("LOADED!");
})
.finally(() => setIsLoading(false));
}, [props.playlistUrl]);
Here you go:
import React from "react";
class PlaylistAPI {
constructor(data = []) {
this.data = data;
this.listeners = [];
}
addListener(fn) {
this.listeners.push(fn);
}
removeEventListener(fn) {
this.listeners = this.listeners.filter(prevFn => prevFn !== fn)
}
setPlayList(data) {
this.data = data;
this.notif();
}
loadUrl(url) {
console.log("called loadUrl", url, this.data)
}
notif() {
this.listeners.forEach(fn => fn());
}
}
export default function App() {
const API = React.useMemo(() => new PlaylistAPI(), []);
React.useEffect(() => {
API.addListener(loadPlaylist);
/**
* Update your playlist and when user job has done, listerners will be called
*/
setTimeout(() => {
API.setPlayList([1,2,3])
}, 3000)
return () => {
API.removeEventListener(loadPlaylist);
}
}, [API])
function loadPlaylist() {
API.loadUrl("my url");
}
return (
<div className="App">
<h1>Watching an object by React Hooks</h1>
</div>
);
}
Demo in Codesandbox
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);
})
}
}
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;
}