Chapter 4. Components for Mobile

In Chapter 3, we built a simple weather app. In doing so, we touched upon the basics of building interfaces with React Native. In this chapter, we will take a closer look at the mobile-based components used for React Native, and how they compare to basic HTML elements. Mobile interfaces are based on different primitive UI elements than web pages, and thus we need to use different components.

This chapter starts with a more detailed overview of the most basic components: <View>, <Image>, and <Text>. Then, we will discuss how touch and gestures factor into React Native components, and how to handle touch events. Next, we will cover higher-level components, such as the <ListView>, <TabView>, and <NavigatorView>, which allow you to combine other views into standard mobile interface patterns.

Analogies Between HTML Elements and Native Components

When developing for the Web, we make use of a variety of basic HTML elements. These include <div>, <span>, and <img>, as well as organizational elements such as <ol>, <ul>, and <table>. (We could include a consideration of elements such as <audio>, <svg>, <canvas>, and so on, but we’ll ignore them for now.)

When dealing with React Native, we don’t use these HTML elements, but we use a variety of components that are nearly analogous to them (Table 4-1).

Table 4-1. Analogous HTML and Native components
HTML React Native

div

View

img

Image

span, p

Text

ul/ol, li

ListView, child items

Although these elements serve roughly the same purposes, they are not interchangeable. Let’s take a look at how these components work on mobile with React Native, and how they differ from their browser-based counterparts.

The Text Component

Rendering text is a deceptively basic function; nearly any application will need to render text somewhere. However, text within the context of React Native and mobile development works differently from text rendering for the Web.

When working with text in HTML, you can include raw text strings in a variety of elements. Furthermore, you can style them with child tags such as <strong> and <em>. So, you might end up with an HTML snippet that looks like this:

<p>The quick <em>brown</em> fox jumped over the lazy <strong>dog</strong>.</p>

In React Native, only <Text> components may have plain text nodes as children. In other words, this is not valid:

<View>
  Text doesn't go here!
</View>

Instead, wrap your text in a <Text> component:

<View>
  <Text>This is OK!</Text>
</View>

When dealing with <Text> components in React Native, you no longer have access to subtags such as <strong> and <em>, though you can apply styles to achieve similar effects through use of attributes such as fontWeight and fontStyle. Here’s how you might achieve a similar effect by making use of inline styles:

<Text>
  The quick <Text style={{fontStyle: "italic"}}>brown</Text> fox
  jumped over the lazy <Text style={{fontWeight: "bold"}}>dog</Text>.
</Text>

This approach could quickly become verbose. You’ll likely want to create styled components as a sort of shorthand when dealing with text, as shown in Example 4-1.

Example 4-1. Creating reusable components for styling text
var styles = StyleSheet.create({
  bold: {
      fontWeight: "bold"
  },
  italic: {
      fontStyle: "italic"
  }
});

var Strong = React.createClass({
  render: function() {
    return (
    <Text style={styles.bold}>
      {this.props.children}
    </Text>);
  }
});

var Em = React.createClass({
  render: function() {
    return (
    <Text style={styles.italic}>
      {this.props.children}
    </Text>);
  }
});

Once you have declared these styled components, you can freely make use of styled nesting. Now the React Native version looks quite similar to the HTML version (see Example 4-2).

Example 4-2. Using styled components for rendering text
<Text>
  The quick <Em>brown</Em> fox jumped
  over the lazy <Strong>dog</Strong>.
</Text>

Similarly, React Native does not inherently have any concept of header elements (h1, h2, etc.), but it’s easy to declare your own styled <Text> elements and use them as needed.

In general, when dealing with styled text, React Native forces you to change your approach. Style inheritance is limited, so you lose the ability to have default font settings for all text nodes in the tree. One again, Facebook recommends solving this by using styled components:

You also lose the ability to set up a default font for an entire subtree. The recommended way to use consistent fonts and sizes across your application is to create a component MyAppText that includes them and use this component across your app. You can also use this component to make more specific components like MyAppHeaderText for other kinds of text.

React Native Documentation

The Text component documentation has more details on this.

You’ve probably noticed a pattern here: React Native is very opinionated in its preference for the reuse of styled components over the reuse of styles. We’ll discuss this further in the next chapter.

The Image Component

If text is the most basic element in an application, images are a close contender, for both mobile and for the Web. When writing HTML and CSS for the Web, we include images in a variety of ways: sometimes we use the <img> tag, while at other times we apply images via CSS, such as when we use the background-image property. In React Native, we have a similar <Image> component, but it behaves a little differently.

The basic usage of the <Image> component is straightforward; just set the source prop:

<Image source={require('image!puppies')} />

How does that require call work? Where does this resource live? Here’s one part of React Native that you’ll have to adjust based on which platform you’re targeting. On iOS, this means that you’ll need to import it into the assets folder within your Xcode project. By providing the appropriate @2x and @3x resolution files, you will enable Xcode to serve the correct asset file for the correct platform. This is a nice change from web development: the relatively limited possible combinations of screen size and resolution on iOS means that it’s easier to create targeted assets.

For React Native on other platforms, we can expect that the image! require syntax will point to a similar assets directory.

It’s worth mentioning that it is also possible to include web-based image sources instead of bundling your assets with your application. Facebook does this as one of the examples in the UIExplorer application:

<Image source={{uri: 'https://facebook.github.io/react/img/logo_og.png'}}
       style={{width: 400, height: 400}} />

When utilizing network resources, you will need to specify dimensions manually.

Downloading images via the network rather than including them as assets has some advantages. During development, for instance, it may be easier to use this approach while prototyping, rather than carefully importing all of your assets ahead of time. It also reduces the size of your bundled mobile application, so that users needn’t download all of your assets. However, it means that instead you’ll be relying on the user’s data plan whenever they access your application in the future. For most cases, you’ll want to avoid using the URI-based method.

If you’re wondering about working with the user’s own images, we’ll cover the camera roll in Chapter 6.

Because React Native emphasizes a component-based approach, images must be included as an <Image> component instead of being referenced via styles. For instance, in Chapter 3, we wanted to use an image as a background for our weather application. Whereas in plain HTML and CSS you would likely use the background-image property to apply a background image, in React Native you instead use the <Image> as a container component, like so:

<Image source={require('image!puppies')}>
  {/* Your content here... */}
</Image>

Styling the images themselves is fairly straightforward. In addition to applying styles, certain props control how the image will be rendered. You’ll often make use of the resizeMode prop, for instance, which can be set to resize, cover, or contain. The UIExplorer app demonstrates this well (Figure 4-1).

lnrn 0401
Figure 4-1. The difference between resize, cover, and contain

The <Image> component is easy to work with, and very flexible. You will likely make extensive use of it in your own applications.

Working with Touch and Gestures

Web-based interfaces are usually designed for mouse-based controllers. We use things like hover state to indicate interactivity and respond to user interaction. For mobile, it’s touch that matters. Mobile platforms have their own norms around interactions that you’ll want to design for. This varies somewhat from platform to platform: iOS behaves differently from Android, which behaves differently yet again from Windows Phone.

React Native provides a number of APIs for you to leverage as you build touch-ready interfaces. In this section, we’ll look at the <TouchableHighlight> container component, as well as the lower-level APIs provided by PanResponder and the Gesture Responder system.

Using TouchableHighlight

Any interface elements that respond to user touch (think buttons, control elements, etc.) should usually have a <TouchableHighlight> wrapper. <TouchableHighlight> causes an overlay to appear when the view is touched, giving the user visual feedback. This is one of the key interactions that causes a mobile application to feel native, as opposed to a mobile-optimized website, where touch feedback is limited. As a general rule of thumb, you should use <TouchableHighlight> anywhere there would be a button or a link on the Web.

At its most basic usage, you just need to wrap your component in a <TouchableHighlight>, which will add a simple overlay when pressed. The <TouchableHighlight> component also gives you hooks for events such as onPressIn, onPressOut, onLongPress, and the like, so you can use these events in your React applications.

Example 4-3 shows how you can wrap a component in a <TouchableHighlight> in order to give the user feedback.

Example 4-3. Using the <TouchableHighlight> component
<TouchableHighlight
  onPressIn={this._onPressIn}
  onPressOut={this._onPressOut}
  style={styles.touchable}>
    <View style={styles.button}>
      <Text style={styles.welcome}>
        {this.state.pressing ? 'EEK!' : 'PUSH ME'}
      </Text>
    </View>
</TouchableHighlight>

When the user taps the button, an overlay appears, and the text changes (Figure 4-2).

lnrn 0402
Figure 4-2. Using <TouchableHighlight> to give the user visual feedback—the unpressed state (left) and the pressed state, with highlight (right)

This is a contrived example, but it illustrates the basic interactions that make a button “feel” touchable on mobile. The overlay is a key piece of feedback that informs the user that an element can be pressed. Note that in order to apply the overlay, we don’t need to apply any logic to our styles; the <TouchableHighlight> handles the logic of that for us.

Example 4-4 shows the full code for this button component.

Example 4-4. Touch/PressDemo.js illustrates the use of <TouchableHighlight>
'use strict';

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

var Button = React.createClass({
  getInitialState: function() {
    return {
      pressing: false
    }
  },

  _onPressIn: function() {
    this.setState({pressing: true});
  },

  _onPressOut: function() {
    this.setState({pressing: false});
  },

  render: function() {
    return (
      <View style={styles.container}>
        <TouchableHighlight
          onPressIn={this._onPressIn}
          onPressOut={this._onPressOut}
          style={styles.touchable}>

          <View style={styles.button}>
            <Text style={styles.welcome}>
              {this.state.pressing ? 'EEK!' : 'PUSH ME'}
            </Text>
          </View>

        </TouchableHighlight>
      </View>
    );
  }
});

var styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: '#F5FCFF',
  },
  welcome: {
    fontSize: 20,
    textAlign: 'center',
    margin: 10,
    color: '#FFFFFF'
  },
  touchable: {
    borderRadius: 100
  },
  button: {
    backgroundColor: '#FF0000',
    borderRadius: 100,
    height: 200,
    width: 200,
    justifyContent: 'center'
  },
});

module.exports = Button;

Try editing this button to respond to other events, by using hooks like onPress and onLongPress. The best way to get a sense for how these events map onto user interactions is to experiment using a real device.

The GestureResponder System

What if you want to do more than just make things “tappable”? React Native also exposes two APIs for custom touch handling: GestureResponder and PanResponder. GestureResponder is a lower-level API, while PanResponder provides a useful abstraction. We’ll start by looking at how the GestureResponder system works, because it’s the basis for the PanResponder API.

Touch on mobile is fairly complicated. Most mobile platforms support multitouch, which means that there can be multiple touch points active on the screen at once. (Not all of these are necessarily fingers, either; think about the difficulty of, for example, detecting the user’s palm resting on the corner of the screen.) Additionally, there’s the issue of which view should handle a given touch. This problem is similar to how mouse events are processed on the Web, and the default behavior is also similar: the topmost child handles the touch event by default. With React Native’s gesture responder system, however, we can override this behavior if we so choose.

The touch responder is the view that handles a given touch event. In the previous section, we saw that the <TouchableHighlight> component acts as a touch responder. We can cause our own components to become the touch responder, too. The lifecycle by which this process is negotiated is a little complicated. A view that wishes to obtain touch responder status should implement four props:

  • View.props.onStartShouldSetResponder

  • View.props.onMoveShouldSetResponder

  • View.props.onResponderGrant

  • View.props.onResponderReject

These then get invoked according to the flow illustrated in Figure 4-3, in order to determine if the view will receive responder status.

lnrn 0403
Figure 4-3. Obtaining touch responder status

Yikes, that looks complicated! Let’s tease this apart. First, a touch event has three main lifecycle stages: start, move, and release (these correspond to mouseDown, mouseMove, and mouseUp in the browser). A view can request to be the touch responder during the start or the move phase. This behavior is specified by onStartShouldSetResponder and onMoveShouldSetResponder. When one of those functions returns true, the view attempts to claim responder status.

After a view has attempted to claim responder status, its attempt may be granted or rejected. The appropriate callback—either onResponderGrant or onResponderReject—will be invoked.

The responder negotiation functions are called in a bubbling pattern. If multiple views attempt to claim responder status, the deepest component will become the responder. This is typically the desired behavior; otherwise, you would have difficulty adding touchable components such as buttons to a larger view. If you want to override this behavior, parent components can make use of onStartShouldSetResponderCapture and onMoveShouldSetResponderCapture. Returning true from either of these will prevent a component’s children from becoming the touch responder.

After a view has successfully claimed touch responder status, its relevant event handlers may be called. Here’s the excerpt from the Gesture Responder documentation:

View.props.onResponderMove

The user is moving her finger

View.props.onResponderRelease

Fired at the end of the touch (i.e., “touchUp”)

View.props.onResponderTerminationRequest

Something else wants to become responder. Should this view release the responder? Returning true allows release

View.props.onResponderTerminate

The responder has been taken from the view. It might be taken by other views after a call to onResponderTerminationRequest, or by the OS without asking (happens with control center/notification center on iOS)

Most of the time, you will primarily be concerned with onResponderMove and onResponderRelease.

All of these methods receive a synthetic touch event object, which adheres to the following format (again, excerpted from the documentation):

changedTouches

Array of all touch events that have changed since the last event

identifier

The ID of the touch

locationX

The X position of the touch, relative to the element

locationY

The Y position of the touch, relative to the element

pageX

The X position of the touch, relative to the screen

pageY

The Y position of the touch, relative to the screen

target

The node id of the element receiving the touch event

timestamp

A time identifier for the touch, useful for velocity calculation

touches

Array of all current touches on the screen

You can make use of this information when deciding whether or not to respond to a touch event. Perhaps your view only cares about two-finger touches, for example.

This is a fairly low-level API; if you want to detect and respond to gestures in this way, you will need to spend a decent amount of time tuning the correct parameters and figuring out which values you should care about. In the next section, we will take a look at PanResponder, which supplies a somewhat higher-level interpretation of user gestures.

PanResponder

Unlike <TouchableHighlight>, PanResponder is not a component, but rather a class provided by React Native. It provides a slightly higher-level API than the basic events returned by the Gesture Responder system, while still providing access to those raw events. A PanResponder gestureState object gives you access to the following, in accordance with the PanResponder documentation:

stateID

ID of the gestureState (persisted as long as there at least one touch on screen)

moveX

The latest screen coordinates of the recently moved touch

moveY

The latest screen coordinates of the recently moved touch

x0

The screen coordinates of the responder grant

y0

The screen coordinates of the responder grant

dx

Accumulated distance of the gesture since the touch started

dy

Accumulated distance of the gesture since the touch started

vx

Current velocity of the gesture

vy

Current velocity of the gesture

numberActiveTouches

Number of touches currently on screeen

As you can see, in addition to raw position data, a gestureState object also includes information such as the current velocity of the touch and the accumulated distance.

To make use of PanResponder in a component, we need to create a PanResponder object and then attach it to a component in the render method.

Creating a PanResponder requires us to specify the proper handlers for PanResponder events (Example 4-5).

Example 4-5. Creating a PanResponder requires us to pass a bunch of callbacks
this._panResponder = PanResponder.create({
  onStartShouldSetPanResponder: this._handleStartShouldSetPanResponder,
  onMoveShouldSetPanResponder: this._handleMoveShouldSetPanResponder,
  onPanResponderGrant: this._handlePanResponderGrant,
  onPanResponderMove: this._handlePanResponderMove,
  onPanResponderRelease: this._handlePanResponderEnd,
  onPanResponderTerminate: this._handlePanResponderEnd,
});

Then, we use spread syntax to attach the PanResponder to the view in the component’s render method (Example 4-6).

Example 4-6. Attaching the PanResponder using spread sytax
render: function() {
  return (
    <View
      {...this._panResponder.panHandlers}>
      { /* View contents here */ }
    </View>
  );
}

After this, the handlers that you passed to the PanResponder.create call will be invoked during the appropriate move events, if the touch originates within this view.

Example 4-7 shows a modified version of the PanResponder example code provided by React Native. This version listens to touch events on the container view, as opposed to just the circle, and so that the values are printed to the screen as you interact with the application. If you plan on implementing your own gesture recognizers, I suggest experimenting with this application on a real device, so that you can get a feel for how these values respond. Figure 4-4 shows a screenshot of this example, but you’ll want to experience it on a device with a real touchscreen.

lnrn 0404
Figure 4-4. PanResponder demo
Example 4-7. Touch/PanDemo.js illustrates the use of PanResponder
// Adapted from
// https://github.com/facebook/react-native/blob/master/
// Examples/UIExplorer/PanResponderExample.js

'use strict';

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

var CIRCLE_SIZE = 40;
var CIRCLE_COLOR = 'blue';
var CIRCLE_HIGHLIGHT_COLOR = 'green';

var PanResponderExample = React.createClass({

  // Set some initial values.
  _panResponder: {},
  _previousLeft: 0,
  _previousTop: 0,
  _circleStyles: {},
  circle: null,

  getInitialState: function() {
    return {
      numberActiveTouches: 0,
      moveX: 0,
      moveY: 0,
      x0: 0,
      y0: 0,
      dx: 0,
      dy: 0,
      vx: 0,
      vy: 0,
    }
  },

  componentWillMount: function() {
    this._panResponder = PanResponder.create({
      onStartShouldSetPanResponder: this._handleStartShouldSetPanResponder,
      onMoveShouldSetPanResponder: this._handleMoveShouldSetPanResponder,
      onPanResponderGrant: this._handlePanResponderGrant,
      onPanResponderMove: this._handlePanResponderMove,
      onPanResponderRelease: this._handlePanResponderEnd,
      onPanResponderTerminate: this._handlePanResponderEnd,
    });
    this._previousLeft = 20;
    this._previousTop = 84;
    this._circleStyles = {
      left: this._previousLeft,
      top: this._previousTop,
    };
  },

  componentDidMount: function() {
    this._updatePosition();
  },

  render: function() {
    return (
      <View style={styles.container}>
        <View
          ref={(circle) => {
            this.circle = circle;
          }}
          style={styles.circle}
          {...this._panResponder.panHandlers}/>
        <Text>
          {this.state.numberActiveTouches} touches,
          dx: {this.state.dx},
          dy: {this.state.dy},
          vx: {this.state.vx},
          vy: {this.state.vy}
        </Text>
      </View>
    );
  },

  // _highlight and _unHighlight get called by PanResponder methods,
  // providing visual feedback to the user.
  _highlight: function() {
    this.circle && this.circle.setNativeProps({
      backgroundColor: CIRCLE_HIGHLIGHT_COLOR
    });
  },

  _unHighlight: function() {
    this.circle && this.circle.setNativeProps({
      backgroundColor: CIRCLE_COLOR
    });
  },

  // We're controlling the circle's position directly with setNativeProps.
  _updatePosition: function() {
    this.circle && this.circle.setNativeProps(this._circleStyles);
  },

  _handleStartShouldSetPanResponder:
 function(e: Object, gestureState: Object): boolean {
    // Should we become active when the user presses down on the circle?
    return true;
  },

  _handleMoveShouldSetPanResponder:
  function(e: Object, gestureState: Object): boolean {
    // Should we become active when the user moves a touch over the circle?
    return true;
  },

  _handlePanResponderGrant: function(e: Object, gestureState: Object) {
    this._highlight();
  },

  _handlePanResponderMove: function(e: Object, gestureState: Object) {
    this.setState({
      stateID: gestureState.stateID,
      moveX: gestureState.moveX,
      moveY: gestureState.moveY,
      x0: gestureState.x0,
      y0: gestureState.y0,
      dx: gestureState.dx,
      dy: gestureState.dy,
      vx: gestureState.vx,
      vy: gestureState.vy,
      numberActiveTouches: gestureState.numberActiveTouches
    });

    // Calculate current position using deltas
    this._circleStyles.left = this._previousLeft + gestureState.dx;
    this._circleStyles.top = this._previousTop + gestureState.dy;
    this._updatePosition();
  },
  _handlePanResponderEnd: function(e: Object, gestureState: Object) {
    this._unHighlight();
    this._previousLeft += gestureState.dx;
    this._previousTop += gestureState.dy;
  },
});

var styles = StyleSheet.create({
  circle: {
    width: CIRCLE_SIZE,
    height: CIRCLE_SIZE,
    borderRadius: CIRCLE_SIZE / 2,
    backgroundColor: CIRCLE_COLOR,
    position: 'absolute',
    left: 0,
    top: 0,
  },
  container: {
    flex: 1,
    paddingTop: 64,
  },
});

module.exports = PanResponderExample;

Choosing how to handle touch

How should you decide when to use the touch and gesture APIs discussed in this section? It depends on what you want to build.

In order to provide the user with basic feedback, and indicate that a button or another element is “tappable,” use the <TouchableHighlight> component.

In order to implement your own custom touch interfaces, use either the raw Gesture Responder system, or a PanResponder. Chances are that you will almost always prefer the PanResponder approach, because it also gives you access to the simpler touch events provided by the Gesture Responder system. If you are designing a game, or an application with an unusual interface, you’ll need to spend some time building out the interactions you want by using these APIs.

For many applications, you won’t need to implement any custom touch handling with either the Gesture Responder system or the PanResponder. In the next section, we’ll look at some of the higher-level components that implement common UI patterns for you.

Working with Organizational Components

In this section, we’re going to look at organizational components that you can use to control general flow within your application. This includes the <TabView>, <NavigatorView>, and <ListView>, which all implement some of the most common mobile interaction and navigational patterns. Once you have planned out your application’s navigational flow, you’ll find that these components are very helpful in making your application a reality.

Using ListView

Let’s start by using the <ListView> component. In this section, we are going to build an app that displays the New York Times Best Seller List and lets us view data about each book, as shown in Figure 4-5. If you’d like, you can grab your own API token from the New York Times. Otherwise, use the API token included in the sample code.

lnrn 0405
Figure 4-5. The BookList application we’ll be building

Lists are extremely useful for mobile development, and you will notice that many mobile user interfaces feature them as a central element. A <ListView> is literally just a list of views, optionally with special views for section dividers, headers, or footers. For example, you can see this interaction pattern in the Dropbox, Twitter, and iOS Settings apps (Figure 4-6).

lnrn 0406
Figure 4-6. Lists as used by Dropbox, Twitter, and the iOS Settings app

<ListView>s are a good example of where React Native shines, because it can leverage its host platform. On mobile, the native <ListView> element is usually highly optimized so that rendering is smooth and stutter-free. If you expect to render a very large number of items in your <ListView>, you should try to keep the child views relatively simple, to try and reduce stutter.

The basic React Native <ListView> component requires two props: dataSource and renderRow. dataSource is, as the name implies, a source of information about the data that needs to be rendered. renderRow should return a component based on the data from one element of the dataSource.

This basic usage is demonstrated in SimpleList.js. We’ll start by adding a dataSource to our <SimpleList> component. A ListView.DataSource needs to implement the rowHasChanged method. Here’s a simple example:

var ds = new ListView.DataSource({rowHasChanged: (r1, r2) => r1 !== r2});

To set the actual contents of a dataSource, we use cloneWithRows. Let’s return the dataSource in our getInitialState call:

getInitialState: function() {
  var ds = new ListView.DataSource({rowHasChanged: (r1, r2) => r1 !== r2});
  return {
    dataSource: ds.cloneWithRows(['a', 'b', 'c', 'a longer example', 'd', 'e'])
  };
}

The other prop we need is renderRow, which should be a function that returns some JSX based on the data for a given row:

_renderRow: function(rowData) {
  return <Text style={styles.row}>{rowData}</Text>;
}

