Stiofán Fordham
Stiofán Fordham

Reputation: 124

Right way to use REST API in react?

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

Answers (3)

Nick Parsons
Nick Parsons

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

codemonkey
codemonkey

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

Daniil Loban
Daniil Loban

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

Related Questions