Fetch the current weather with `weather-js` and display in `react-blessed`

AvatarElijah Manor 3 months
1154 words 6 min read
The above video is hosted on egghead.io.

This is the 5th post in a series where we will be creating a developer dashboard in the terminal using react-blessed and react-blessed-contrib. For more information about the series and to take a sneak peak at what we're building, go to the 1st post in the series for more context.

  1. Bootstrap react-blessed Application
  2. Add ESLint Rules to react-blessed App
  3. Change text font with figlet
  4. Extract Component and Add useInterval hook
  5. Fetch and display current weather with weather-js
  6. Extract custom hook to simplify data fetching
  7. Change text color with chalk and gradient-string
  8. Position and Align Text inside a <box> Element
  9. Make a Percentage Based Layout
  10. Layout Dashboard with react-blessed-contrib Grid
... ~10 more lessons to come ...

NOTE: You can find the code for this project in GitHub and you watch the whole Build a Terminal Dashboard with React video series on egghead.io.

Add the Weather Module

The goal for this lesson is to augment our terminal application with some weather information. To grab the weather, we'll use the weather-js node module from npm. The reason I picked this module is because it doesn't require a special token to use it. The information comes from the weather service of msn.com.

You can install the module from your terminal with the following command:

npm install weather-js

Playground: Callback

Before we integrate the library into our app, let's play around with the API to get an idea of how it works. Let's start by creating a new weather variable and requiring the weather-js library.

Then we'll call the find method off of weather and pass search of "Nashville, TN" with a degreeType of "Fahrenheit". This API takes a callback function that accepts an error and result. If there's an error, we'll console.error the message otherwise we'll console.log the stringified version of the result.

const weather = require('weather-js');

weather.find(
  { search: 'Nashville, TN', degreeType: 'F' },
  (error, result) => {
    if (error) console.error(error);

    console.log(JSON.stringify(result, null, 2));
  },
);

Weather Output

If we run the code snippet from above, we'll get a bunch of weather information from msn.com. Once we integrate the data into our dashboard app, we'll make a function to gather notable weather info to display.

Playground: Promise

In our case, I don't really want to use the callback style API that's provided by weather-js. I'd rather use a promise so that I can also use it with async / await. Thankfully, in Node there's a util.promisify method that can convert an error-first callback into a promise.

To convert the callback to a promise, you wrap the function in util.promisfy and then use the resulting function as you would a normal promise.

const weather = require('weather-js');
const util = require('util');

findWeather({ search: 'Nashville, TN', degreeType: 'F' })
  .then((result) => {
    console.log(JSON.stringify(result, null, 2));
  })
  .catch((error) => {
    console.error(error);
  });

NOTE: The above code will have the same output as the previous code snippet had, but it uses a promise instead of a callback

Fetch the Weather inside of Component

We'll add some new weather state to our app using React.useState to maintain the status, error, and data of our weather information.

Inside of a React.useEffect we'll callfetchWeather, which is a function that is wrapped inReact.useCallbackso that we don't keep getitng a new instance of it. The function isasync, which will allow us toawait` the converted promise. Before we actually fetch the weather we update the weather state so we know the status is currently loading, then we fetch the data passing our options.

If the fetch resolves successfully then we'll update the weather state with status of "complete" and pass the data that was returned. Or, if there was an exception we'll catch that and update the weather state to indicate that there was an error and capture the problem.

const [weather, setWeather] = React.useState({
  status: 'loading',
  error: null,
  data: null,
});

const fetchWeather = React.useCallback(async () => {
  setWeather({ status: 'loading', error: null, data: null });
  let data;
  try {
    data = await findWeather({ search, degreeType });
    setWeather({ status: 'complete', error: null, data });
  } catch (error) {
    setWeather({ status: 'error', error, data: null });
  }
}, [search, degreeType]);

React.useEffect(() => {
  fetchWeather();
}, [fetchWeather]);

NOTE: Currently our code will only update after the code is first mounted. Technically it could update if either of the deps were changed, but nothing is changing those at the moment.

Fix the Async / Await Error

If we happend to run our code at this moment, we get an error "ReferenceError: regeneratorRuntime is not defined". Unforutneatly, this error is a bit tricky to decipher, however, after Googling for a while you should eventually land to the need for a special babel plugin.

ReferenceError: regeneratorRuntime is not defined

One way to get around this error is to install the @babel/plugin-transform-runtime plugin. Once installed, you'll need to add it to your babel configuration as a plugin.

require('@babel/register')({
  presets: [['@babel/preset-env'], ['@babel/preset-react']],
  plugins: ['@babel/plugin-transform-runtime'],
});
require('./dashboard');

Format the Weather from weather-js

Once we get the weather information back from weather-js, we need to massage the data to be something consumable by the user. The following code will format the weather to something like: 92°F and Mostly Sunny (74°F → 94°F);

const formatWeather = ([results]) => {
  const { location, current, forecast } = results;
  const degreeType = location.degreetype;
  const temperature = `${current.temperature}°${degreeType}`;
  const conditions = current.skytext;
  const low = `${forecast[1].low}°${degreeType}`;
  const high = `${forecast[1].high}°${degreeType}`;

  return `${temperature} and ${conditions} (${low}${high})`;
};

Render the Weather in Component

Now that we have both the weather and a function to format the weather, it's time to show the actual text inside our React component. To do this, we'll update the return from our component. If our status is loading, then show "Loading...", if there is an error, then show "Error" along with the problem, and otherwise show the formatted weather data.

return (
  <box>
    {`${date}

${time}

${
  weather.status === 'loading'
    ? 'Loading...'
    : weather.error
    ? `Error!: ${weather.error}`
    : formatWeather(weather.data)
}`}
  </box>
);

NOTE: In later lessons we will focus on how to provide more advanced layout with the text (alignment, etc...) and show how to style the output with color.

Add useInterval to Regularly Update the Weather

Our previous code was only updating the weather on initial mount (or if the deps where changed), but if we keep our Terminal Dashboard open for a long time we'd probably want the weather information updated every now and then. So, we can add the following snippet to update the weather at a pre-defined interval (which is passed in via props).

export default function Today({
updateInterval = 900000, // 15 mins
search = 'Nashville, TN', degreeType = 'F', }) { /* ... more code ... */
useInterval(() => {
fetchWeather();
}, updateInterval);
/* ... more code ... */ }

Final Output

const App = () => {
  return <Today updateInterval={5000} />;
};

Our final output thus far in this series should look something like this. You should be able to see the "Loading..." indicator briefly while the weather is fetched, then it's replaced by the real weather information that is updated every so often (in this case I set the parent interval to 5 seconds).

NOTE: The final result of this series will have many more widgets than just the Today component. Here is a preview of what the final output may look like...

Share article