Reputation: 124
I am using a free account on weatherstack.com to get weather info on a specified city. The relevant component from the App is the following
const WeatherInfo = (props) => {
const [weather, setWeather] = useState()
const url = 'http://api.weatherstack.com/current?access_key='
+ process.env.REACT_APP_API_KEY + '&query=' + props.city
axios.get(url)
.then(response => {setWeather(response.data)})
return (
<div>
<p>Weather in {props.city}</p>
<p><b>Temperature:</b> {weather.current.temperature} celsius</p>
<img src={weather.current.weather_icons[0]} alt="Country flag" width="150"></img>
<p><b>Wind:</b> {weather.current.wind_speed} mph direction {weather.current.wind_dir}</p>
</div>
)
}
This fails with TypeError: weather.current is undefined
because I believe the axios.get
is asyncronously called so the return happens before the setWeather()
call inside the .then()
. So I replaced the return statement with the following:
if (weather === undefined) {
return (
<div>Loading...</div>
)
}
else {
return (
<div>
<p>Weather in {props.city}</p>
<p><b>Temperature:</b> {weather.current.temperature} celsius</p>
<img src={weather.current.weather_icons[0]} alt="Country flag" width="150"></img>
<p><b>Wind:</b> {weather.current.wind_speed} mph direction {weather.current.wind_dir}</p>
</div>
)
}
This succeeds briefly and then fails with the same error as previous. I guess I must have some fundamental misunderstanding of the correct way to wait for a response before rendering.
Question What is the correct way to wait for REST call when using react?
Upvotes: 1
Views: 105
Reputation: 50674
Whenever you set state, your functional component will re-render, causing the body of the function to execute again. This means that when your axios call does eventually complete, doing setWeather(response.data)
will cause your functional component body to run again, making you do another get request.
Since you only want to run your axios get request once, you can put your axios get request inside of the useEffect()
hook. This will allow you to only run your get request when the component initially mounts:
import React, {useState, useEffect} from "react";
...
const WeatherInfo = (props) => {
const [weather, setWeather] = useState();
useEffect(() => {
const url = 'http://api.weatherstack.com/current?access_key=' + process.env.REACT_APP_API_KEY + '&query=' + props.city
const cancelTokenSource = axios.CancelToken.source();
axios.get(url, {cancelToken: cancelTokenSource.token})
.then(response => {setWeather(response.data)});
return () => cancelTokenSource.cancel();
}, [props.city, setWeather]);
return weather ? (
<div>
<p>Weather in {props.city}</p>
<p><b>Temperature:</b> {weather.current.temperature} celsius</p>
<img src={weather.current.weather_icons[0]} alt="Country flag" width="150"></img>
<p><b>Wind:</b> {weather.current.wind_speed} mph direction {weather.current.wind_dir}</p>
</div>
) : <div>Loading...</div>;
}
The above useEffect()
callback will only run when the things in the dependency array (second argument of useEffect) change. Since setWeather
is a function and doesn't change, and props.city
only changed when the prop is changed, the callback is only executed when your component initially mounts (provided the city
prop isn't changing). The useEffect()
hook also allows you to return a "clean-up" function, which, in this case, will get called when your component unmounts. Here I have used Axios's CancelToken
to generate a source token so that you can cancel any outgoing requests if your component unmounts during the request.
Upvotes: 1
Reputation: 7905
Your code will continuously make the axios call on every rerender. You need to put that call into a function and call if once on Component load. Also your render function should check to see if the state is set. Here is a basic example and a Sandbox: https://codesandbox.io/s/zen-glitter-1ci2j?file=/src/App.js
import React from "react";
import "./styles.css";
const axioscall = () => {
return new Promise((resolve) => {
setTimeout(() => {
resolve({ city: "Scottsdale", tempt: 75 });
}, 1000);
});
};
export default (props) => {
const [weather, setWeather] = React.useState(null);
React.useEffect(() => {
axioscall()
.then((result) => setWeather(result));
}, []);
return (
<div>
{weather ? (
<>
<p>Weather in {weather.city}</p>
<p>
<b>Temperature:</b> {weather.temp} F
</p>
</>
) : (
<div>Loading...</div>
)}
</div>
);
};
Upvotes: 1
Reputation: 4381
First of all wrap axios
in useEffect:
useEffect(() => {
axios.get(url)
.then(response => {setWeather(response.data)})
});
then wrap return part with weather
in condition:
return (
<div>
<p>Weather in {props.city}</p>
{
weather && (
<>
<p><b>Temperature:</b> {weather.current.temperature} celsius</p>
<img src={weather.current.weather_icons[0]} alt="Country flag" width="150"></img>
<p><b>Wind:</b> {weather.current.wind_speed} mph direction {weather.current.wind_dir}</p>
</>
)
}
</div>
)
Upvotes: 1