Fetch the current weather with `weather-js` and display in `react-blessed`
July 17, 2020
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.
- Bootstrap
react-blessed
Application - Add ESLint Rules to
react-blessed
App - Change text font with
figlet
- Extract Component and Add
useInterval
hook - Fetch and display current weather with
weather-js
- Extract custom hook to simplify data fetching
- Change text color with
chalk
andgradient-string
- Position and Align Text inside a
<box>
Element - Make a Percentage Based Layout
- Layout Dashboard with
react-blessed-contrib
Grid
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 call
fetchWeather, which is a function that is wrapped in
React.useCallbackso that we don't keep getitng a new instance of it. The function is
async, which will allow us to
await` 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');
weather-js
Format the Weather from 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...
Tweet about this post and have it show up here!