Now we can put it all together to see a simple <ListView>, by rendering a <ListView> like so:

<ListView
  dataSource={this.state.dataSource}
  renderRow={this._renderRow}
  />

It looks like Figure 4-7.

lnrn 0407
Figure 4-7. The SimpleList component renders a barebones <ListView>

What if we want to do a little more? Let’s create a <ListView> with more complex data. We will be using the New York Times API to create a simple Best Sellers application, which renders the New York Times Best Seller list.

First, we initialize our data source to be empty, because we’ll need to fetch the data:

getInitialState: function() {
  var ds = new ListView.DataSource({rowHasChanged: (r1, r2) => r1 !== r2});
  return {
    dataSource: ds.cloneWithRows([])
  };
}

Then, we add a method for fetching data, and update the data source once we have it. This method will get called from componentDidMount:

_refreshData: function() {
  var endpoint =
  'http://api.nytimes.com/svc/books/v3/lists/hardcover-fiction?response-format
  =json&api-key=' + API_KEY;
  fetch(endpoint)
    .then((response) => response.json())
    .then((rjson) => {
      this.setState({
        dataSource: this.state.dataSource.cloneWithRows(rjson.results.books)
      });
    });
}

Each book returned by the New York Times API has three properties: coverURL, author, and title. We update the <ListView>’s render function to return a component based on those props.

Example 4-8. For _renderRow, we just pass along the relevant data to the <BookItem>
  _renderRow: function(rowData) {
    return <BookItem coverURL={rowData.book_image}
                     title={rowData.title}
                    author={rowData.author}/>;
  },

We’ll also toss in a header and footer component, to demonstrate how these work (Example 4-9). Note that for a <ListView>, the header and footer are not sticky; they scroll with the rest of the list. If you want a sticky header or footer, it’s probably easiest to render them separately from the <ListView> component.

All together, the Best Sellers application consists of two files: BookListV2.js and BookItem.js. BookListV2.js is shown in Example 4-10. (BookList.js is a simpler file that omits fetching data from an API, and is included in the GitHub repository for your reference.)

Example 4-10. Bestsellers/BookListV2.js
'use strict';

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

var BookItem = require('./BookItem');
var API_KEY = '73b19491b83909c7e07016f4bb4644f9:2:60667290';
var QUERY_TYPE = 'hardcover-fiction';
var API_STEM = 'http://api.nytimes.com/svc/books/v3/lists'
var ENDPOINT = `${API_STEM}/${QUERY_TYPE}?response-format=json&api-key=${API_KEY}`;

var BookList = React.createClass({
  getInitialState: function() {
    var ds = new ListView.DataSource({rowHasChanged: (r1, r2) => r1 !== r2});
    return {
      dataSource: ds.cloneWithRows([])
    };
  },

  componentDidMount: function() {
    this._refreshData();
  },

  _renderRow: function(rowData) {
    return <BookItem coverURL={rowData.book_image}
    title={rowData.title}
    author={rowData.author}/>;
  },

  _renderHeader: function() {
    return (<View style={styles.sectionDivider}>
      <Text style={styles.headingText}>
        Bestsellers in Hardcover Fiction
      </Text>
      </View>);
  },

  _renderFooter: function() {
    return(
      <View style={styles.sectionDivider}>
        <Text>Data from the New York Times Best Seller list.</Text>
      </View>
      );
  },

  _refreshData: function() {
    fetch(ENDPOINT)
      .then((response) => response.json())
      .then((rjson) => {
        this.setState({
          dataSource: this.state.dataSource.cloneWithRows(rjson.results.books)
        });
      });
  },

  render: function() {
    return (
        <ListView
          style=
          dataSource={this.state.dataSource}
          renderRow={this._renderRow}
          renderHeader={this._renderHeader}
          renderFooter={this._renderFooter}
          />
    );
  }
});

var styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: '#FFFFFF',
    paddingTop: 24
  },
  list: {
    flex: 1,
    flexDirection: 'row'
  },
  listContent: {
    flex: 1,
    flexDirection: 'column'
  },
  row: {
    flex: 1,
    fontSize: 24,
    padding: 42,
    borderWidth: 1,
    borderColor: '#DDDDDD'
  },
  sectionDivider: {
    padding: 8,
    backgroundColor: '#EEEEEE',
    alignItems: 'center'
  },
  headingText: {
    flex: 1,
    fontSize: 24,
    alignSelf: 'center'
  }
});

module.exports = BookList;

The <BookItem> is a simple component that handles rendering each child view in the list (Example 4-11).

Example 4-11. Bestsellers/BookItem.js
'use strict';

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


var styles = StyleSheet.create({
  bookItem: {
    flex: 1,
    flexDirection: 'row',
    backgroundColor: '#FFFFFF',
    borderBottomColor: '#AAAAAA',
    borderBottomWidth: 2,
    padding: 5
  },
  cover: {
    flex: 1,
    height: 150,
    resizeMode: 'contain'
  },
  info: {
    flex: 3,
    alignItems: 'flex-end',
    flexDirection: 'column',
    alignSelf: 'center',
    padding: 20
  },
  author: {
    fontSize: 18
  },
  title: {
    fontSize: 18,
    fontWeight: 'bold'
  }
});

var BookItem = React.createClass({
  propTypes: {
    coverURL: React.PropTypes.string.isRequired,
    author: React.PropTypes.string.isRequired,
    title: React.PropTypes.string.isRequired
  },

  render: function() {
    return (
      <View style={styles.bookItem}>
        <Image style={styles.cover} source=/>
        <View style={styles.info}>
          <Text style={styles.author}>{this.props.author}</Text>
          <Text style={styles.title}>{this.props.title}</Text>
        </View>
      </View>
      );
  }
});

module.exports = BookItem;

If you have complex data, or very long lists, you will need to pay attention to the performance optimizations enabled by some of <ListView>’s more complex, optional properties. For most uses, however, this will suffice.

Using Navigators

The <ListView> is a good example of combining multiple views together into a more usable interaction. On a higher level, we can use components such as the <Navigator> to present different screens of an app, much as we might have various pages on a website.

The <Navigator> is a subtle but important component, and is used in many common applications. For instance, the iOS Settings app could be implemented as a combination of <Navigator> with many <ListView> components (Figure 4-8). The Dropbox app also makes use of a Navigator.

lnrn 0408
Figure 4-8. The iOS Settings app is a good example of Navigator behavior

A <Navigator> allows your application to transition between different screens (often referred to as “scenes”), while maintaining a “stack” of routes, so that you can push, pop, or replace states. You can think of this as analogous to the history API on the Web. A “route” is the title of a screen, coupled with an index.

For instance, in the Settings app, initially the stack is empty. When you select one of the submenus, the initial scene is pushed onto the stack. Tapping “back,” in the top-left corner of the screen, will pop it back off.

If you’re interested in how this plays out, the UIExplorer app has a good demo of the various ways of using the Navigator API.

Note that there are actually two Navigator options: the cross-platform <Navigator> component and the <NavigatorIOS> component. In this book, we’ll be opting to use the <Navigator>.

Other Organizational Components

There are plenty of other organizational components, too. For example, a few useful ones include <TabBarIOS> and <SegmentedControlIOS> (illustrated in Figure 4-9) and <DrawerLayoutAndroid> and <ToolbarAndroid> (illustrated in Figure 4-10).

You’ll notice that these are all named with platform-specific suffixes. That’s because they wrap native APIs for platform-specific UI elements.

lnrn 0409
Figure 4-9. An iOS segmented control (top), and an iOS tab bar (bottom)
lnrn 0410
Figure 4-10. An Android toolbar (left), and an Android drawer (right)

These components are very useful for organizing multiple screens within your application. <TabBarIOS> and <DrawerLayoutAndroid>, for example, give you an easy way to switch between multiple modes or functions. <SegmentedControlIOS> and <ToolbarAndroid> are better suited for more fine-grained controls.

You’ll want to refer to the platform-specific design guidelines for how best to use these components:

But wait! How do we make use of platform-specific components? Let’s now take a look at how to handle platform-specific components in cross-platform applications.

Platform-Specific Components

Not all components are available on all platforms, and not all interaction patterns are appropriate for all devices. That doesn’t mean that you can’t use platform-specific code in your application, though! In this section, we’ll cover platform-specific components, as well as strategies for how to incorporate them in your cross-platform applications.

Tip

Writing cross-platform code in React Native is not an all-or-nothing endeavor! You can mix cross-platform and platform-specific code in your application, as we’ll do in this section.

iOS- or Android-Only Components

Some components are only available on a specific platform. This includes things like <TabBarIOS> or <SwitchAndroid>. They’re usually platform-specific because they wrap some kind of underlying platform-specific API. For some components, having a platform-agnostic version doesn’t make sense. For instance, the <ToolbarAndroid> component exposes an Android-specific API for a view type that doesn’t exist on iOS anyway.

Platform-specific components are named with an appropriate suffix: either IOS or Android. If you try to include one on the wrong platform, your application will crash.

Components can also have platform-specific props. These are tagged in the documentation with a small badge indicating their usage. For instance, <TextInput> has some props that are platform-agnostic, and others that are specific to iOS or Android (Figure 4-11).

lnrn 0411
Figure 4-11. <TextInput> has Android and iOS-specific props

Components with Platform-Specific Versions

So, how do you handle platform-specific components or props in a cross-platform application? The good news is that you can still use these components. Remember how our app has both an index.ios.js and an index.android.js file? This naming convention can be used for any file, to create a component that has different implementations on Android and iOS.

As an example, we’ll use the <SwitchIOS> and <SwitchAndroid> components. They expose slightly different APIs, but what if we just want to use a simple switch? Let’s create a wrapper component, <Switch>, which renders the appropriate platform-specific component.

We’ll start by implementing switch.ios.js (Example 4-12). It’s a very simple wrapper around <SwitchIOS>, and allows us to provide a callback for when the switch value changes.

Example 4-12. Switch.ios.js
var React = require('react-native');
var { SwitchIOS } = React;

var Switch = React.createClass({
  getInitialState() {
    return {value: false};
  },

  _onValueChange(value) {
    this.setState({value: value});
    if (this.props.onValueChange) {
      this.props.onValueChange(value);
    }
  },

  render() {
    return (
      <SwitchIOS
        onValueChange={this._onValueChange}
        value={this.state.value}/>
      );
  }
});

module.exports = Switch;

Next, let’s implement switch.android.js (Example 4-13).

Example 4-13. Switch.android.js
var React = require('react-native');
var { SwitchAndroid } = React;

var Switch = React.createClass({
  getInitialState() {
    return {value: false};
  },

  _onValueChange(value) {
    this.setState({value: value});
    if (this.props.onValueChange) {
      this.props.onValueChange(value);
    }
  },

  render() {
    return (
      <SwitchAndroid
        onValueChange={this._onValueChange}
        value={this.state.value}/>
      );
  }
});

module.exports = Switch;

Note that it looks almost identical to switch.ios.js, and it implements the same API. The only difference is that it uses <SwitchAndroid> internally instead of <SwitchIOS>.

We can now import our <Switch> component from another file with the syntax:

var Switch = require('./switch');
...
var switchComp = <Switch onValueChange={(val) => {console.log(val); }}/>;

Let’s actually use the <Switch> component. Create a new file, CrossPlatform.js, and include the code shown in Example 4-14. We’ll have the background color change based on the current value of a <Switch>.

Example 4-14. CrossPlatform.js makes use of the <Switch> component
var React = require('react-native');
var {
  StyleSheet,
  Text,
  View,
} = React;
var Switch = require('./switch');

var CrossPlatform = React.createClass({
  getInitialState() {
    return {val: false};
  },

  _onValueChange(val) {
    this.setState({val: val});
  },

  render: function() {
    var colorClass = this.state.val ? styles.blueContainer : styles.redContainer;
    return (
      <View style={[styles.container, colorClass]}>
        <Text style={styles.welcome}>
          Make me blue!
        </Text>
        <Switch onValueChange={this._onValueChange}/>
      </View>
    );
  }
});

var styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
  },
  blueContainer: {
    backgroundColor: '#5555FF'
  },
  redContainer: {
    backgroundColor: '#FF5555'
  },
  welcome: {
    fontSize: 20,
    textAlign: 'center',
    margin: 10,
  }
});

module.exports = CrossPlatform;

Note that there’s no switch.js file, but we can call require(./switch). The React Native packager will automatically select the correct implementation based on our platform, and use either switch.ios.js or switch.android.js as appropriate.

Finally, replace the contents of index.android.js and index.ios.js so that we can render the <CrossPlatform> component.

Example 4-15. The index.ios.js and index.android.js files should be identical, and simply import the crossplatform.js file
var React = require('react-native');
var { AppRegistry } = React;
var CrossPlatform = require('./crossplatform');

AppRegistry.registerComponent('PlatformSpecific', () => CrossPlatform);

Now we can run our application on both iOS and Android (Figure 4-12).

lnrn 0412
Figure 4-12. The CrossPlatform application should render on both iOS and Android, using the appropriate <Switch> component

When to Use Platform-Specific Components

When is it appropriate to use a platform-specific component? In most cases, you’ll want to do so when there’s a platform-specific interaction pattern that you want your application to adhere to. If you want your application to feel truly “native,” it’s worth paying attention to platform-specific UI norms.

Apple and Google both provide human interface guidelines for their platforms, which are worth consulting:

By creating platform-specific versions of only certain components, you can strike a balance between code reuse and platform-based customization. In most cases, you should only need separate implementations of a handful of components in order to support both iOS and Android.

Summary

In this chapter, we dug into the specifics of a variety of the most important components in React Native. We discussed how to utilize basic low-level components, like <Text> and <Image>, as well as higher-order components like <ListView>, <Navigator>, and <TabBarIOS>. We also took a look at how to use various touch-focused APIs and components, in case you want to build your own custom touch handlers. Finally, we saw how to use platform-specific components in our applications.

At this point, you should be equipped to build basic, functional applications using React Native! Now that you’ve acquainted yourself with the components discussed in this chapter, building upon them and combining them to create your own applications should feel remarkably similar to working with React on the Web.

Of course, building up basic, functioning applications is only part of the battle. In the next chapter, we’ll focus on styling, and how to use React Native’s implementation of styles to get the look and feel you want on mobile.

Get Learning React Native now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.