Originally posted here.
React is a fantastic piece of engineering that has allowed developers to implement declarative user interfaces that are easy to work with and reason about. It can be daunting on initial encounter (JSX), but the API surface is small and the concepts are few enough to allow developers get productive quickly. This series will primarily be a walkthrough of building small useful apps with React (what did you expect?) and varied technologies while touching on useful concepts along the way. Strap in.
Often, tutorials start off with clichés like a Todo or Counter; these are useful for learning basic concepts, but are inadequate for our needs. We need something more challenging. Enter Cerebro: a cross platform extensible electron app that provides the perfect tools to build something small but useful with React. It has many plugins, some useful, and some not, but we are hopefully going to be contributing to the former category by creating a simple weather plugin.
To proceed, you need basic understanding of how React (surprise surprise), JavaScript, JSON and REST APIs work. Without these you might struggle a bit, but fear not, the internet is a wonderful place with lots - of - useful - resources to bring you up to speed.
Now we can do a bit of setup:
- Download and install the most recent version of Cerebro.
- Follow the instructions on setting up a new plugin.
- Read through the plugin structure to get a good idea of what the plugin API allows.
Our plugin, cerebro-weather, will allow users to search for a city and then show a concise preview of the weather conditions of that city if it exists. This data will be powered by the excellent API from MetaWeather. Start off by adding the following to your index.js
file:
export const fn = ({ term, display }) => {
display({
title: `Weather in ${term}`,
subtitle: 'Cloudy with a chance of meatballs',
})
}
This uses object destructuring, an ES2015 feature, to retrieve the display
function and term
from the scope object that is passed into the plugin by Cerebro. It is easier than always typing out scope.display
or scope.term
. If everything has been setup correctly, when you type in 'london' in Cerebro, you should see this:
Now, we can add a preview, which is where our plugin's UI will be displayed. Create a preview.js
file and add the following to it:
import React, { Component, PropTypes } from 'react'
export default class Preview extends Component {
render() {
const { city } = this.props
return (
<div>
{city == 'london' ? 'Endless Rain' : 'Look Outside'}
</div>
)
}
}
Preview.propTypes = {
city: PropTypes.string.isRequired,
}
This is basic stuff. In this snippet, we are simply creating a react component that defines a city
prop which is the term
the user typed in passed from the index file. It renders a simple div with text. Update index.js
to display the new preview:
+import React from 'react'
+import Preview from './preview'
export const fn = ({ term, display }) => {
display({
title: `Weather in ${term}`,
subtitle: 'Cloudy with a chance of meatballs',
+ getPreview: () => <Preview city={term} />
})
}
That should give us something like this:
With that done, we can now fetch (no pun intended) the actual data from MetaWeather with the fetch
API. Create an api.js
file with the following:
const BASE_URL = 'https://www.metaweather.com/api/location';
export const getWeather = city => {
return fetch(`${BASE_URL}/search/?query=${city}`)
.then(response => response.json())
.then(results => fetch(`${BASE_URL}/${results[0].woeid}`))
.then(response => response.json())
}
This function makes an initial request to the API to search for a given city. If the city exists, an array of locations is returned. We convert this response to JSON and retrieve the weather details for the first location with the location's woeid
, which is its unique identifier and the only actual way to retrieve weather data from the API. This final response is then converted to JSON and displayed in a list in our Preview
component.
import React, { Component, PropTypes } from 'react'
import { getWeather } from './api'
const headers = {
weather_state_name: 'State',
applicable_date: 'Date',
min_temp: 'Min Temp',
max_temp: 'Max Temp',
the_temp: 'Temp',
humidity: 'Humidity',
}
export default class Preview extends Component {
constructor(props) {
super(props)
this.state = {
weather: null,
}
}
componentDidMount() {
getWeather(this.props.city).then(weather => this.setState({ weather }))
}
render() {
if (! this.state.weather) return <div>Loading...</div>
const weather = this.state.weather.consolidated_weather[0]
return (
<ul>
{Object.keys(headers).map(metric => (
<ul key={metric}>{`${headers[metric]}: ${weather[metric]}`}</ul>
))}
</ul>
)
}
}
Preview.propTypes = {
city: PropTypes.string.isRequired,
}
This gets the weather data with our API function, and displays the first consolidated weather report. Eventually, we will display all the available weather reports. This is what we should have now:
d144c25: The Awwward is coming home
The data returned from our API call contains more information than we need, so with headers
object, we will filter out just the data we need; this is a recurring theme when consuming external REST APIs.
Our app is in desperate need of styling, so let us pull in a package to help with that. Run npm install bootstrap@4.0.0-alpha.6 --save
. Import boostrap css with import styles from 'bootstrap/dist/css/bootstrap.css'
, and change the return part of the render function to:
return (
<table className={styles.table}>
<thead>
<tr>
{Object.keys(headers).map(header => <th key={header}>{headers[header]}</th>)}
</tr>
</thead>
<tbody>
<tr>
{Object.keys(headers).map(metric => (
<td key={metric}>{weather[metric]}</td>
))}
</tr>
</tbody>
</table>
)
If you were wondering, what className={styles.table}
is, it is a CSS Module one of the many ways of styling in React. Okay, detour over. Now we have a much better looking app.
We can now focus on rendering the remaining weather reports. For brevity, I will only include the changes.
const weatherReports = this.state.weather.consolidated_weather
<tbody>
{weatherReports.map((report, index) => (
<tr key={index}>
{Object.keys(headers).map(metric => {
let value = report[metric]
if (typeof value == 'number' && value % 1 != 0) {
value = value.toFixed(2)
}
return <td key={metric}>{value}</td>
})}
</tr>
))}
</tbody>
In the first snippet, we stop picking the first element of the consolidated_weather
array and choose the entire thing instead. In addition to displaying the extra reports, the function now formats decimal numbers to only two decimal places. This gives us:
which you have to admit is much nicer and actually useful. Also, A minor change was made to the temperature header to add the unit (c)
. While this is great, we can make one more tweak to improve the general look and experience. Add a new header to the top of our headers object weather_state_abbr: '#'
. This will be used to display a small image representation of the weather state. Add the following to your render function just under the first if
statement for formatting decimals.
if (metric == 'weather_state_abbr') {
value = <img src={`https://www.metaweather.com/static/img/weather/${value}.svg`} />
}
This uses an SVG image from MetaWeather to give us:
Pretty good! We have done a lot, but as always with software, it can be improved further. Showing the city and country would be a start, along with time zone, but I will leave those for the reader to implement. Suggestions, feedback and PRs are welcome at the github repo. Thanks for reading.
Top comments (0)