Project: Weather App

The idea of this project is to fetch weather forecasts from the OpenWeatherMap API based on the city name provided by the user (Figure 10). In this project we’ll be utilizing Navigator to switch between the screens and show a navigation menu on top with a button to go back.

Figure 10: Weather app with city name search input
Figure 10: Weather app with city name search input

Also, there will be a “remember me” feature to save the entered city name for future uses. The persistence will be implemented with AsyncStorage.

The resulting forecast data will be shown in a grid with the date, description, and temperature in F and C, as shown in Figure 11.

Figure 11: Weather forecast for London, UK for every 3 hours
Figure 11: Weather forecast for London, UK for every 3 hours

To get started, use the scaffolding provided by the React Native CLI tool (if you don’t have v0.1.7, follow the instructions at the beginning of this chapter to get it):

1 $ react-native init weather

The command will output something like this:

 1 This will walk you through creating a new React Native project in /Users/azat/Do\
 2 cuments/Code/react/ch9/weather
 3 Installing react-native package from npm...
 4 Setting up new React Native app in /Users/azat/Documents/Code/react/ch9/weather
 5 To run your app on iOS:
 6    Open /Users/azat/Documents/Code/react/ch9/weather/ios/weather.xcodeproj in Xc\
 7 ode
 8    Hit the Run button
 9 To run your app on Android:
10    Have an Android emulator running (quickest way to get started), or a device c\
11 onnected
12    cd /Users/azat/Documents/Code/react/ch9/weather
13    react-native run-android

Open the iOS project in Xcode with this command:

1 $ open ios/weather.xcodeproj

In addition to the already existing index.ios.js, create four files, forecast.ios.js, search.ios.js, weather-api.js, and response.json, so the project structure looks like this:

 1 /weather
 2   /android
 3     ...
 4   /ios
 5     /weather
 6       /Base.Iproj
 7         ...
 8       /Images.xcassets
 9         ...
10       - AppDelegate.h
11       - AppDelegate.m
12       - Info.plist
13       - main.m
14     /weather.xcodeproj
15       /project.xcworkspace
16         ...
17       /xcshareddata
18         ...
19       /xcuserdata
20         ...
21       - project.pbxproj
22     /weatherTests
23       - Info.plist
24       - weatherTests.m
25   /node_modules
26     ...
27   - .flowconfig
28   - .gitignore
29   - .watchmanconfig
30   - forecast.ios.js
31   - index.android.js
32   - index.ios.js
33   - package.json
34   - response.json
35   - search.ios.js
36   - weather-api.json

The files search.ios.js and forecast.ios.js will be the components for the first screen, which will have the input field for the city name, and the second screen, which will show the forecast, respectively. But before we start implementing Search and Forecast, let’s code the App component and the navigation that will enable us to switch between the Search and Forecast screens.

In the index.ios.js file, add the React Native classes shown in the following listing. The only classes that should be unfamiliar to you by now are AsyncStorage and PixelRatio—everything else was covered earlier in this chapter:

 1 'use strict'
 2 
 3 var React = require('react-native')
 4 
 5 var {
 6   AppRegistry,
 7   StyleSheet,
 8   Text,
 9   View,
10   Navigator,
11   ListView,
12   AsyncStorage,
13   TouchableOpacity,
14   PixelRatio
15 } = React

Import Search. The const is an ES6 thing. You can use var or learn about const and let in ES6/ES2016 cheatsheet.

1 const Search = require('./search.ios.js')

Now let’s create an abstraction for the storage, i.e., AsyncStorage. You can use AsyncStorage directly, but it’s better to have an abstraction like the one shown here. The AsyncStorage interface is very straightforward. It uses the getItem(), removeItem(), and setItem() methods. I’m sure you can guess what they mean. The only interesting part is that for getItem() we need to utilize Promise. The idea behind it is that getItem() results are asynchronous. There’s more on ES6 promises in the cheatsheet.

 1 const storage = {
 2   getFromStorage(name, callback) {
 3     AsyncStorage.getItem(name).then((value) => {
 4       console.log(`AsyncStorage GET for ${name}: "${value}"`)
 5       if (value) callback(value)
 6       else callback(null)
 7     }).done()
 8   },
 9   setInStorage(name, value) {
10     console.log(`AsyncStorage SET for ${name}: "${value}"`)
11     AsyncStorage.setItem(name, value)
12   },
13   removeItem: AsyncStorage.removeItem
14 }

Remove the boilerplate component and replace it with App:

1 const App = React.createClass({
2   render() {
3     return (

The App component needs to render Navigator. We provide the Search component as the initial route:

1       <Navigator
2         initialRoute={{
3           name: 'Search',
4           index: 0,
5           component: Search,
6           passProps: {
7             storage: storage
8           }
9         }}

The ref property is how we can access the Navigator instance in the App component itself. The navigator object will be in this.refs.navigator, assuming this refers to App:

1         ref='navigator'

The navigation bar is the menu at the top of the screen, and we render it by using the Navigator.NavigationBar component and supplying the routeMapper property (we still need to implement this):

1         navigationBar={
2           <Navigator.NavigationBar
3             routeMapper={NavigationBarRouteMapper}
4             style={styles.navBar}
5           />
6         }

While the navigation bar is a nice-to-have but not necessary feature, the next property is important. It basically renders every route. In this example, I assume that the route argument has everything I need, such as components and properties. Another way to implement Navigator is to pass only IDs in route and resolve the component object from the ID by using some hash table (i.e., a route stack object).

1         renderScene={(route, navigator) => {
2           let props = route.passProps

You can control where the navigator object is in children by setting it to whatever property you want to use. I keep it consistent; the navigator object is placed under this.props.navigator:

1           props.navigator = navigator
2           props.name = route.name

After we’ve added navigator and name, the props object is ready for rendering:

1           return React.createElement(route.component, props)

And then, let’s close all the parentheses and tags:

1         }}
2       />
3     )
4   }
5 })

We are done with most of the heavy lifting. If you opted not to implement the navigation bar, you can skip NavigationBarRouteMapper. If you want to use the bar, this is how you can implement it.

The route mapper must have certain methods: LeftButton, RightButton, and Title. This pattern was inspired by the official React navigation bar example. The first method checks whether this is the initial route or not with the index == 0 condition. Alternatively, we can check for the name of the scene, such as name == 'Search'.

1 var NavigationBarRouteMapper = {
2   LeftButton(route, navigator, index, navState) {
3     if (index == 0) return null

If we pass the first statement, we are on the Forecast. Set the previous route (Search):

1     var previousRoute = navState.routeStack[index - 1]

Now, return the button, which is a TouchableOpacity component with Text in it. I use angle brackets with the previous route’s name as the button label, as shown in Figure 12. You can use Next or something else. This Navigator component is highly customizable. Most likely, you’d have some nicely designed images as well.

Figure 12: Navigator menu
Figure 12: Navigator menu
1     return (
2       <TouchableOpacity

The event handler uses the pop() method. Similar to Array.pop(), it removes the last element from a stack/array. The last element is the current screen, so we revert back to the previous route:

1         onPress={() => navigator.pop()}
2         style={styles.navBarLeftButton}>
3         <Text style={[styles.navBarText, styles.navBarButtonText ]}>
4           {'<'} {previousRoute.name}
5         </Text>
6       </TouchableOpacity>
7     )
8   },

We don’t need the right button in this project, but if you need it, you can implement it analogously to the left button. You might want to use a list of routes, such that you know which one is the next one based on the index of the current route.

1   RightButton(route, navigator, index, navState) {
2     return (
3       <View/>
4     )
5   },

The last method is straightforward. We render the name of the route as the title. You can use the title property instead of name if you wish; just don’t forget to update it everywhere (that is, in initialRoute, renderScene, and push() in Search).

1   Title(route, navigator, index, navState) {
2     return (
3       <Text style={[styles.navBarText, styles.navBarTitleText]}>
4         {route.name}
5       </Text>
6     )
7   }
8 }

Lastly, the styles! They are easy to read. One new addition is PixelRatio. It will give us the ratio of pixels so we can control the values on a lower level:

 1 var styles = StyleSheet.create({
 2   navBar: {
 3     backgroundColor: 'white',
 4     borderBottomWidth: 1 / PixelRatio.get(),
 5     borderBottomColor: '#CDCDCD'
 6   },
 7   navBarText: {
 8     fontSize: 16,
 9     marginVertical: 10,
10   },
11   navBarTitleText: {
12     color: 'blue',
13     fontWeight: '500',
14     marginVertical: 9,
15   },
16   navBarLeftButton: {
17     paddingLeft: 10,
18   },
19   navBarRightButton: {
20     paddingRight: 10,
21   },
22   navBarButtonText: {
23     color: 'black'
24   }
25 })

Change the weather component to App in the register call:

1 AppRegistry.registerComponent('weather', () => App)

We are done with one file, and we have two more to go. Moving in the logical sequence of the app flow, we continue with search.ios.js by importing the objects:

 1 'use strict'
 2 
 3 var React = require('react-native')
 4 const Forecast = require('./forecast.ios')
 5 
 6 var {
 7   StyleSheet,
 8   Text,
 9   TextInput,
10   View,
11   Switch,
12   TouchableHighlight,
13   ListView,
14   Alert
15 } = React

Next, we want to declare the OpenWeatherMap API key, which you can get from their website after registering as a developer. Pick the free plan unless you’re sure your app will hit the limits when it becomes number one on iTunes (or is it the App Store?). Refrain from using my keys, and get your own:

1 const openWeatherAppId = '2de143494c0b295cca9337e1e96b00e0', 
2   // This is Azat's key. Get your own!

In the event that OpenWeatherMap changes the response format or if you want to develop offline (as I do), keep the real URL commented and use the local version (weather-api.js Node.js server):

1   // openWeatherUrl = 'http://api.openweathermap.org/data/2.5/forecast' // Real \
2 API
3   openWeatherUrl = 'http://localhost:3000/' // Mock API, start with $ node weath\
4 er-api

Because this file is imported by index.ios.js, we need to export the needed component. You can create another variable/object, but I just assign the component to module.exports for eloquence:

1 module.exports = React.createClass({
2   getInitialState() {

When we get the initial state, we want to check if the city name was saved. If it was, then we’ll use that name and set isRemember to true, because the city name was remembered in the previous use:

1     this.props.storage.getFromStorage('cityName', (cityName) => {
2       if (cityName) this.setState({cityName: cityName, isRemember: true})
3     })

While we wait for the asynchronous callback with the city name to be executed by the storage API, we set the value to none:

1     return ({isRemember: false, cityName: ''})
2   },

Next, we handle the switch by setting the state of isRemember, because it’s a controlled component:

1   toggleRemember() {
2     console.log('toggle: ', this.state.isRemember)
3     this.setState({ isRemember: !this.state.isRemember}, ()=>{

If you remember from previous chapters (I know, it was so long ago!), setState() is actually asynchronous. We want to remove the city name if the Remember? toggle is off, so we need to implement removeItem() in the callback of setState(), and not just on the next line (we might have a race condition and the state will be old if we don’t use a callback):

1       if (!this.state.isRemember) this.props.storage.removeItem('cityName')
2     })
3   },

On every change of the city name TextInput, we update the state. This is the handler for onChangeText, so we get the value as an argument, not the event:

1   handleCityName(cityName) {
2     this.setState({ cityName: cityName})
3   },

The search() method is triggered by the Search button and the virtual keyboard’s “enter.” First, we define the states as local variables to eliminate unnecessary typing:

1   search(event) {
2     let cityName = this.state.cityName,
3       isRemember = this.state.isRemember

It’s good to check that the city name is not empty. There’s a cross-platform component Alert for that:

1     if (!cityName) return Alert.alert('No City Name',
2       'Please enter city name',
3       [{text: 'OK', onPress: () => console.log('OK Pressed!')}]
4     )

The most interesting piece of logic in the entire app is how we make the external call. The answer is easy. We’ll use the new fetch API, which is already part of Chrome. We don’t care about Chrome right now too much; all we need to know is that React Native supports it. In this example, I resorted to the ES6 string interpolation (a.k.a. string template) to construct the URL. If you’re using the local server, the response will be the same (response.json), so the URL doesn’t matter.

1     fetch(`${openWeatherUrl}/?appid=${openWeatherAppId}&q=${cityName}&units=metr\
2 ic`, {
3       method: 'GET'
4     }).then((response) => response.json())
5       .then((response) => {

Once we get the data, we want to store the city name. Maybe you want to do it before making the fetch call. It’s up to you.

1         if (isRemember) this.props.storage.setInStorage('cityName', cityName)

The ListView will render the grid, but it needs a special object data source. Create it like this:

1         let dataSource = new ListView.DataSource({
2           rowHasChanged: (row1, row2) => row1 !== row2
3         })

Everything is ready to render the forecast. Use the Navigator object by invoking push() and passing all the necessary properties:

1         this.props.navigator.push({
2           name: 'Forecast',
3           component: Forecast,

passProps is an arbitrary name. I followed the NavigatorIOS syntax here. You can pick another name. For the ListView, we populate the rows from the JavaScript/Node array with cloneWithRows():

 1           passProps: {
 2             forecastData: dataSource.cloneWithRows(response.list),
 3             forecastRaw: response
 4           }
 5         })
 6       })
 7       .catch((error) => {
 8         console.warn(error)
 9       })
10   },

We are done with the methods of Search. Now we can render the elements:

1   render: function() {
2     return (
3       <View style={styles.container}>
4         <Text style={styles.welcome}>
5           Welcome to Weather App, React Quickly project
6         </Text>
7         <Text style={styles.instructions}>
8           Enter your city name:
9         </Text>

The next element is a TextInput for the city name. It has two callbacks, onChangeText, which triggers handleCityName, and onEndEditing, which calls search:

1         <TextInput
2           placeholder="San Francisco"
3           value={this.state.cityName}
4           returnKeyType="search"
5           enablesReturnKeyAutomatically={true}
6           onChangeText={this.handleCityName}
7           onEndEditing={this.search} style={styles.textInput}/>

The last few elements are the label for the switch, the switch itself, and the Search button:

 1         <Text>Remember?</Text>
 2         <Switch onValueChange={this.toggleRemember} value={this.state.isRemember\
 3 }></Switch>
 4         <TouchableHighlight onPress={this.search}>
 5           <Text style={styles.button}>Search</Text>
 6         </TouchableHighlight>
 7       </View>
 8     )
 9   }
10 })

And of course the styles—without them, the layout and fonts will be all skewed. The properties are self-explanatory for the most part, so we won’t go into detail on them.

 1 var styles = StyleSheet.create({
 2   navigatorContainer: {
 3     flex: 1
 4   },
 5   container: {
 6     flex: 1,
 7     justifyContent: 'center',
 8     alignItems: 'center',
 9     backgroundColor: '#F5FCFF',
10   },
11   welcome: {
12     fontSize: 20,
13     textAlign: 'center',
14     margin: 10,
15   },
16   instructions: {
17     textAlign: 'center',
18     color: '#333333',
19     marginBottom: 5,
20   },
21   textInput: {
22     borderColor: '#8E8E93',
23     borderWidth: 0.5,
24     backgroundColor: '#fff',
25     height: 40,
26     marginLeft: 60,
27     marginRight: 60,
28     padding: 8,
29   },
30   button: {
31     color: '#111',
32     marginBottom: 15,
33     borderWidth: 1,
34     borderColor: 'blue',
35     padding: 10,
36   	borderRadius: 20,
37     fontWeight: '600',
38     marginTop: 30
39   }
40 })

So, we invoke the push() method from the Search component when we press Search. This will trigger an event in the Navigator element: namely renderScene, which renders the forecast. Let’s implement it. I promise, we are almost done!

