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 tab bars, navigators, and lists, 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

<FlatList>, 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 by using 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
const styles = StyleSheet.create({
  bold: {
      fontWeight: "bold"
  },
  italic: {
      fontStyle: "italic"
  }
});

class Strong extends Component {
  render() {
    return (
    <Text style={styles.bold}>
      {this.props.children}
    </Text>);
  }
}

class Em extends Component {
  render() {
    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. Once again, the React Native documentation 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.

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 reusing styled components over inheriting or reusing styles. While it can be time-consuming initially, this approach leads to better isolation so that you can render a component anywhere in your application and get the same result. This in turn makes it easier to maintain the styling code in your application. We’ll discuss this approach 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 the web. When writing HTML and CSS for the web, we include images in a variety of ways: sometimes we use the <img> tag whereas 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("./puppies.png")} />

The image path is resolved exactly as JavaScript modules are resolved. So, in the preceding example, puppies.png should be provided in the same folder as the component that requires it.

There’s some filename magic going on here, too. If you provide puppies.ios.png and puppies.android.png, the appropriate file will be rendered on each platform. Similarly, if you provide images with suffixes @2x and @3x, the React Native packager will select the appropriate image for the device’s screen density.

It is also possible to include web-based image sources instead of bundling your assets with your application. For example:

<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 do not need to download all of your assets. However, it means that instead you’ll be relying on their 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 <Image> components 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("./puppies.png")}>
  {/* Your content here... */}
</Image>

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

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

The <Image> component is 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 humble <Button> component and the <TouchableHighlight> container component, as well as lower-level APIs that give you direct access to touch events.

Creating Basic Interactions with <Button>

If you’re just getting started and need a basic, interactive button, the default <Button> component has you covered. It provides a simple API, which allows you to set the color, label text, and callback function.

<Button
  onPress={this._onPress}
  title="Press me"
  color="#841584"
  accessibilityLabel="Press this button"
/>

This <Button> component is a decent starting point, but you’ll probably want to create your own interactive components. For that, we’ll need to use <TouchableHighlight>.

Using the <TouchableHighlight> Component

Any interface elements that respond to user touch (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}
  accessibilityLabel={'PUSH ME'}
  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).

lrn2 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. PressDemo.js illustrates the use of <TouchableHighlight>
import React, { Component } from "react";
import { StyleSheet, Text, View, TouchableHighlight } from "react-native";

class Button extends Component {
  constructor(props) {
    super(props);
    this.state = { pressing: false };
  }

  _onPressIn = () => {
    this.setState({ pressing: true });
  };

  _onPressOut = () => {
    this.setState({ pressing: false });
  };

  render() {
    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>
    );
  }
}

const 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"
  }
});

export default 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.

Using the PanResponder Class

Unlike <TouchableHighlight>, PanResponder is not a component but rather a class provided by React Native. A PanResponder gestureState object gives you access to raw position data as well as information such as velocity and accumulated distance of the current gesture.

To make use of PanResponder in a React component, we need to create a PanResponder object and then attach it to a component’s 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 register several callbacks
this._panResponder = PanResponder.create({
  onStartShouldSetPanResponder: this._handleStartShouldSetPanResponder,
  onMoveShouldSetPanResponder: this._handleMoveShouldSetPanResponder,
  onPanResponderGrant: this._handlePanResponderGrant,
  onPanResponderMove: this._handlePanResponderMove,
  onPanResponderRelease: this._handlePanResponderEnd,
  onPanResponderTerminate: this._handlePanResponderEnd,
});

These six functions give us access to the full lifecycle of a touch event. onStartShouldSetPanResponder and onMoveShouldSetPanResponder determine whether or not we should respond to a given touch event. onPanResponderGrant will be invoked when a touch event begins, and onPanResponderRelease and onPanResponderTerminate will be invoked when a touch event ends. We’ll be able to access data about the ongoing touch event during onPanResponderMove.

We use spread syntax to attach the PanResponder to the view in our 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.

Figure 4-3 renders a circle that you can drag around the screen. Its coordinates will be updated as you move it.

lrn2 0403
Figure 4-3. PanResponder demo

In order to implement this, let’s flesh out our PanResponder callbacks now. The first two are straightforward: by implementing _handleStartShouldSetPanResponder and _handleMoveShouldSetPanResponder, we can declare that we want this responder to receive touch events (Example 4-7).

Example 4-7. For the first two callbacks, we simply return true
_handleStartShouldSetPanResponder = (event, gestureState) => {
  // Should we become active when the user presses down on the circle?
  return true;
};

_handleMoveShouldSetPanResponder = (event, gestureState) => {
  // Should we become active when the user moves a touch over the circle?
  return true;
};

Then we’ll want to use the location data in _handlePanResponderMove to update the coordinates of our circle view (Example 4-8).

Example 4-8. Updating the circle’s position in _handlePanResponderMove
_handlePanResponderMove = (event, gestureState) => {
  // Calculate current position using deltas
  this._circleStyles.style.left = this._previousLeft + gestureState.dx;
  this._circleStyles.style.top = this._previousTop + gestureState.dy;
  this._updatePosition();
};

_updatePosition = () => {
  this.circle && this.circle.setNativeProps(this._circleStyles);
};

Note that we’re calling setNativeProps here in order to update the position of the circle view.

Tip

When working with animations, you can use setNativeProps to directly modify a component instead of the typical approach of setting state and props. This lets you bypass the overhead of re-rendering the component hierarchy, but it should be used sparingly.

Next, let’s implement _handlePanResponderGrant and _handlePanResponderEnd so that the circle changes color when a touch is active (Example 4-9).

Example 4-9. Implementing highlight behavior
_highlight = () => {
  this.circle &&
    this.circle.setNativeProps({
      style: { backgroundColor: "blue" }
    });
};

_unHighlight = () => {
  this.circle &&
    this.circle.setNativeProps({ style: { backgroundColor: "green" } });
};

_handlePanResponderGrant = (event, gestureState) => {
  this._highlight();
};

_handlePanResponderEnd = (event, gestureState) => {
  this._unHighlight();
};

Let’s put it all together to build an interactive view using PanResponder, as shown in Example 4-10.

Example 4-10. PanDemo.js illustrates the use of PanResponder
// Adapted from https://github.com/facebook/react-native/blob/master/
// Examples/UIExplorer/PanResponderExample.js

"use strict";

import React, { Component } from "react";
import { StyleSheet, PanResponder, View, Text } from "react-native";

const CIRCLE_SIZE = 40;
const CIRCLE_COLOR = "blue";
const CIRCLE_HIGHLIGHT_COLOR = "green";

class PanResponderExample extends Component {
  // Set some initial values.
  _panResponder = {};
  _previousLeft = 0;
  _previousTop = 0;
  _circleStyles = {};
  circle = null;

  constructor(props) {
    super(props);
    this.state = {
      numberActiveTouches: 0,
      moveX: 0,
      moveY: 0,
      x0: 0,
      y0: 0,
      dx: 0,
      dy: 0,
      vx: 0,
      vy: 0
    };
  }

  componentWillMount() {
    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 = {
      style: { left: this._previousLeft, top: this._previousTop }
    };
  }

  componentDidMount() {
    this._updatePosition();
  }

  render() {
    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 = () => {
    this.circle &&
      this.circle.setNativeProps({
        style: { backgroundColor: CIRCLE_HIGHLIGHT_COLOR }
      });
  };

  _unHighlight = () => {
    this.circle &&
      this.circle.setNativeProps({ style: { backgroundColor: CIRCLE_COLOR } });
  };

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

  _handleStartShouldSetPanResponder = (event, gestureState) => {
    // Should we become active when the user presses down on the circle?
    return true;
  };

  _handleMoveShouldSetPanResponder = (event, gestureState) => {
    // Should we become active when the user moves a touch over the circle?
    return true;
  };

  _handlePanResponderGrant = (event, gestureState) => {
    this._highlight();
  };

  _handlePanResponderMove = (event, gestureState) => {
    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.style.left = this._previousLeft + gestureState.dx;
    this._circleStyles.style.top = this._previousTop + gestureState.dy;
    this._updatePosition();
  };

  _handlePanResponderEnd = (event, gestureState) => {
    this._unHighlight();
    this._previousLeft += gestureState.dx;
    this._previousTop += gestureState.dy;
  };
}

const 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 }
});

export default PanResponderExample;

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-3 shows a screenshot, but you’ll want to experience it on a device with a real touchscreen.

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, you can use PanResponder. If you are designing a game, or an application with an unusual interface, you’ll need to spend some time building out the custom touch interactions you want.

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

Working with Lists

Many mobile user interfaces feature lists as a central element. You can see this interaction pattern in the Dropbox, Twitter, and iOS Settings apps (Figure 4-4). At its heart, a list is just a scrolling container with some child views. This deceptively simple design pattern is integral to many mobile interfaces.

lrn2 0404
Figure 4-4. Lists as used by Dropbox, Twitter, and the iOS Settings app

React Native provides two list components with convenient APIs. The <FlatList> component is designed to work with long scrolling lists of changing but similarly structured data. It has several performance optimizations baked in. The <SectionList> component is designed for data that is broken into logical sections, usually with section headings, similar to the iOS UITableView. Together, <FlatList> and <SectionList> cover most common use cases but if you need to peek under the hood and add some custom list handling, take a look at <VirtualizedList>.

Tip

Optimizing list-rendering performance is a notoriously tricky problem because different use cases call for different approaches. Is your user swiping hastily through a contacts list to find a particular person or are they slowly perusing a feed of images? Do you have a homogeneous list or is every child view different? If you hit performance issues, pay attention to your lists.

In this section, we are going to build an app that displays the New York Times Best Sellers list and lets us view data about each book, as shown in Figure 4-5. We’ll build two versions, one with <FlatList> and the other with <SectionList>.

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.

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

Using the Basic <FlatList> Component

We’re going to start with the basic <FlatList> component, which requires two props: data and renderItem.

<FlatList
  data={this.state.data}
  renderItem={this._renderItem} />

data is, as the name implies, the data that your <FlatList> will render. It should be an array where each element has a unique key property, plus whatever other properties you find useful.

renderItem should be a function that returns a component based on the data from one element of the data array.

The basic usage of a <FlatList> is demonstrated in Example 4-11.

Example 4-11. src/bestsellers/SimpleList.js
import React, { Component } from "react";

import { StyleSheet, Text, View, FlatList } from "react-native";

class SimpleList extends Component {
  constructor(props) {
    super(props);
    this.state = {
      data: [
        { key: "a" },
        { key: "b" },
        { key: "c" },
        { key: "d" },
        { key: "a longer example" },
        { key: "e" },
        { key: "f" },
        { key: "g" },
        { key: "h" },
        { key: "i" },
        { key: "j" },
        { key: "k" },
        { key: "l" },
        { key: "m" },
        { key: "n" },
        { key: "o" },
        { key: "p" }
      ]
    };
  }

  _renderItem = data => {
    return <Text style={styles.row}>{data.item.key}</Text>;
  };

  render() {
    return (
      <View style={styles.container}>
        <FlatList data={this.state.data} renderItem={this._renderItem} />
      </View>
    );
  }
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: "center",
    alignItems: "center",
    backgroundColor: "#F5FCFF"
  },
  row: { fontSize: 24, padding: 42, borderWidth: 1, borderColor: "#DDDDDD" }
});

export default SimpleList;

One of the common “gotchas” of working with <FlatList> is that renderItem gets passed an object with the actual data accessible via the item property.

_renderItem = data => {
  return <Text style={styles.row}>{data.item.key}</Text>;
};

We could simplify this with destructuring shorthand:

