简体   繁体   中英

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?

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>
)

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.

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>
  );
};

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM