Making calls to a third-party API and using the returned data to drive a React view is something a web developer will need to do many times.
This guide will show how to build a React app that shows users the current weather anywhere in the world. The weather API used in the guide is the Open Weather Map API; however, the concepts are equally relevant to using any API.
The app will allow the user to add a panel, type in a location for each panel, and retrieve the current weather in the location. Any number of panels can be added and the locations will be stored in browser localStorage
and retrieved whenever the app is reloaded. The source for the app can be found here,(https://github.com/ChrisDobby/react-simple-weather-app) along with details of how to set up the weather API. The app behaves like this:
The most important part of this app is the weather information. To get the weather for a particular location, a GET
request is sent to the weather API, which then returns the weather information as JSON. The function to do this looks like this:
1async function getLocationWeather(location) {
2 const result = await fetch(`https://api.openweathermap.org/data/2.5/weather?q=${location}&appid=${process.env.REACT_APP_WEATHER_API_KEY}&units=metric`);
3 return result.json();
4}
and can be consumed like this:
1await getLocationWeather("London");
The above implementation of getLocationWeather
is very naive in that it assumes that the API will be running, the entered location will always be found, and there will be no errors in the API. Instead of simply returning the json()
result from the function, you can check the status of the call to fetch
and return an appropriate result:
1const result = await fetch(
2`https://api.openweathermap.org/data/2.5/weather?q=${location}&appid=${process.env.REACT_APP_WEATHER_API_KEY}&units=metric`);
3
4if (result.status === 200) {
5 return { success: true, data: await result.json() };
6}
7
8return { success: false, error: result.statusText };
This tests the status
property of the result. If it's 200
—the http status code for OK—then the API call has succeeded and you can return the result of the call to json()
. However, if the status
isn't 200
, then the API call has failed for some reason and the statusText
property can be returned as an error description. So that a consumer of this function can simply identify whether the call was successful or not, the function returns a success
property: if success
is true
then the result will have a data
property—which will be the weather data for the location—and if success
is false
, then the result will have an error
property that is a description of the error. For this fairly simple app, the error description is simply the statusText
, but in a real world app it should be a more user-friendly description.
Finally, the getLocationWeather
function should wrap the call to fetch
in a try...catch
block in case an exception is thrown. If an exception is caught, success
should be false
and the error description will be the exception message text but, as above, in a real-world app, this should be more user friendly. The function ends up like this:
1async function getLocationWeather(location) {
2 try {
3 const result = await fetch(`https://api.openweathermap.org/data/2.5/weather?q=${location}&appid=${process.env.REACT_APP_WEATHER_API_KEY}&units=metric`);
4
5 if (result.status === 200) {
6 return { success: true, data: await result.json() };
7 }
8
9 return { success: false, error: result.statusText };
10 } catch (ex) {
11 return { success: false, error: ex.message };
12 }
13}
The consumer of getLocationWeather
can then test the success
property and show the weather data or an error appropriately. This will be dealt with in the next section.
The UI for the demo app has been built using Material UI, but whether using this, a different UI library, or no UI library at all, the concepts for building the view will be the same.
The App
component is the root component for the app and stores the currently visible locations in component state as an array of string
. When initializing the state, the last used list of locations is retrieved from localStorage
and stored as the state like this:
1const [weatherLocations, setWeatherLocations] = React.useState(readFromLocalStorage());
The readFromLocalStorage
function checks whether there are any locations in localStorage
. If there are, it returns them, and if not, it returns an empty array.
This component also includes a helper function—updateLocations
—that accepts an array of string as a parameter and both writes the array into localStorage
and sets the weather location's state. As long as any updates to locations go through this function, then localStorage
and state will be kept synchronized:
1const updateLocations = locations => {
2 setWeatherLocations(locations);
3 saveToLocalStorage(locations);
4};
The structure of the view looks like this:
1<div>
2 <AppBar position="static">
3 ...
4 </AppBar>
5 <Grid>
6 {weatherLocations.map((location, index) => (
7 <Grid key={location} xs={12} sm={6} md={4} lg={3} item>
8 <WeatherCard
9 location={location}
10 canDelete={!location || canAddOrRemove}
11 onDelete={removeAtIndex(index)}
12 onUpdate={updateAtIndex(index)}
13 />
14 </Grid>
15 ))}
16 </Grid>
17 <Fab
18 onClick={handleAddClick}
19 color="secondary"
20 disabled={!canAddOrRemove}
21 >
22 <AddIcon />
23 </Fab>
24</div>
The Fab
component is a Material UI button, which when clicked will call handleAddClick
, which simply adds an empty location string to the state setWeatherLocations([...weatherLocations, ""])
.
The view for a location is the responsibility of the WeatherCard
component. If the location
prop being viewed is an empty string, then this location has been newly added and the WeatherCard
will render a component allowing the user to enter a location. If the location has already been entered—location
prop is not an empty string—then the LocationWeather
component is rendered, which is responsible for retrieving the weather for a location and displaying it inside the WeatherCard
.
This component accepts a single prop—location
—and stores two states: one for the weather data that has been retrieved and the other for any error message that was returned from the API.
To retrieve the weather data, you can use the effect hook with a parameter of the location
prop; this means the side effect will be called whenever the location
prop changes, which in this app is when the component is mounted. The code looks like this:
1React.useEffect(() => {
2 const getWeather = async () => {
3 const result = await getLocationWeather(location);
4 setWeatherData(result.success ? result.data : {});
5 setApiError(result.success ? "" : result.error);
6 };
7
8 getWeather();
9}, [location]);
Because the function passed into useEffect
cannot be an async
function, an async inline function is declared inside the effect function—getWeather
—which is called (but not awaited) by the effect. This function calls the getLocationWeather
function and, depending on the success
property of the result, sets the weather data and error states; if the API call was successful, then the weather data is set to the result and error description to an empty string, and if the call failed, then weather data is set to an empty object and the error description to the error
property. The component can then be rendered.
Currently, while the app is waiting for the API call to return, the component will show nothing. If the call returns quickly, then this is fine; however, if the app is running on a slow network or the API is running slowly, then leaving the component view blank is not a very good user experience. To improve this experience, show a loading indicator if the API call takes longer than a specific length of time. To do this, wadd a new state to the component that will be set to true
when you want to display a loading indicator:
1const [isLoading, setIsLoading] = React.useState(false);
Then, inside useEffect
, set a timeout that will call setIsLoading(true)
after 500ms, meaning that 500ms after getLocationWeather
is called, a loading indicator will be shown. To ensure the indicator isn't shown if the API call has taken less than 500ms, the timeout should be cleared after the call has completed. The timeout should also be cleared in the return of the effect so that the component doesn't create a memory leak by updating state after a component has been dismounted. So the final useEffect
will look like this:
1React.useEffect(() => {
2 const loadingIndicatorTimeout = setTimeout(() => setIsLoading(true), 500);
3 const getWeather = async () => {
4 const result = await getLocationWeather(location);
5 clearTimeout(loadingIndicatorTimeout);
6 setIsLoading(false);
7 setWeatherData(result.success ? result.data : {});
8 setApiError(result.success ? "" : result.error);
9 };
10
11 getWeather();
12 return () => clearTimeout(loadingIndicatorTimeout);
13}, [location]);
The view for this component looks like this:
1<div>
2 <LoadingIndicator isLoading={isLoading} />
3 <ErrorMessage apiError={apiError} />
4 <WeatherDisplay weatherData={weatherData} />
5</div>
The LoadingIndicator
and ErrorMessage
components simply display a spinner and error text, respectively, and the WeatherDisplay
component transforms the data from the API and displays it in a view.
Finally, take the data from the weather API and show it to the user. The data returned by the OpenWeatherMap API can be found here. This app will show the temperature, weather icon, wind speed, wind direction, and a description of the weather.
WeatherDisplay
accepts one prop—weatherData
in the OpenWeatherMap format—and inside a useMemo hook will take this prop and transform it into an object that can be displayed. The code to do this is:
1const { temp, description, icon, windTransform, windSpeed } = React.useMemo(() => {
2 const [weather] = weatherData.weather || [];
3 return {
4 temp: weatherData.main && weatherData.main.temp ? Math.round(weatherData.main.temp).toString() : "",
5 description: weather ? weather.description : "",
6 icon: weather ? `http://openweathermap.org/img/wn/${weather.icon}@2x.png` : "",
7 windTransform: weatherData.wind ? weatherData.wind.deg - 90 : null,
8 windSpeed: weatherData.wind ? Math.round(weatherData.wind.speed) : 0,
9 };
10}, [weatherData]);
For everything except the windTransform
property, this code checks that the required properties exist in the weatherData
. If they do, return them; otherwise, set a blank or empty value. The windTransform
property will be used to create a css transform on a right arrow icon. Therefore, if a wind direction has been returned in the wind.deg
property, then it needs to be reduced by 90 degrees. The code for the view looks like this:
1<>
2 {temp && <Typography variant="h6">{temp}°C</Typography>}
3 {icon && (
4 <Avatar className={classes.largeAvatar} alt={description} src={icon} />
5 )}
6 {windSpeed > 0 && (
7 <>
8 <Typography variant="h6">{`${windSpeed} km/h`}</Typography>
9 {windTransform !== null && (
10 <ArrowRightAltIcon style={{ transform: `rotateZ(${windTransform}deg)` }} />
11 )}
12 </>
13 )}
14</>
Which will render a view like this: