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).
HTML | React Native |
---|---|
|
|
|
|
|
|
|
|
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).
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).
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.
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.
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.
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).
<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.
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.
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.
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).
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).
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.