Going native with React, an introduction

11 May 2015
React Native has moved on a lot since this article - some methods may not work
{
  // Versions
  "react-native": "0.4.0"
}

As a JavaScript developer, I would’ve never thought that I could make native mobile applications easily in JavaScript. Sure, we have tools such as PhoneGap, but wrapping our browser based application in native application leaves a lot to be desired.

It’s all changed now - the React team over at Facebook have released React Native. This not only allows us to use the React framework to create applications that utilise native mobile components, but it does that all in real time - meaning that we don’t have to recompile our applications as we develop on them - this makes it incredibly easy to create mobile apps! Luckily I got to preview React Native in its beta and even since then it’s progressed massively.

Please note that currently iOS is supported. You will therefore need to be running OS X with XCode in order to follow this tutorial.

If you haven’t had a chance to learn React, take a look at my tutorial to get started with it.

It’s important to note that this doesn’t mean we can write code once and use it everywhere. Trying to do that can be disastrous as the level of abstraction would be insane. Instead, React Native allows us to learn once, write everywhere.

Going back to 2004

If you have watched The Social Network you will remember FaceMash, the application that kicked off Facebook. For those who haven’t watched, 11 years ago (wow) Mark Zuckerberg created FaceMash, an application where you could view two people and rate which one is hotter. Each person would have a score (although the original algorithm is unknown, the movie shows the Elo rating algorithm being used) that reflected how “hot” they are.

Here it is in all it’s glory -

Let’s go full circle - we’re going to be recreating FaceMash in React Native. If you think it’s immoral to rate girls on their looks, feel free to change the pictures to something else that you find attractive (dogs, snippets of code, etc, I’m not judging you).

Creating our app

You can clone the start repo here if you wish. This isn’t necessary, however there different branches of the code at different stages just in case you get lost!

Houston, we have lift off

If you haven’t cloned the repo, we need to setup our base project. React Native allows us to quickly start up a project with the react-native-cli npm package CLI. If you don’t have it installed, quickly run

npm install -g react-native-cli

And then we can get started.

Navigate to a folder in the terminal and run

react-native init FaceMash

This will create our base application ready for us to dig in and add to.

Open it up

Launch XCode and navigate to the directory you created the application in. From here, we will need to open up facemash.xcodeproj.

React Native supports us working in both the iOS Simulator and our actual iOS devices.

I will be developing in the iOS Simulator as it allows more rapid application development - we can press Command + R to refresh the application when we make changes to our JavaScript, or we can be super lazy and enable live reloading via the developer menu (accessible via Command + Control + Z). We can even debug our code in the Chrome developer tools.

If you wish to develop the application using your iOS device, you will need to be on the same network as your computer. By default, React Native will look at localhost for the JavaScript so you will need to point it to your computer.

We can do this by editing the AppDelegate.m file and changing localhost to our local IP. You can find this on OS X by Alt + Click-ing the wireless menu.

We can now run our application. The application will open in whatever target you’ve picked in XCode. When we click run, a terminal process will also spawn running npm start in the directory of our application. If you don’t wish to run the app through XCode, make sure you run npm start. This will create a local web server on port 8081 which points to our compiled JavaScript code and also watch and recompile our code on save.

I’m running the application in a simulated iPhone 6, running at a 50% scale.

There we go, we’ve got a blank canvas with so much room for activities!

Taking a peek

Let’s take a look at the code it takes to render what we can see in the screenshot above. Open up index.ios.js.

index.ios.js
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
/**
 * Sample React Native App
 * https://github.com/facebook/react-native
 */
'use strict';

var React = require('react-native');
var {
  AppRegistry,
  StyleSheet,
  Text,
  View,
} = React;

var facemash = React.createClass({
  render: function() {
    return (
      <View style={styles.container}>
        <Text style={styles.welcome}>
          Welcome to React Native!
        </Text>
        <Text style={styles.instructions}>
          To get started, edit index.ios.js
        </Text>
        <Text style={styles.instructions}>
          Press Cmd+R to reload,{'\n'}
          Cmd+Control+Z for dev menu
        </Text>
      </View>
    );
  }
});

var styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: '#F5FCFF',
  },
  welcome: {
    fontSize: 20,
    textAlign: 'center',
    margin: 10,
  },
  instructions: {
    textAlign: 'center',
    color: '#333333',
    marginBottom: 5,
  },
});

AppRegistry.registerComponent('facemash', () => facemash);

You can close your mouth now - yes, that is all it takes to render our application. Looks familiar, doesn’t it?

React Native vs the browser

Not all of React Native matches what you would’ve been using with React in the browser. However, the differences between the two are so minuscule there’s no need to worry about them.

  • Instead of using block elements such as div or section, we use the View component in React Native. This maps over to the native iOS component UIView.
  • All text must be wrapped inside the Text component
  • No stylesheets - all your styling is written as JavaScript objects
  • We don’t need to worry about what is browser compatibility - ES6 harmony is supported out of the box as well as flexbox

Get to work

We’re going to start by clearing out the stylesheet and render function of our React component. Ideally, to get a basic feeling of React Native, we’re going to try and use as many different components as we can.

Let’s start off with the TabBarIOS component. You might recognise the TabBar component, used in some of the core iOS apps such as Clock and Photos.

index.ios.js
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
var React = require('react-native');
var {
  AppRegistry,
  StyleSheet,
  Text,
  View,
  TabBarIOS
} = React;

var facemash = React.createClass({
  getInitialState() {
    return {
      selectedTab: 'faceMash'
    }
  },
  render: function() {
    return (
      <TabBarIOS>
        <TabBarIOS.Item
          title="FaceMash"
          icon={ require('image!facemash') }
          selected={ this.state.selectedTab === 'faceMash' }>
          <View style={ styles.pageView }>
            <Text>Face Mash</Text>
          </View>
        </TabBarIOS.Item>
        <TabBarIOS.Item
          title="Messages"
          icon={ require('image!messages') }
          selected={ this.state.selectedTab === 'messages' }>
          <View style={ styles.pageView }>
            <Text>Messages</Text>
          </View>
        </TabBarIOS.Item>
        <TabBarIOS.Item
          title="Settings"
          icon={ require('image!settings') }
          selected={ this.state.selectedTab === 'settings' }>
          <View style={ styles.pageView }>
            <Text>Settings</Text>
          </View>
        </TabBarIOS.Item>
      </TabBarIOS>
    );
  }
});

var styles = StyleSheet.create({
  pageView: {
    backgroundColor: '#fff',
    flex: 1
  }
});

// omitted code

Look at that! You’ll notice that the text is currently over the status bar, but don’t worry, we’ll fix that later on.

The TabBarIOS component uses TabBarIOS.Item for each of its children. We’re going to have three pages - the page where you rate people, a messages list and a settings page.

The TabBarIOS.Item must have one child. This will be the contents of the page that has been selected (you can see us setting selected to true/false depending on the state of the component).

Obviously, a tab bar wouldn’t look good without icons. There are a few system icons you can use, however if you use them the text of the tab also changes to match the icon. Instead, we can use our own icons. To import a local image asset in React Native, you can use require and prepend the asset name with image!.

I’m using icons that are available free, under the CC 3.0 licence, from flaticon.

Using Static Images

To add a static image for React Native, open up XCode. In the Project Navigator (the first icon on the left hand pane), open up Images.xcassets. All your images will sit here.

This allows us to keep all our assets under one name, whilst providing different images for both each resolution, and even device specific images.

Images must follow a strict naming convention. Use the asset name (such as messages or settings) and append it with the resolution it should be displayed at. For instance, I’m building my application for an iPhone 6 so I would use the @2x resolution.

Once you’ve named your image file correctly, drag and drop it into the left hand page in Images.xcassets.

You can then use require('image!assetname') in React Native!

Back to the code

The next logical step is to set up our main component to allow switching between tabs. We can do this by just setting the state when the user clicks on it. TabBarIOS.Item allows us to give it an onPress property, which we can use to detect when a user taps on the tab.

index.ios.js
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
// omitted code

var facemash = React.createClass({
  getInitialState() {
    return {
      selectedTab: 'faceMash'
    }
  },
  changeTab(tabName) {
    this.setState({
      selectedTab: tabName
    });
  },
  render: function() {
    return (
      <TabBarIOS>
        <TabBarIOS.Item
          title="FaceMash"
          icon={ require('image!facemash') }
          onPress={ () => this.changeTab('faceMash') }
          selected={ this.state.selectedTab === 'faceMash' }>
          <View style={ styles.pageView }>
            <Text>Face Mash</Text>
          </View>
        </TabBarIOS.Item>
        <TabBarIOS.Item
          title="Messages"
          icon={ require('image!messages') }
          onPress={ () => this.changeTab('messages') }
          selected={ this.state.selectedTab === 'messages' }>
          <View style={ styles.pageView }>
            <Text>Messages</Text>
          </View>
        </TabBarIOS.Item>
        <TabBarIOS.Item
          title="Settings"
          icon={ require('image!settings') }
          onPress={ () => this.changeTab('settings') }
          selected={ this.state.selectedTab === 'settings' }>
          <View style={ styles.pageView }>
            <Text>Settings</Text>
          </View>
        </TabBarIOS.Item>
      </TabBarIOS>
    );
  }
});

// omitted code

Done! It’s that simple. Refresh your app by pressing Command + R in the iOS simulator (or recompile it via XCode if you’re developing on your device) and you’ll notice we can now tap between tabs, and the screen changes!

Although we haven’t written much code, you can check out the step-one branch, which also contains all the icons we’ve used for the tabs.

Let’s Mash

Let’s work on the FaceMash tab. We’re going to hit an endpoint to load our data using fetch. In the step-one branch, I’ve included a config.yaml file in the rest/ folder that we’re going to use to mock our endpoints using stubby. All the users in the endpoint/pictures were randomly generated at randomuser.me.

Open up your terminal and run

stubby -d rest/config.yaml

And let’s get started!

Create a new file in the folder named tabs/ called FaceMash.js and stick a basic React component in there -

tabs FaceMash.js
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
'use strict';

var React = require('react-native');
var {
  StyleSheet,
  Text,
  View
  } = React;

var facemashTab = React.createClass({
  render: function() {
    return (
      <View style={ styles.container }>
        <Text>
          FaceMash tab!
        </Text>
      </View>
    );
  }
});

var styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff'
  }
});

module.exports = facemashTab;

All we’ve got on this tab so far is a basic View component with some text inside of it. We’ve also got some basic styling for the View, to make sure that it takes up all available height/width. We’re going to add a header, purely for display purposes.

tabs FaceMash.js
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// omitted code

var facemashTab = React.createClass({
  render: function() {
    return (
      <View style={ styles.container }>
        <View style={ styles.header }>
        </View>
        <View style={ styles.content }>
          <Text>
            FaceMash tab!
          </Text>
        </View>
      </View>
    );
  }
});

var styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff'
  },
  header: {
    height: 40,
    background: '#ff0000'
  }
});

module.exports = facemashTab;

Now we’re talking. However, this looks terrible as the status bar is black. Not to worry, we can change that using the StatusBarIOS API. When the changeTab function is called, we can check if the tab is the FaceMash tab. If it is, we’ll set the status bar style to 1 (white), if not we’ll set it to 0 (black).

index.ios.js
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// omitted code

var {
  AppRegistry,
  StyleSheet,
  Text,
  View,
  TabBarIOS,
  StatusBarIOS
} = React;

var facemash = React.createClass({
  ...,
  changeTab(tabName) {
  StatusBarIOS.setStyle(tabName === 'faceMash' ? 1 : 0);
    this.setState({
      selectedTab: tabName
    });
  },
  ...
});

// omitted code

Refresh and you’ll see a white status bar - sorted!

We can now get to hitting the endpoint and displaying our users. We’ll be using fetch, which is included for us in React Native by default.

tabs FaceMash.js
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
// omitted code

var facemashTab = React.createClass({
  getInitialState: function() {
    return {
      list: [],
      currentIndex: 0
    };
  },
  componentWillMount: function() {
    fetch('http://localhost:8882/rest/mash')
      .then(res => res.json())
      .then(res => this.setState({ list: res }));
  },
  render: function() {
    return (
      ...
    );
  }
});

// omitted code

The request will populate our state with the returned data. As the initial state is an empty array, we can check that in our render function and show the user a loading page whilst they wait.

tabs FaceMash.js
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
var {
  StyleSheet,
  Text,
  View,
  ActivityIndicatorIOS
  } = React;

var facemashTab = React.createClass({
  ...,
  render: function() {
    var contents;
    if (!this.state.list.length) {
      contents = (
        <View style={ styles.loading }>
          <Text style={ styles.loadingText }>Loading</Text>
          <ActivityIndicatorIOS />
        </View>
      )
    } else {
      contents = (
        <View style={ styles.content }>
          <Text>Loaded</Text>
        </View>
      )
    }

    return (
      <View style={ styles.container }>
        <View style={ styles.header }>
          <Text style={ styles.headerText }>FaceMash</Text>
        </View>
        { contents }
      </View>
    );
  }
});

var styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff'
  },
  loading: {
    flex: 1,
    backgroundColor: '#fff',
    justifyContent: 'center',
    alignItems: 'center'
  },
  loadingText: {
    fontSize: 14,
    marginBottom: 20
  },
  header: {
    height: 50,
    backgroundColor: '#760004',
    paddingTop: 20,
    alignItems: 'center'
  },
  headerText: {
    color: '#fff',
    fontSize: 20,
    fontWeight: 'bold'
  }
});

We will now have access to our data in this.state.list. We’ve also got what current index of that array we’re at in the state as the endpoint returns an array of objects - each object being two people that the user can rate.

As there are two people to pick from, both of which have the same data associated with them, we’re going to make a React component that will display their data.

tabs FaceMash.js
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
// omitted code

var Person = React.createClass({
  render: function() {
    var person = this.props.person;

    return (
      <View style={ styles.person }>
        <Text>Person!</Text>
      </View>
    )
  }
});

var facemashTab = React.createClass({
  getInitialState: function() {
    return {
      list: [],
      currentIndex: 0
    };
  },
  componentWillMount: function() {
    fetch('http://localhost:8882/rest/mash')
      .then(res => res.json())
      .then(res => this.setState({ list: res }));
  },
  render: function() {
    var contents;
    if (!this.state.list.length) {
      contents = (
        <View style={ styles.loading }>
          <Text style={ styles.loadingText }>Loading</Text>
          <ActivityIndicatorIOS />
        </View>
      )
    } else {
      var { list, currentIndex } = this.state;

      var record = list[currentIndex];

      var people = record.users.map(person => <Person person={ person } />);

      contents = (
        <View style={ styles.content }>
          { people }
        </View>
      )
    }

    return (
      <View style={ styles.container }>
        <View style={ styles.header }>
          <Text style={ styles.headerText }>FaceMash</Text>
        </View>
        { contents }
      </View>
    );
  }
});

var styles = StyleSheet.create({
  ...,
  person: {
    flex: 1,
    margin: 10,
    borderRadius: 3,
    overflow: 'hidden'
  }
});

We’ve now got a component that mounts twice (once for each person), and the appropriate configuration passed through to it. We can now display the profile picture and the relevant user info.

Displaying external images

Unlike our tab icons, the images we are displaying for each user come from an external source. This isn’t a problem, in fact it’s even easier to display them than displaying static assets.

Instead of passing a require‘d image through to the Image component, we just pass through an object instead. This object will have one property - uri, which points to the image we want to load.

As we’ve passed through the user information as a property named person, we can access the image URL via this.props.person.picture.

tabs FaceMash.js
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// omitted code

var Person = React.createClass({
  render: function() {
    var person = this.props.person;

    return (
      <View style={ styles.person }>
        <Image style={ styles.personImage } source={ { uri: person.picture } } />
      </View>
    )
  }
});

// omitted code

var styles = StyleSheet.create({
  ...
  person: {
    flex: 1,
    margin: 10,
    borderRadius: 3,
    overflow: 'hidden'
  },
  personImage: {
    flex: 1,
    height: 200
  },
  ...
});

module.exports = facemashTab;

There is also some styling needed - resizing images is incredibly easy. Similar CSS properties such as background-size can be applied to images in React Native, but we’re just going to put a height on it, and the image will be resized accordingly.

We can now add the rest of the user information.

tabs FaceMash.js
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
// omitted code

var Person = React.createClass({
  render: function() {
    var person = this.props.person;

    return (
      <View style={ styles.person }>
        <Image style={ styles.personImage } source={ { uri: person.picture } } />
        <View style={ styles.personInfo }>
          <Text style={ styles.personName }>
            { person.firstName } { person.lastName }
          </Text>
          <View style={ styles.personScore }>
            <Text style={ styles.personScoreHeader }>
              WON
            </Text>
            <Text style={ [styles.personScoreValue, styles.won] }>
              { person.won }
            </Text>
          </View>
          <View style={ styles.personScore }>
            <Text style={ styles.personScoreHeader }>
              LOST
            </Text>
            <Text style={ [styles.personScoreValue, styles.lost] }>
              { person.lost }
            </Text>
          </View>
          <View style={ styles.personScore }>
            <Text style={ styles.personScoreHeader }>
              SCORE
            </Text>
            <Text style={ styles.personScoreValue }>
              { person.score }
            </Text>
          </View>
        </View>
      </View>
    )
  }
});

// omitted code

var styles = StyleSheet.create({
  ...,
  person: {
    flex: 1,
    margin: 10,
    borderRadius: 3,
    overflow: 'hidden'
  },
  personInfo: {
    borderLeftColor: 'rgba( 0, 0, 0, 0.1 )',
    borderLeftWidth: 1,
    borderRightColor: 'rgba( 0, 0, 0, 0.1 )',
    borderRightWidth: 1,
    borderBottomColor: 'rgba( 0, 0, 0, 0.1 )',
    borderBottomWidth: 1,
    padding: 10,
    alignItems: 'center',
    flexDirection: 'row'
  },
  personImage: {
    flex: 1,
    height: 200
  },
  personName: {
    fontSize: 18,
    flex: 1,
    paddingLeft: 5
  },
  personScore: {
    flex: 0.25,
    alignItems: 'center'
  },
  personScoreHeader: {
    color: 'rgba( 0, 0, 0, 0.3 )',
    fontSize: 10,
    fontWeight: 'bold'
  },
  personScoreValue: {
    color: 'rgba( 0, 0, 0, 0.6 )',
    fontSize: 16
  },
  won: {
    color: '#93C26D'
  },
  lost: {
    color: '#DD4B39'
  }
});

module.exports = facemashTab;

You can check out the code up to here on the step-two branch.

Now we’ve got the users displaying, we can add our tab events that allow the user to pick who is hottest.

Tap tap

React Native offers us the TouchableHighlight component. This allows our View components to respond properly to touches. When it is touched, the opacity of the wrapped view is decreased. This makes our components “feel” touchable.

We’re going to wrap the person information section with this. In the future, we might want to make it so the user can tap the photo to see more pictures of that person.

tabs FaceMash.js
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// omitted code

var Person = React.createClass({
  render: function() {
    var person = this.props.person;

    return (
      <View style={ styles.person }>
        <Image style={ styles.personImage } source={ { uri: person.picture } } />
        <TouchableHighlight>
          <View style={ styles.personInfo }>
            <Text style={ styles.personName }>
              { person.firstName } { person.lastName }
            </Text>
            <View style={ styles.personScore }>
              <Text style={ styles.personScoreHeader }>
                WON
              </Text>
              <Text style={ [styles.personScoreValue, styles.won] }>
                { person.won }
              </Text>
            </View>
            <View style={ styles.personScore }>
              <Text style={ styles.personScoreHeader }>
                LOST
              </Text>
              <Text style={ [styles.personScoreValue, styles.lost] }>
                { person.lost }
              </Text>
            </View>
            <View style={ styles.personScore }>
              <Text style={ styles.personScoreHeader }>
                SCORE
              </Text>
              <Text style={ styles.personScoreValue }>
                { person.score }
              </Text>
            </View>
          </View>
        </TouchableHighlight>
      </View>
    )
  }
});

// omitted code

If your reload our changes and tap on the user information, you’ll notice it works - but it looks horrible. This is because we haven’t set a background colour on the view, meaning the whole component is darkened.

tabs FaceMash.js
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// omitted code

var styles = StyleSheet.create({
  ...,
  person: {
    flex: 1,
    margin: 10,
    borderRadius: 3,
    overflow: 'hidden'
  },
  personInfo: {
    backgroundColor: '#fff',
    borderLeftColor: 'rgba( 0, 0, 0, 0.1 )',
    borderLeftWidth: 1,
    borderRightColor: 'rgba( 0, 0, 0, 0.1 )',
    borderRightWidth: 1,
    borderBottomColor: 'rgba( 0, 0, 0, 0.1 )',
    borderBottomWidth: 1,
    padding: 10,
    alignItems: 'center',
    flexDirection: 'row'
  },
  ...
});

// omitted code

If you tap the info box now, it’ll work correctly!

TouchableHighlight offers us the same events that TouchableWithoutFeedback has. TouchableWithoutFeedback shouldn’t be used, as everything that can be tapped should provide some sort of visual feedback.

This means we can use onPress - which is called when the user has released the tap, but as long as it hasn’t been interrupted (e.g. moving their finger off of the tappable area).

We need to pass a property down to our Person component for when it is tapped on.

tabs FaceMash.js
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
'use strict';

var React = require('react-native');
var {
  StyleSheet,
  Text,
  View,
  Image,
  ActivityIndicatorIOS,
  TouchableHighlight
  } = React;

var Person = React.createClass({
  render: function() {
    var person = this.props.person;

    return (
      <View style={ styles.person }>
        <Image style={ styles.personImage } source={ { uri: person.picture } } />
        <TouchableHighlight onPress={ this.props.onPress }>
          ...
        </TouchableHighlight>
      </View>
    )
  }
});

var facemashTab = React.createClass({
  ...,
  onPersonPress: function() {
    this.setState({
      currentIndex: this.state.currentIndex + 1
    });
  },
  ...,
  render: function() {
    var contents;
    if (!this.state.list.length) {
      contents = (
        <View style={ styles.loading }>
          <Text style={ styles.loadingText }>Loading</Text>
          <ActivityIndicatorIOS />
        </View>
      )
    } else {
      var { list, currentIndex } = this.state;

      var record = list[currentIndex];

      var people = record.users.map(person => <Person person={ person } onPress={ this.onPersonPress } />);

      contents = (
        <View style={ styles.content }>
          { people }
        </View>
      )
    }

    return (
      <View style={ styles.container }>
        <View style={ styles.header }>
          <Text style={ styles.headerText }>FaceMash</Text>
        </View>
        { contents }
      </View>
    );
  }
});

// omitted code

As you can see, there is now a onPersonPress property in our main tab component. We then pass this down to our Person components, and they then call it when the TouchableHighlight area has been tapped. We can then increment our index, and the view will rerender with the new set of people.

This is the final changes to the facemash tab. If you wish to go further with it, here are some ideas

  • Hit a REST endpoint when a user has been picked
  • Check if the end of the list has been reached, and display a message
  • Allow the user to click on the photos and see more

You can check out the final code for our facemash tab on the step-three branch.

Messages

We can now move onto the messages tab. This will function a bit like iMessage - a scrollable list of users, and clicking on one will navigate us to the chat view for that user.

Luckily, React Native gives us the ListView component. This allows us to have a simple, scrollable list (using ScrollView) as well as efficiently displaying the list (only re-renders changed rows and limits row rendering to one per event loop).

To use a ListView, we need to have a data source. A data source allows us to have a custom function to check whether or not a row has changed (think of it similar to shouldComponentUpdate) and we can then put our JSON data into it. The data source lives in our state object.

Create a new file in the tabs/ folder called Messages.js.

tabs Messages.js
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
'use strict';

var React = require('react-native');
var {
  StyleSheet,
  Text,
  View,
  Image
  } = React;

var messagesTab = React.createClass({
  render: function() {
    return (
      <View style={ styles.container }>
        <Text>Messages!</Text>
      </View>
    );
  }
});

var styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff'
  }
});

module.exports = messagesTab;

You’ll also need to edit index.ios.js to point to the newly created component -

index.ios.js
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// omitted code

var MessagesTab = require('./tabs/Messages');

var facemash = React.createClass({
  getInitialState() {
    return {
      selectedTab: 'faceMash'
    }
  },
  changeTab(tabName) {
    StatusBarIOS.setStyle(tabName === 'faceMash' ? 1 : 0);
    this.setState({
      selectedTab: tabName
    });
  },
  render: function() {
    return (
      <TabBarIOS>
        ...
        <TabBarIOS.Item
          title="Messages"
          icon={ require('image!messages') }
          onPress={ () => this.changeTab('messages') }
          selected={ this.state.selectedTab === 'messages' }>
          <MessagesTab />
        </TabBarIOS.Item>
        ...
      </TabBarIOS>
    );
  }
});

// omitted code

As I mentioned above, we need a data source for our list view. This is accessible via ListView.DataSource. We’ll initialise one in our getInitialState.

tabs Messages.js
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
'use strict';

var React = require('react-native');
var {
  StyleSheet,
  Text,
  View,
  Image,
  ListView
  } = React;

var messagesTab = React.createClass({
  getInitialState: function() {
    return {
      dataSource: new ListView.DataSource({
        rowHasChanged: (r1, r2) => r1 !== r2
      })
    };
  },
  render: function(){
    return (
      <View style={ styles.container }>
        <Text>Messages!</Text>
      </View>
    );
  }
});

var styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff'
  }
});

module.exports = messagesTab;

Now we’ve got our data source, we need to fetch some data from the server to know who we’ve been messaging. I’ve included in an endpoint configuration for stubby in the step-three branch.

tabs Messages.js
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// omitted code

var messagesTab = React.createClass({
  componentWillMount: function() {
    fetch('http://localhost:8882/rest/messages')
      .then(res => res.json())
      .then(res => this.updateDataSource(res));
  },
  getInitialState: function() {
    return {
      dataSource: new ListView.DataSource({
        rowHasChanged: (r1, r2) => r1 !== r2
      })
    };
  },
  updateDataSource: function(data){
    this.setState({
      dataSource: this.state.dataSource.cloneWithRows(data)
    })
  },
  render: function(){
    return (
      <View style={ styles.container }>
        <Text>Messages!</Text>
      </View>
    );
  }
});

// omitted code

This will fetch our data and then update our data source with it. As this is all handled in the state, whenever our data source gets updated so will our view!

We can now move on to our ListView component. At a minimum, the component needs two properties - dataSource (which we already have) and renderRow. renderRow is a function that needs to return a React element. It’s called once for every row in the data source, and is passed the appropriate data for that row as an argument.

tabs Messages.js
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// omitted code

var messagesTab = React.createClass({
  ...,
  renderRow: function (){
    return (
      <View>
        <Text>Row goes here!</Text>
      </View>
    );
  },
  render: function(){
    return (
      <View style={ styles.container }>
        <ListView dataSource={ this.state.dataSource } renderRow={ this.renderRow } />
      </View>
    );
  }
});

// omitted code

We can now get to displaying the correct data for each row. The object for each item is passed through as the first parameter to our renderRow function.

tabs Messages.js
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// omitted code

var messagesTab = React.createClass({
  ...,
  renderRow: function (person){
    return (
      <View>
        <Text>{ person.user.firstName } { person.user.lastName }</Text>
      </View>
    );
  },
  render: function(){
    return (
      <View style={ styles.container }>
        <ListView dataSource={ this.state.dataSource } renderRow={ this.renderRow } />
      </View>
    );
  }
});

// omitted code

We can now go ahead and add the other information in there, such as the picture and the last received message.

tabs Messages.js
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
'use strict';

var React = require('react-native');
var {
  StyleSheet,
  Text,
  View,
  Image,
  ListView,
  PixelRatio
  } = React;

function prettyTime(timestamp) {
  var createdDate = new Date(timestamp);
  var distance = Math.round( ( +new Date() - timestamp ) / 60000 );

  var hours = ('0' + createdDate.getHours()).slice(-2);
  var minutes = ('0' + createdDate.getMinutes()).slice(-2);
  var month = ('0' + (createdDate.getMonth() + 1)).slice(-2);
  var date = ('0' + createdDate.getDate()).slice(-2);
  var year = createdDate.getFullYear();

  var string;
  if (distance < 1440) {
    string = [hours, minutes].join(':');
  } else if (distance < 2879) {
    string = 'Yesterday';
  } else {
    string = [date, month, year].join('/');
  }

  return string;
}

var messagesTab = React.createClass({
  ...,
  renderRow: function (person){
    var time = prettyTime(person.lastMessage.timestamp);
    return (
      <View>
        <View style={ styles.row }>
          <Image
            source={ { uri: person.user.picture } }
            style={ styles.cellImage }
            />
          <View style={ styles.textContainer }>
            <Text style={ styles.name } numberOfLines={ 1 }>
              { person.user.firstName } { person.user.lastName }
            </Text>
            <Text style={ styles.time } numberOfLines={ 1 }>
              { time }
            </Text>
            <Text style={ styles.lastMessage } numberOfLines={ 1 }>
              { person.lastMessage.contents }
            </Text>
          </View>
        </View>
        <View style={ styles.cellBorder } />
      </View>
    );
  },
  render: function(){
    return (
      <View style={ styles.container }>
        <ListView dataSource={ this.state.dataSource } renderRow={ this.renderRow } />
      </View>
    );
  }
});

var styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff'
  },
  row: {
    flex: 1,
    alignItems: 'center',
    backgroundColor: 'white',
    flexDirection: 'row',
    padding: 10
  },
  textContainer: {
    flex: 1
  },
  cellImage: {
    height: 60,
    borderRadius: 30,
    marginRight: 10,
    width: 60
  },
  time: {
    position: 'absolute',
    top: 0,
    right: 0,
    fontSize: 12,
    color: '#cccccc'
  },
  name: {
    flex: 1,
    fontSize: 16,
    fontWeight: 'bold',
    marginBottom: 2
  },
  lastMessage: {
    color: '#999999',
    fontSize: 12
  },
  cellBorder: {
    backgroundColor: 'rgba(0, 0, 0, 0.1)',
    height: 1 / PixelRatio.get(),
    marginLeft: 4
  }
});

module.exports = messagesTab;

Looking good!

You’ll notice in our stylesheet we’re using something called PixelRatio. This is so we can get the thinnest line that can be possibly displayed on the device used. Normally, we would use 1px for the thinnest border, but there is no concept of px in React Native.

We can now add the code to deal with tapping on a user. We’re going to make use of the NavigatorIOS component - you’ll recognise it in apps such as iMessage and Notes. It allows us to have back-swipe functionality between views, as well as a navigation bar at the top.

We’re actually going to make a new React component to house the navigator. This is because the component needs an initial React component to render.

We’ll change our messagesTab component to be called messageList, and create another component called messagesTab

tabs Messages.js
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
'use strict';

var React = require('react-native');
var {
  StyleSheet,
  Text,
  View,
  Image,
  ListView,
  PixelRatio,
  NavigatorIOS
  } = React;

// omitted code

var messageList = React.createClass({
  ...,
  render: function(){
    return (
      <View style={ >
        <ListView dataSource={ this.state.dataSource } renderRow={ this.renderRow } />
      </View>
    );
  }
});

var messagesTab = React.createClass({
  render: function() {
    return (
      <NavigatorIOS
        style={ styles.container }
        initialRoute={
          {
            title: 'Messages',
            component: messageList
          }
        }
        />
    );
  }
});

// omitted code

Looking much more professional already. Like we did in the facemash tab, we can now add TouchableHighlight to our rows.

tabs Messages.js
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
'use strict';

var React = require('react-native');
var {
  StyleSheet,
  Text,
  View,
  Image,
  ListView,
  PixelRatio,
  NavigatorIOS,
  TouchableHighlight
  } = React;

// omitted code

var messageList = React.createClass({
  componentWillMount: function() {
    fetch('http://localhost:8882/rest/messages')
      .then(res => res.json())
      .then(res => this.updateDataSource(res));
  },
  getInitialState: function() {
    return {
      dataSource: new ListView.DataSource({
        rowHasChanged: (r1, r2) => r1 !== r2
      })
    };
  },
  updateDataSource: function(data){
    this.setState({
      dataSource: this.state.dataSource.cloneWithRows(data)
    })
  },
  renderRow: function (person){
    var time = prettyTime(person.lastMessage.timestamp);
    return (
      <View>
        <TouchableHighlight>
          <View style={ styles.row }>
            <Image
              source={ { uri: person.user.picture } }
              style={ styles.cellImage }
              />
            <View style={ styles.textContainer }>
              <Text style={ styles.name } numberOfLines={ 1 }>
                { person.user.firstName } { person.user.lastName }
              </Text>
              <Text style={ styles.time } numberOfLines={ 1 }>
                { time }
              </Text>
              <Text style={ styles.lastMessage } numberOfLines={ 1 }>
                { person.lastMessage.contents }
              </Text>
            </View>
          </View>
          <View style={ styles.cellBorder } />
        </TouchableHighlight>
      </View>
    );
  },
  render: function(){
    return (
      <View style={ styles.container }>
        <ListView dataSource={ this.state.dataSource } renderRow={ this.renderRow } />
      </View>
    );
  }
});

// omitted code

Reload the changes and.. you’ll get an error. This is because we are passing two child components through to TouchableHighlight, and it only plays nicely with one. Not to worry, we can just wrap the two components in another View component.

tabs Messages.js
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// omitted code

var messageList = React.createClass({
  ...,
  renderRow: function (person){
    var time = prettyTime(person.lastMessage.timestamp);
    return (
      <View>
        <TouchableHighlight>
          <View>
            <View style={ styles.row }>
              <Image
                source={ { uri: person.user.picture } }
                style={ styles.cellImage }
                />
              <View style={ styles.textContainer }>
                <Text style={ styles.name } numberOfLines={ 1 }>
                  { person.user.firstName } { person.user.lastName }
                </Text>
                <Text style={ styles.time } numberOfLines={ 1 }>
                  { time }
                </Text>
                <Text style={ styles.lastMessage } numberOfLines={ 1 }>
                  { person.lastMessage.contents }
                </Text>
              </View>
            </View>
            <View style={ styles.cellBorder } />
          </View>
        </TouchableHighlight>
      </View>
    );
  },
  ...
});

// omitted code

Now, when we tap on the row, we get the expected behaviour. Wait a minute - our bottom border is now looking a bit weird! This is because we’re using an rgba value. The background of the whole view is being changed, which means our border is then changed to be darker. Not to worry, we can give it a hex code instead.

tabs Messages.js
1
2
3
4
5
6
7
8
var styles = StyleSheet.create({
  ...,
  cellBorder: {
    backgroundColor: '#F2F2F2',
    height: 1 / PixelRatio.get(),
    marginLeft: 4
  }
});

You can see all the code up to this point on the step-four branch.

Pushing and popping

Now we’ve got our main list styled, we can now handle the navigation change when the user is tapped.

NavigatorIOS allows us to “push” components into it when we want to change the current route. To do this, we need some way to access the NavigatorIOS instance in our child component, messageList. Luckily for us, this gets passed down as a prop named navigator.

Let’s add an onPress event to our TouchableHighlight component.

tabs Messages.js
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// omitted code

var messageList = React.createClass({
  ...,
  openChat: function (user){

  },
  renderRow: function (person){
    var time = prettyTime(person.lastMessage.timestamp);
    return (
      <View>
        <TouchableHighlight onPress={ this.openChat.bind(this, person.user) }>
          ...
        </TouchableHighlight>
      </View>
    );
  },
  ...
});

// omitted code

We now need a React component to pass through to our navigator. Create a new file called MessageView.js in our tabs/ folder.

tabs MessageView.js
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
'use strict';

var React = require('react-native');
var {
  StyleSheet,
  Text,
  View
  } = React;

var messageView = React.createClass({
  render: function(){
    return (
      <View style={ styles.container }>
        <Text>Message view!</Text>
      </View>
    );
  }
});

var styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff'
  },
});

module.exports = messageView;

We can then include this in our Messages.js file, and push it through to the navigator.

tabs Messages.js
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// omitted code

var messageList = React.createClass({
  ...,
  openChat: function (user){
    this.props.navigator.push({
      title: `${user.firstName} ${user.lastName}`,
      component: MessageView,
      passProps: { user }
    });
  },
  renderRow: function (person){
    var time = prettyTime(person.lastMessage.timestamp);
    return (
      <View>
        <TouchableHighlight onPress={ this.openChat.bind(this, person.user) }>
          <View>
            <View style={ styles.row }>
              <Image
                source={ { uri: person.user.picture } }
                style={ styles.cellImage }
                />
              <View style={ styles.textContainer }>
                <Text style={ styles.name } numberOfLines={ 1 }>
                  { person.user.firstName } { person.user.lastName }
                </Text>
                <Text style={ styles.time } numberOfLines={ 1 }>
                  { time }
                </Text>
                <Text style={ styles.lastMessage } numberOfLines={ 1 }>
                  { person.lastMessage.contents }
                </Text>
              </View>
            </View>
            <View style={ styles.cellBorder } />
          </View>
        </TouchableHighlight>
      </View>
    );
  },
  ...
});

// omitted code

You can see here we’re pushing the next route into our navigator. From here we can set the next route’s title (the user’s first and last name), what component to render (our newly created MessageView) and what properties to pass through. This allows us to access anything we need in the MessageView component (we’re going to pass through the user object).

This will work for everyone in our chat list, no matter how big or small.

However, the text is being cut off by our new title bar. To solve this, we can just put some padding on our first View component.

Now we’ve got a user property being passed through, we can also display that!

tabs MessageView.js
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
'use strict';

var React = require('react-native');
var {
  StyleSheet,
  Text,
  View
  } = React;

var messageView = React.createClass({
  render: function(){
    var user = this.props.user;
    return (
      <View style={ styles.container }>
        <Text>Chat with { user.firstName } { user.lastName }</Text>
      </View>
    );
  }
});

var styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff',
    paddingTop: 64
  },
});

module.exports = messageView;

You can checkout the code up to here on the step-five branch.

If you wish to take this tab further, here are a few ideas

  • Get the chat information for that user when MessageView mounts
  • Try and implement pull down to refresh - hint: use the renderHeader property on ListView
  • Add a “Settings” button to the right of the user’s first/last name

Conclusion

Feel free to try and add some functionality to the settings tab. Try and make use of other components, such as DatePickerIOS and TextInput for some common settings (DOB, name, etc).

Hopefully this has hit on quite a few different components that you will be commonly using in your React Native application. If you have any queries, feel free to contact me on twitter @rynclark.