The forecast.ios.js file starts with importations. By now, if this is unfamiliar to you, I am powerless.

 1 'use strict'
 2 
 3 var React = require('react-native')
 4 var {
 5   StyleSheet,
 6   Text,
 7   TextInput,
 8   View,
 9   ListView,
10   ScrollView
11 } = React

I wrote this function, mostly for Americans, to calculate F from C . It’s probably not very precise, but it’ll do for now:

1 const fToC = (f) => {
2   return Math.round((f - 31.996)*100/1.8)/100
3 }

The ForecastRow component is stateless (more on stateless components in chapter 10). Its sole purpose is to render a single forecast item:

1 const ForecastRow = (forecast)=> {
2   return (
3     <View style={styles.row}>
4       <View style={styles.rightContainer}>
5         <Text style={styles.subtitle}></Text>
6         <Text style={styles.subtitle}>

In the row, we output the date (dt_txt), description (rainy or sunny), and temperatures in C and F (figure 9-X). The latter is achieved by invoking the fToC function defined earlier in this file:

1           {forecast.dt_txt}: {forecast.weather[0].description}, {forecast.main.t\
2 emp}C/{fToC(forecast.main.temp)}F
3         </Text>
4        </View>
5     </View>
6   )
7 }

The result will look as shown in figure 9-X.

alt text needed
alt text needed

Next, we export the Forecast component, which is a ScrollView with Text and a ListView:

1 module.exports = React.createClass({
2   render: function() {
3     return (
4       <ScrollView style={styles.scroll}>
5         <Text style={styles.text}>{this.props.forecastRaw.city.name}</Text>

The ListView takes dataSource and renderRow properties to render the grid. The data source must be of a special type. It cannot be a plain JavaScript/Node array:

1         <ListView dataSource={this.props.forecastData} renderRow={ForecastRow} s\
2 tyle={styles.listView}/>
3       </ScrollView>
4     )
5   }
6 })

And the styles. Tadaah!

 1 var styles = StyleSheet.create({
 2   listView: {
 3     marginTop: 10,
 4   },
 5   row: {
 6     flex: 1,
 7     flexDirection: 'row',
 8     justifyContent: 'center',
 9     alignItems: 'center',
10     backgroundColor: '#5AC8FA',
11     paddingRight: 10,
12     paddingLeft: 10,
13     marginTop: 1
14   },
15   rightContainer: {
16     flex: 1
17   },
18   scroll: {
19     flex: 1,
20     padding: 5
21   },
22   text: {
23     marginTop: 80,
24     fontSize: 40
25   },
26   subtitle: {
27     fontSize: 16,
28     fontWeight: 'normal',
29     color: '#fff'
30   }
31 })

The last final touch is if you’re working offline and using a local URL. There are two files you need to have:

  1. response.json—Response to the real API call for London
  2. weather-api.js—Ultra-minimalistic Node web server that takes response.json and serves it to a client

Go ahead and copy response.json from GitHub. Then implement this Node.js server using only the core modules (I love Express or Swagger, but using them here is an overkill):

1 var http = require('http'),
2   forecastData = require('./response.json')
3 
4 http.createServer(function(request, response){
5   response.end(JSON.stringify(forecastData))
6 }).listen(3000)

Start the server with $ node weather-api, bundle the React Native code with $ react-native start, and reload the Simulator. The bundler and the server must be running together, so you might need to open a new tab or a window in your terminal app/iTerm.

Note: if you get an “Invariant Violation: Callback with id 1-5” error, make sure you don’t have the Chrome debugger opened more than once.

You should see an empty city name field. That’s okay, because this is the first time you’ve launched the app. I intentionally left the logs in the storage implementation. You should see the following when you open DevTools in the Chrome tab for debugging React Native (it typically opens automatically once you enable it by going to Hardware->Shake Gestures->Debug in Chrome—not that you are going to shake your laptop!):

1 AsyncStorage GET for cityName: "null"

Play with the toggle, enter a name (Figure 13), and get the weather report. The app is done. Boom! Now put some nice UI on it and ship it!

Figure 13: By using storage, developers can save city name
Figure 13: By using storage, developers can save city name