_renderItem = ({item}) => {
  return <Text style={styles.row}>{item.key}</Text>;
};

The app should look like Figure 4-6.

lrn2 0406
Figure 4-6. The SimpleList component renders a bare-bones <FlatList>

Updating the <FlatList> Contents

What if we want to do something a little more interesting? Let’s create a <FlatList> 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 Sellers list.

To begin with, we’ll use fake data to represent an example response from the New York Times API, as shown in Example 4-12.

Example 4-12. Mock data based on the expected API response
const mockBooks = [
  {
    rank: 1,
    title: "GATHERING PREY",
    author: "John Sandford",
    book_image:
      "http://du.ec2.nytimes.com.s3.amazonaws.com/prd/books/9780399168796.jpg"
  },
  {
    rank: 2,
    title: "MEMORY MAN",
    author: "David Baldacci",
    book_image:
      "http://du.ec2.nytimes.com.s3.amazonaws.com/prd/books/9781455586387.jpg"
  }
];

Then we’ll add a component that can render this data. The <BookItem> component, shown in Example 4-13, uses a combination of <View>, <Text>, and <Image> to display basic information about each book.

Example 4-13. src/bestsellers/BookItem.js
import React, { Component } from "react";

import { StyleSheet, Text, View, Image, ListView } from "react-native";

const styles = StyleSheet.create({
  bookItem: {
    flexDirection: "row",
    backgroundColor: "#FFFFFF",
    borderBottomColor: "#AAAAAA",
    borderBottomWidth: 2,
    padding: 5,
    height: 175
  },
  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" }
});

class BookItem extends Component {
  render() {
    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>
    );
  }
}

export default BookItem;

In order to use the <BookItem> component, we need to update our _renderItem function. A <BookItem> expects three props: coverURL, title, and author.

_renderItem = ({ item }) => {
  return (
    <BookItem
      coverURL={item.book_image}
      title={item.key}
      author={item.author}
    />
  );
};

Remember that in a <FlatList>, each element in the data array must have a unique key property defined. So, we’ll add a helper method that takes an array of objects and adds a key property to them, as shown in Example 4-14.

Example 4-14. The _addKeysToBooks method adds a key to each object in the books array
_addKeysToBooks = books => {
  return books.map(book => {
    return Object.assign(book, { key: book.title });
  });
};

Now that we have this helper method, we can update our initial state using the mock data from Example 4-12:

constructor(props) {
  super(props);
  this.state = { data: this._addKeysToBooks(mockBooks) };
}

Once we put it all together, our mocked-out Best Sellers application code should look like Example 4-15, with the resulting app displayed in Figure 4-7.

Example 4-15. src/bestsellers/MockBookList.js
import React, { Component } from "react";

import { StyleSheet, Text, View, Image, FlatList } from "react-native";

import BookItem from "./BookItem";

const mockBooks = [
  {
    rank: 1,
    title: "GATHERING PREY",
    author: "John Sandford",
    book_image:
      "http://du.ec2.nytimes.com.s3.amazonaws.com/prd/books/9780399168796.jpg"
  },
  {
    rank: 2,
    title: "MEMORY MAN",
    author: "David Baldacci",
    book_image:
      "http://du.ec2.nytimes.com.s3.amazonaws.com/prd/books/9781455586387.jpg"
  }
];

class BookList extends Component {
  constructor(props) {
    super(props);
    this.state = { data: this._addKeysToBooks(mockBooks) };
  }

  _renderItem = ({ item }) => {
    return (
      <BookItem
        coverURL={item.book_image}
        title={item.key}
        author={item.author}
      />
    );
  };

  _addKeysToBooks = books => {
    // Takes the API response from the NYTimes
    // and adds a key property to the object
    // for rendering purposes
    return books.map(book => {
      return Object.assign(book, { key: book.title });
    });
  };

  render() {
    return <FlatList data={this.state.data} renderItem={this._renderItem} />;
  }
}

const styles = StyleSheet.create({ container: { flex: 1, paddingTop: 22 } });

export default BookList;
lrn2 0407
Figure 4-7. Mock data displayed using <FlatList>

Integrating Real Data

Hardcoded data is well and good, but let’s test the real thing. The actual code to access the New York Times API is provided in Example 4-16.

Example 4-16. src/bestsellers/NYT.js
const API_KEY = "73b19491b83909c7e07016f4bb4644f9:2:60667290";
const LIST_NAME = "hardcover-fiction";
const API_STEM = "https://api.nytimes.com/svc/books/v3/lists";

function fetchBooks(list_name = LIST_NAME) {
  let url = `${API_STEM}/${LIST_NAME}?response-format=json&api-key=${API_KEY}`;
  return fetch(url)
    .then(response => response.json())
    .then(responseJson => {
      return responseJson.results.books;
    })
    .catch(error => {
      console.error(error);
    });
}

export default { fetchBooks: fetchBooks };

Let’s import that library into our component now.

import NYT from "./NYT";

Now let’s add a _refreshData method that invokes the New York Times API:

_refreshData = () => {
  NYT.fetchBooks().then(books => {
    this.setState({ data: this._addKeysToBooks(books) });
  });
};

Finally, we need to set our initial state to an empty array and call _refreshData in componentDidMount. Once we do that, our application will render live data from the New York Times Best Sellers list! The full code is shown in Example 4-17, and you can see the updated app in Figure 4-8.

Example 4-17. src/bestsellers/BookList.js
import React, { Component } from "react";

import { StyleSheet, Text, View, Image, FlatList } from "react-native";

import BookItem from "./BookItem";
import NYT from "./NYT";

class BookList extends Component {
  constructor(props) {
    super(props);
    this.state = { data: [] };
  }

  componentDidMount() {
    this._refreshData();
  }

  _renderItem = ({ item }) => {
    return (
      <BookItem
        coverURL={item.book_image}
        title={item.key}
        author={item.author}
      />
    );
  };

  _addKeysToBooks = books => {
    // Takes the API response from the NYTimes
    // and adds a key property to the object
    // for rendering purposes
    return books.map(book => {
      return Object.assign(book, { key: book.title });
    });
  };

  _refreshData = () => {
    NYT.fetchBooks().then(books => {
      this.setState({ data: this._addKeysToBooks(books) });
    });
  };

  render() {
    return (
      <View style={styles.container}>
        <FlatList data={this.state.data} renderItem={this._renderItem} />
      </View>
    );
  }
}

const styles = StyleSheet.create({ container: { flex: 1, paddingTop: 22 } });

export default BookList;
lrn2 0405
Figure 4-8. Viewing current best sellers, with <FlatList>

As you can see, working with the <FlatList> component is straightforward as long as you remember to structure your data properly. In addition to handling scrolling and touch interactions, <FlatList> also includes many performance optimizations to speed up rendering and reduce memory usage.

Working with <SectionList>

The <SectionList> component is designed for data sets where you have mostly homogeneous items plus optional section headings. For example, if we wanted to render several different kinds of best sellers lists with headings between them, a <SectionList> would be a good choice.

A <SectionList> expects the props sections, renderItem, and renderSectionHeader. We’ll start with sections, which should be an array where each object contains section data. Each section object must have the title and data keys. The data must look similar to data in a <FlatList>: it should be an array where each element has a unique key property.

Let’s update our _renderData method to fetch both the fiction and nonfiction best sellers lists, and update our component’s state accordingly.

_refreshData = () => {
  Promise
    .all([
      NYT.fetchBooks("hardcover-fiction"),
      NYT.fetchBooks("hardcover-nonfiction")
    ])
    .then(results => {
      if (results.length !== 2) {
        console.error("Unexpected results");
      }

      this.setState({
        sections: [
          {
            title: "Hardcover Fiction",
            data: this._addKeysToBooks(results[0])
          },
          {
            title: "Hardcover NonFiction",
            data: this._addKeysToBooks(results[1])
          }
        ]
      });
    });
};

We don’t need to update our _renderItem method, but we do need to add a new _renderHeader method. Let’s do that next.

_renderHeader = ({ section }) => {
  return (
    <Text style={styles.headingText}>
      {section.title}
    </Text>
  );
};

Finally, we need to update our render method to return a <SectionList> instead of a <FlatList>.

<SectionList
  sections={this.state.sections}
  renderItem={this._renderItem}
  renderSectionHeader={this._renderHeader}
/>

When we put everything together, our usage of <SectionList> should look like Example 4-18, resulting in the updated app shown in Figure 4-9.

Example 4-18. src/bestsellers/BookSectionList.js
import React, { Component } from "react";

import { StyleSheet, Text, View, Image, SectionList } from "react-native";

import BookItem from "./BookItem";
import NYT from "./NYT";

class BookList extends Component {
  constructor(props) {
    super(props);
    this.state = { sections: [] };
  }

  componentDidMount() {
    this._refreshData();
  }

  _addKeysToBooks = books => {
    // Takes the API response from the NYTimes
    // and adds a key property to the object
    // for rendering purposes
    return books.map(book => {
      return Object.assign(book, { key: book.title });
    });
  };

  _refreshData = () => {
    Promise
      .all([
        NYT.fetchBooks("hardcover-fiction"),
        NYT.fetchBooks("hardcover-nonfiction")
      ])
      .then(results => {
        if (results.length !== 2) {
          console.error("Unexpected results");
        }

        this.setState({
          sections: [
            {
              title: "Hardcover Fiction",
              data: this._addKeysToBooks(results[0])
            },
            {
              title: "Hardcover NonFiction",
              data: this._addKeysToBooks(results[1])
            }
          ]
        });
      });
  };

  _renderItem = ({ item }) => {
    return (
      <BookItem
        coverURL={item.book_image}
        title={item.key}
        author={item.author}
      />
    );
  };

  _renderHeader = ({ section }) => {
    return (
      <Text style={styles.headingText}>
        {section.title}
      </Text>
    );
  };

  render() {
    return (
      <View style={styles.container}>
        <SectionList
          sections={this.state.sections}
          renderItem={this._renderItem}
          renderSectionHeader={this._renderHeader}
        />
      </View>
    );
  }
}

const styles = StyleSheet.create({
  container: { flex: 1, paddingTop: 22 },
  headingText: {
    fontSize: 24,
    alignSelf: "center",
    backgroundColor: "#FFF",
    fontWeight: "bold",
    paddingLeft: 20,
    paddingRight: 20,
    paddingTop: 2,
    paddingBottom: 2
  }
});

export default BookList;
lrn2 0409
Figure 4-9. Viewing current best sellers, with <SectionList>

Navigation

Navigation in the context of mobile apps refers, roughly, to the code that allows users to transition from one screen to another. On the web, this is part of the window.history API, which provides concepts such as “backward” and “forward.”

Commonly used components for navigation in React Native include the built-in <Navigator> and <NavigatorIOS> components, as well as community solutions like <StackNavigator> (provided by the react-navigation library).

Navigation logic is necessary in order to move between screens in your mobile application. It also enables “deep linking,” so that users can jump from a URL into a particular screen within your app.

We’ll cover navigation in depth in Chapter 10.

Other Organizational Components

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

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

lrn2 0410
Figure 4-10. An iOS segmented control (top) and an iOS tab bar (bottom)
lrn2 0411
Figure 4-11. 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:

We’ll cover how to use platform-specific components in more depth in Chapter 7.

Summary

In this chapter, we dug into the specifics of a variety of the most important components in React Native. We discussed how to use basic low-level components, like <Text> and <Image>, as well as more abstract components like <FlatList>, <SectionList>, and <TabBarIOS>. We also looked at how to use various touch-focused APIs and components in case you want to build your own custom touch handlers.

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, 2nd Edition 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.