Beginning Mobile App Development PDF
Beginning Mobile App Development PDF
React Native
A comprehensive tutorial-style eBook that gets you from
zero to native iOS app development with JavaScript in no
time.
Manuel Kiessling
This book is for sale at http://leanpub.com/beginning-mobile-app-development-with-react-native
This is a Leanpub book. Leanpub empowers authors and publishers with the Lean Publishing
process. Lean Publishing is the act of publishing an in-progress ebook using lightweight tools and
many iterations to get reader feedback, pivot until you have the right book and build traction once
you do.
Trademark notes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1
Preface . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2
Status . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2
Intended audience . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2
Prerequisites . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2
As of now React Native has only been published for the iOS platform. Therefore, developing apps
for Android devices is not yet covered, but will be included in a free book update once React Native
for Android has been released.
.
Intended audience
The book will introduce readers to the React Native JavaScript framework and its mobile app
development environment. In the course of the book, the reader will build a full-fledged native
mobile app, learning about each React Native framework detail on the way to the final product.
Furthermore, the reader will be introduced to every tool and all JavaScript language constructs
needed to fully master software development with React Native: JSX, ECMAScript 6, the CSS
Flexbox system, Xcode, io.js and NPM, utilities like watchman, and more.
If you did some JavaScript programming before and want to become a mobile app developer, then
this book is for you. It introduces everything that is needed to work with the React Native JavaScript
framework in an easy-to-follow and comprehensive manner.
Prerequisites
In order to create React Native based iOS applications and work through the examples of this book,
you need all of the following:
Note that unless you want to run your applications on real iOS hardware like your iPhone, you do
not need to be enrolled to the Apple iOS Developer Program. In other words, you can run your
applications using the iOS simulator without the need to be enrolled.
In case you do not have access to an Apple computer, you can try to set up a Virtual Machine
running Mac OS X following the guide at https://blog.udemy.com/xcode-on-windows/.
From this point on, the book presumes that you have a running installation of Mac OS X with the
most recent version of Xcode installed.
.
Setting up your development
environment
React Native is a collection of JavaScript and Objective-C code plus supporting tools that allow to
create, run, and debug native iOS applications.
In order to reach the point where we can actually start working on our first React Native application,
some preparation is necessary.
On your development machine, the following components need to be made available:
Homebrew
io.js
Watchman
Google Chrome
React Native CLI
Installing Homebrew
Homebrew is a package manager for Mac OS X. We will use it to subsequently install most of the
software tools we need.
In order to install and set up Homebrew, open a Terminal shell window and run the following
command:
Installing io.js
io.js is a modern fork of Node.js, the popular server-side JavaScript runtime environment. React
Native ships with some helper tools that are written for io.js. Also, we will regularly use the
Node.js/io.js package manager, NPM, to install the React Native command line tool and other
dependencies.
We are not going to install io.js directly. Instead, we will use Homebrew to install nvm, the Node
Version Manager.
Setting up your development environment 5
nvm is a nifty little tool that takes the hassle out of managing parallel installs of different versions
of Node.js and/or io.js. With it, you can install as many versions of Node.js and io.js as you wish,
and quickly switch to the version you want to actually use for a given project.
In order to install and set up nvm, open a Terminal window and run the following command:
Afterwards, create or open file $HOME/.bashrc, and add the following lines at the beginning:
export NVM_DIR=~/.nvm
source $(brew --prefix nvm)/nvm.sh
Close the Terminal session, and start Terminal anew - you now have nvm enabled in your Terminal
session.
Using nvm, we can now install io.js, like this:
This will install and setup the most recent stable version of io.js, plus the matching version of NPM,
the Node Package Manager, and make this setup the default.
Installing Watchman
React Native uses Watchman to monitor and react to changes in source code files. This is explained
in more detail in a later chapter.
In order to install and set up Watchman, open a Terminal window and run the following command:
The --HEAD parameter ensures that the most recent version of Watchman is installed.
You might also want to install a decent code editor. While Xcode is part of our development
environment, its certainly not the most well-suited JavaScript editor out there. Check out
TextWrangler at http://www.barebones.com/products/textwrangler/ if you prefer a simple yet
sufficient tool, or IntelliJ IDEA Community Edition for a full-fledged IDE at https://www.jetbrains.
com/idea/download/. Atom, available at https://atom.io/, lies somewhat in between. All three tools
are usable free of charge.
.
Creating your first native iOS
application using React Native
In this chapter, we will create a very simple application and make it run in the iOS simulator. By
doing so, we are using a whole lot of different components that in total allow us to end up with
a working app. Once our app runs, we will look under the hood in order to understand all those
components and get a feeling for the inner workings of React Native applications. These insights
form the basis that allows us to build more complex applications while truly understanding what
we are doing.
This results in some work being done which gives you a new folder named HelloWorld. We will
analyze its contents later - for now, we want to get our feet wet as quickly as possible.
In case you get an ugly red error screen in the iOS Simulator that says Could not connect to
development server, then proceed as follows: - Switch to a Terminal window - cd to the root folder
of the Hello World project - Run npm start and keep the Terminal open
.
Creating your first native iOS application using React Native 8
An awful lot of very interesting things just happened in order to display this simulator screen, and
dissecting all the components involved and analyzing all the ways these components interact with
each other will be a very exciting trip down the rabbit hole that is React Native - but, not yet. First
the doing, then the explanations.
In Xcode, you probably saw a file named main.jsbundle. For now, this is not what we are looking
for.
.
Its immediately obvious that this is the file behind the UI we see in the simulator:
1 /**
2 * Sample React Native App
3 * https://github.com/facebook/react-native
4 */
5 'use strict';
6
7 var React = require('react-native');
8 var {
9 AppRegistry,
10 StyleSheet,
11 Text,
12 View,
13 } = React;
14
15 var HelloWorld = React.createClass({
16 render: function() {
17 return (
18 <View style={styles.container}>
19 <Text style={styles.welcome}>
20 Welcome to React Native!
21 </Text>
22 <Text style={styles.instructions}>
23 To get started, edit index.ios.js
24 </Text>
25 <Text style={styles.instructions}>
26 Press Cmd+R to reload,{'\n'}
27 Cmd+Control+Z for dev menu
Creating your first native iOS application using React Native 9
28 </Text>
29 </View>
30 );
31 }
32 });
33
34 var styles = StyleSheet.create({
35 container: {
36 flex: 1,
37 justifyContent: 'center',
38 alignItems: 'center',
39 backgroundColor: '#F5FCFF',
40 },
41 welcome: {
42 fontSize: 20,
43 textAlign: 'center',
44 margin: 10,
45 },
46 instructions: {
47 textAlign: 'center',
48 color: '#333333',
49 marginBottom: 5,
50 },
51 });
52
53 AppRegistry.registerComponent('HelloWorld', () => HelloWorld);
Clearly, this is JavaScript code, but if you havent followed the latest developments in the JS world
or if you did not yet play around with the normal React framework, then some parts of this code
might look a bit odd. Not that this should stop us from fiddling around!
Change line 20 from
to
Hello, World!
Then switch back to the iOS Simulator window and hit -R. The UI of our app will refresh and
display the new text.
Now, if you havent done any native iOS app development before, hitting Refresh and seeing
the changes you did to the code reflected in the Simulator probably doesnt feel like a big deal,
but it actually is. The same procedure for an Objective-C or Swift based application involves a
recompilation of the changed source code and a rebuild of the application that is then completely
restarted in the Simulator. That doesnt only sound like it takes longer, it does take longer.
Once again, the explanation for why and how this refresh workflow actually works will follow
later.
Creating your first native iOS application using React Native 10
It would be great if we could make our app do something. Lets try to add a text input field for
entering a name and change the behaviour of our app in a way that makes it greet the name we
enter - if this doesnt secure us the #1 spot in the App Store charts, then I dont know what will.
What we need in order to achieve this functionality is a) an additional UI element plus styling that
allows to input text, b) a function that handles the text that is input, and c) a way to output the input
text within the greet text element. With that, the result should look like this:
1 /**
2 * Sample React Native App
3 * https://github.com/facebook/react-native
4 */
5 'use strict';
6
7 var React = require('react-native');
8 var {
9 AppRegistry,
10 StyleSheet,
11 Text,
12 TextInput,
13 View,
14 } = React;
15
16 var HelloWorld = React.createClass({
17 getInitialState: function() {
18 return {
19 name: 'World'
20 };
21 },
22 onNameChanged: function(event) {
23 this.setState({ name: event.nativeEvent.text });
24 },
25 render: function() {
26 return (
27 <View style={styles.container}>
28 <TextInput
29 style={styles.nameInput}
30 onChange={this.onNameChanged}
31 placeholder='Who should be greeted?'/>
32 <Text style={styles.welcome}>
33 Hello, {this.state.name}!</Text>
34 <Text style={styles.instructions}>
35 To get started, edit index.ios.js
36 </Text>
37 <Text style={styles.instructions}>
38 Press Cmd+R to reload,{'\n'}
39 Cmd+Control+Z for dev menu
40 </Text>
41 </View>
42 );
43 }
44 });
45
46 var styles = StyleSheet.create({
47 container: {
48 flex: 1,
49 justifyContent: 'center',
50 alignItems: 'center',
51 backgroundColor: '#F5FCFF',
52 },
53 welcome: {
54 fontSize: 20,
55 textAlign: 'center',
56 margin: 10,
Creating your first native iOS application using React Native 12
57 },
58 instructions: {
59 textAlign: 'center',
60 color: '#333333',
61 marginBottom: 5,
62 },
63 nameInput: {
64 height: 36,
65 padding: 4,
66 margin: 24,
67 fontSize: 18,
68 borderWidth: 1,
69 }
70 });
71
72 AppRegistry.registerComponent('HelloWorld', () => HelloWorld);
With this, we introduce a new React Native UI element, TextInput (line 12), which we add to our
view (line 28). An accompanying style definition is added, too (line 63). The view is set up with an
initial state (line 17), and we define a function called onNameChanged that changes this state (line
22). This function is called whenever the value of the text input changes (line 30). Our text block is
now dynamic and always reflects the value of the state variable name (line 33).
Re-running the application (remember, -R while in iOS Simulator is all it takes) presents the new
user interface which now shows a text input field. Whatever we put into this field is immediately
reflected within the greet message below. Achievement unlocked.
React Native architecture explained
Ok, we played around with it, but learning to understand how React Native actually works is
overdue.
HelloWorld.xcodeproj/
Podfile
iOS/
index.ios.js
node_modules/
package.json
To give you the full picture, here is what exactly happens when running react-native init
HelloWorld:
We already saw that the React Native based JavaScript code that makes up our actual application
lives in index.ios.js.
The package.json file is no surprise for those who already worked with io.js or Node.js; it defines
some metadata for our project and, most importantly, declares react-native (this time, this means
the actual framework that makes our application possible) as a dependency of our own project.
The node_modules folder is simply a result of the npm install run that took place during project
initialization. It contains the react-native code, which in turn consists of other NPM dependencies,
helper scripts, and a lot of JavaScript and Objective-C code.
The initialization process also provided the minimum Xcode project definitions plus some
Objective-C boilerplate code, which allows us to open our new project in Xcode and to instantly
run the application without any further ado. All of this could have been done manually, but would
include a lot of steps that are identical no matter what kind of application we are going to write,
thus it makes sense to streamline this process via react-native init.
Podfile is similar to package.json; it declares Objective-C library dependencies for the CocoaPods
dependency manager. We will talk about this in more detail later in the book, and ignore it for
now.
.
The takeaway here is that our HelloWorld project is multiple things at once. It is an Xcode project,
but its also an NPM project. Its a React Native based JavaScript application, but it also contains
some iOS glue code that is needed to make our JavaScript code run on iOS in the first place.
With this overview, we can dive one level deeper and start to look at how the different elements in
our project folder interact with each other in order to end up as a working native iOS application.
This allows to provide users something that is very close to an actual mobile app by utilizing common
web technologies. However, even though the Objective-C code that makes up the wrapper which
creates the UIWebView is native, the content within the UIWebView is not - its just a webpage,
which is why these hybrid apps gained the reputation of providing a user experience that isnt as
seamless as those of full native apps.
In our HelloWorld example, we wrote JavaScript, but we didnt create a webpage. No UIWebView
is utilized in order to execute our code.
Instead, our code is run in an embedded instance of JavaScriptCore inside our app and rendered to
higher-level platform-specific components. This might sound a bit cryptic, but lets follow the code.
In the Xcode Project navigator, open the file HelloWorld/AppDelegate.m. This is what it looks like:
1 /**
2 * Copyright (c) 2015-present, Facebook, Inc.
3 * All rights reserved.
4 *
5 * This source code is licensed under the BSD-style license found in the
6 * LICENSE file in the root directory of this source tree. An additional grant
7 * of patent rights can be found in the PATENTS file in the same directory.
8 */
9
10 #import "AppDelegate.h"
11
12 #import "RCTRootView.h"
13
14 @implementation AppDelegate
15
16 - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
17 {
18 NSURL *jsCodeLocation;
19
20 // Loading JavaScript code - uncomment the one you want.
21
22 // OPTION 1
23 // Load from development server. Start the server from the repository root:
24 //
25 // $ npm start
26 //
27 // To run on device, change `localhost` to the IP address of your computer, and make sure your computer and
28 // iOS device are on the same Wi-Fi network.
29 jsCodeLocation = [NSURL URLWithString:@"http://localhost:8081/index.ios.bundle"];
30
31 // OPTION 2
32 // Load from pre-bundled file on disk. To re-generate the static bundle, run
33 //
34 // $ curl 'http://localhost:8081/index.ios.bundle?dev=false&minify=true' -o iOS/main.jsbundle
35 //
36 // and uncomment the next following line
37 // jsCodeLocation = [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"];
38
39 RCTRootView *rootView = [[RCTRootView alloc] initWithBundleURL:jsCodeLocation
40 moduleName:@"HelloWorld"
41 launchOptions:launchOptions];
React Native architecture explained 16
42
43 self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
44 UIViewController *rootViewController = [[UIViewController alloc] init];
45 rootViewController.view = rootView;
46 self.window.rootViewController = rootViewController;
47 [self.window makeKeyAndVisible];
48 return YES;
49 }
50
51 @end
AppDelegate.m is our apps entry point from iOS point of view. Its the place where the native
Objective-C code and our React Native JavaScript code are glued together. The key player in bringing
these two worlds together is the RCTRootView component, whose header file is imported on line 12.
The RCTRootView component is a native Objective-C class provided by the React Native framework.
It is the component that takes our React JavaScript code and executes it. And, in the other direction,
it allows us to call into native iOS UI elements from our JavaScript code. This way, the controlling
code of our applications always is actual JavaScript (its not transformed into Objective-C or Swift
or byte code or anything under the hood), but the UI elements that end up on the screen of our iOS
device are native UIKit elements like those used in classical Objective-C or Swift based apps, and
not the result of some webpage rendering.
This architecture also explains why we can simply reload our application. If we only change
our JavaScript code, then no Objective-C code changes, thus no recompilation is neccessary. The
RCTRootView listens to the -R sequence and reacts by resetting the UI, retrieving the latest
JavaScript code (how this is done is explained in a moment), and executing the new code. The
actual native Objective-C app that wraps the RCTRootView simply keeps running.
In case the React packager currently isnt running, simply go to the project folder and run npm
start. As defined in the package.json, this executes node_modules/react-native/packager/pack-
ager.sh.
.
As we have seen, the React packager is started automatically upon building and running the app in
Xcode. Sometimes, it is not stopped automatically when closing the iOS Simulator and Xcode.
When this happens, you are not able to start another instance on the packager, because the TCP
port is blocked. If this happens, look for a stray Terminal window that still runs the packager, and
close it.
.
render: function() {
return (
<View style={styles.container}>
<TextInput
style={styles.nameInput}
onChange={this.onNameChanged}
placeholder='Who should be greeted?'/>
<Text style={styles.welcome}>
Hello, {this.state.name}!</Text>
<Text style={styles.instructions}>
To get started, edit index.ios.js
</Text>
<Text style={styles.instructions}>
Press Cmd+R to reload,{'\n'}
Cmd+Control+Z for dev menu
</Text>
</View>
);
}
React Native architecture explained 18
This type of code is called JSX, the XML-like syntax extension to ECMAScript, to quote from the
project homepage at http://facebook.github.io/jsx/. Lets quote even more from that page:
The interesting bit here is the one about transpilers. I said that the React packager needs to convert
the source code before serving it to the app. In fact, there are multiple conversions in place, and
transpiling JSX into ES5 syntax is one of these conversions.
The transpiled version of the above code block, as we find it at http://localhost:8081/index.ios.bundle,
looks like this:
render: function() {
return (
React.createElement(View, {style: styles.container},
React.createElement(TextInput, {
style: styles.nameInput,
onChange: this.onNameChanged,
placeholder: "Who should be greeted?"}),
React.createElement(Text, {style: styles.welcome},
"Hello, ", this.state.name, "!"),
React.createElement(Text, {style: styles.instructions},
"To get started, edit index.ios.js"
),
React.createElement(Text, {style: styles.instructions},
"Press Cmd+R to reload,", '\n',
"Cmd+Control+Z for dev menu"
)
)
);
}
Hence my claim that we would totally get away with writing straight ES5 JavaScript, but its obvious
that this would result in quite a lot more keystrokes and in much less readable code. This is not to say
that the JSX syntax doesnt take some time getting used to, but in my experience it feels very natural
real quick. JSX is a very central component of React and React Native, and we will constantly be
using it in the course of this book.
There is another transpiler at work in the React packager. It converts from ECMAScript 2015 to ES5.
ECMAScript 2015, or simply ES2015, is the upcoming version of JavaScript. It brings a whole lot
of new language features to JavaScript like destructuring, computed property keys, classes, arrow
functions, block-scoped variables, and much more.
React Native architecture explained 19
There has been a lot of irritation around the name of the new JavaScript language version, which
is why next to ECMAScript 2015, one encounters any of the following labels around the web:
ECMAScript.next, ECMAScript 6, ECMAScript Harmony, and, to simply sum it up, ES6. ES6 is still
the most widely used term and probably your best friend when searching the web for information
on this topic. In this book I will use the short form of the final term that was decided on recently:
ES2015.
Tip: there is a live ES2015 to ES5 transpiler at https://babeljs.io/repl/.
.
The specification of the new ES2015 language version isnt 100% final yet, but thats not stopping
people from writing ES2015 to ES5 transpilers, which is why the following ES2015 code in our
index.ios.js file works:
Here, 5 different variables are assigned a value at once, from the variable React. This is called a
destructuring assignment. Here is a more simple example that shows how it works:
var fruits = {banana: "A banana", orange: "An orange", apple: "An apple"};
This will assign the value A banana to the variable banana, the value An orange to the variable
orange, and An apple to the variable apple. The transpiled code looks like this:
var fruits = { banana: "A banana", orange: "An orange", apple: "An apple" };
Thus we can follow that the React variable is an object with keys like AppRegistry, StyleSheet etc.,
and we can use this shorthand notation to create and assign variables with the same name all at
once.
There is yet another part of our code in index.ios.js that needs to be transpiled, on the last line:
React Native architecture explained 20
becomes
Its another nifty shorthand notation in ES2015, the arrow function, which simplifies anonymous
function declaration. Theres a bit more to it than just saving keystrokes, we will get to this later.
Summary
Ok, lets recap:
We have seen that architecturally, our React Native applications are native Objective-C
programs that set up an RCTRootView, which can be seen as a kind of container where
JavaScript code can be executed. This container also allows the JavaScript code to bind to
native iOS UI elements, which results in a very fluid user interface.
In order to get our JavaScript application code together with the React and React Native
JavaScript library loaded into the RCTRootView, the React packager is used. This io.js
application is a transpiler (it converts JSX and ES2015 into ES5 JavaScript), a bundler (it creates
one single large script from our own code files and the React library scripts) and a webserver
(it provides the transpiled and bundled code via HTTP).
We learned about some of the modern language approaches React Native takes in regards to
JavaScript code, like JSX, destructuring assignments, and the arrow function.
We are now prepared to explore serious application development with React Native. We will learn
the details of working with JSX, StyleSheets, ES2015, UIKit elements and much more while doing
so.
Creating a first real app: BookBrowser
Playing around with our Hello World example wasnt really satisfying. We will now build a new
app from scratch that provides useful, real-world functionality.
The app we are going to build will be called BookBrowser. It is a small mobile app that acts as an
interface to the Google Books database. It allows to search for specific books and provides detailed
information about the books it finds.
The React packager is a web server that listens on TCP port 8081, and we cannot start another
packager process for our new project on that port while another process still occupies it.
.
On the command line, leave the HelloWorld project folder and create the new project by running
react-native init BookBrowser.
Consider always running npm update -g react-native before creating new projects. React Native
is still a very young project that releases new versions regularly.
You may also encounter the scenario where you are working on an application, and the React
Native team releases a new version of the framework. You can find out about this by keeping an
eye on https://github.com/facebook/react-native/releases/latest.
When a new release takes place and you want to upgrade your current app to the new release,
proceed as follows:
In Xcode, stop the running app by hitting -. (thats the command key and a period), and
then hit shift--K to clean all compiled sources
You can now run the app again, and it will compile with the updated framework files.
.
When react-native has finished building the project structure, open the newly created index.ios.js
file in your JavaScript editor, and strip this file to a bare minimum, like this:
1 'use strict';
2
3 var React = require('react-native');
4 var {
5 AppRegistry,
6 View,
7 } = React;
8
9 var BookBrowser = React.createClass({
10 render: function() {
11 return (
12 <View>
13 </View>
14 );
15 }
16 });
17
18 AppRegistry.registerComponent('BookBrowser', () => BookBrowser);
This gives us a bare minimum React Native app in the sense that you cant remove another element
without ending up with a broken application. From this, we can start. One take away from this is that
we cant have an application without a view. We even need a view even if we dont view anything.
___
BookBrowser
Find books containing:
Starting the search should result in a scrollable results list showing rows of found books with a
thumbnail of the cover, the title, and the subtitle:
___
< Back
Title
Subtitle
Title
Subtitle
Title
Subtitle
Title
Subtitle
Finally, the app allows to tap on a result, which makes the app navigate to a book detail screen that
displays further information about the selected title:
Creating a first real app: BookBrowser 24
___
< Back
Title
Subtitle
Summary text, summary
text, summary text,
summary text, summary
text, summary text.
Name of author
We likely want to show some kind of activity indicator and a Please wait message during long-
running operations (e.g. when querying the Google Books API for search results).
With that said, we need to implement the following elements:
Lets see what building blocks React Native provides for us:
Screens can be realized by building UIs via JSX and styling them using React.StyleSheet.
Text can be added to these UIs with the Text JSX element
Text inputs are added with the TextInput element
Images are place on the UI with the Image element
Lists are built with the ListView element
Tap events can be handled using TouchableHighlight element
We can build a hierarchy of screens including navigation handling using the NavigatorIOS
element
Creating a first real app: BookBrowser 25
Using React Natives implementation (or rather, polyfill) of the fetch abstraction, we can query
resources from the web
Looks like we are going to create a lot of JSX structures. These will be accompanied by and combined
with some plain JavaScript mechanisms that take care of handling user touch events, resource
fetching, and wiring things up.
1 'use strict';
2
3 var React = require('react-native');
4 var {
5 AppRegistry,
6 View,
7 } = React;
8
9 var BookBrowser = React.createClass({
10 render: function() {
11 return (
12 <View>
13 <Text>
14 BookBrowser
15 </Text>
16 </View>
17 );
18 }
19 });
20
21 AppRegistry.registerComponent('BookBrowser', () => BookBrowser);
Reload this in the iOS simulator with -R, and you get - a not very subtle red error screen talking
about a getInvalidGlobalUseError. Why? We are now using another JSX element, Text, which is
transpiled to
React.createElement(Text, null,
"BookBrowser"
)
As you can see, React.createElement references an entity named Text, but this is not available in
the current scope. We need to make it available by assigning it from the React object, using the
destructuring assignment in line 4:
Creating a first real app: BookBrowser 26
1 'use strict';
2
3 var React = require('react-native');
4 var {
5 AppRegistry,
6 View,
7 Text,
8 } = React;
9
10 var BookBrowser = React.createClass({
11 render: function() {
12 return (
13 <View>
14 <Text>
15 BookBrowser
16 </Text>
17 </View>
18 );
19 }
20 });
21
22 AppRegistry.registerComponent('BookBrowser', () => BookBrowser);
Ok, this makes the app work error-free, but the text we added is located in the upper left corner of
the UI. We need to make use of React Natives styling facilities if we want to build a UI that looks
reasonable.
1 'use strict';
2
3 var React = require('react-native');
4 var {
5 AppRegistry,
6 View,
7 Text,
8 StyleSheet,
9 } = React;
10
11 var BookBrowser = React.createClass({
12 render: function() {
13 return (
14 <View style={styles.container}>
15 <Text>
16 BookBrowser
17 </Text>
18 </View>
19 );
20 }
21 });
22
23 var styles = StyleSheet.create({
24 container: {
25 flex: 1,
26 flexDirection: 'column',
27 justifyContent: 'center',
28 alignItems: 'center'
29 }
30 });
31
32 AppRegistry.registerComponent('BookBrowser', () => BookBrowser);
Note how line 8 introduces the StyleSheet object before using it on line 23.
.
With this, the string BookBrowser is now placed at the center of the screen, horizontally as well as
vertically:
___
BookBrowser
Creating a first real app: BookBrowser 28
We didnt add that much code, but I need to back up a bit and explain what forces are at work here.
A
B A B C
>
C v
The Flexbox system in React Native is an approximation of the real Flexbox system of CSS. It is
not complete and doesnt provide everything that the original provides. E.g., in CSS you can define
two other flow directions, column-reverse and row-reverse. As of this writing, this is not possible
with React Native.
.
Conceptually, what flexDirection defines is the so-called main axis. Its the orientation and direction
along which elements in the container flow. If you define the flexDirection as row, and you add
Creating a first real app: BookBrowser 29
three elements A, B and C to the container, these elements will be rendered on the screen next to
each other from left to right, i.e., A B C.
If our flexDirection is column, then our main axis runs from top to bottom. As a result, the so called
cross axis then runs from left to right, and vice versa:
m c
a r
i o
n s
s
cross axis main axis
> >
a a
x x
i i
s s
v v
Now the other two parameters we defined come into play: justifyContent and alignItems. Their
value defines how elements are aligned on the main axis (via justifyContent) and on the cross axis
(via alignItems).
With both parameters set to center in our case, and the container filling the whole screen, our
element ends up in the middle of the screen, horizontally as well as vertically.
The most useful settings for both parameters are flex-start, center, and flex-end (there are more, but
we will not discuss them now). Lets see how these work in our case:
flexDirection = column
1 2 3
m
a
i
n
cross axis
4 5 > 6
a
x
i
s
Creating a first real app: BookBrowser 30
v
7 8 9
Here are the parameter settings that result in the location of elements 1 to 9:
The key to understand where an element ends up on the screen is to visualize where the main axis
and the cross axis run (which depends on the value of flexDirection) - probably visualize them as
actual arrows like in the ascii art diagrams in this book.
A justifyContent value of flex-start means that the element is placed at the start of the arrow, center
means its placed half-way between the start and the end of the arrow, and flex-end means its placed
at the end of the arrow, at the arrowhead. The same applies for the cross axis and the placement
definition via alignItems.
According to the mental model we have built around the mechanics of the Flexbox system, another
JSX text element right after the one for BookBrowser should end up below that. Thus, this code:
1 'use strict';
2
3 var React = require('react-native');
4 var {
5 AppRegistry,
6 View,
7 Text,
8 StyleSheet,
9 } = React;
10
11 var BookBrowser = React.createClass({
12 render: function() {
13 return (
14 <View style={styles.container}>
15 <Text>
Creating a first real app: BookBrowser 31
16 BookBrowser
17 </Text>
18 <Text>
19 Find books containing:
20 </Text>
21 </View>
22 );
23 }
24 });
25
26 var styles = StyleSheet.create({
27 container: {
28 flex: 1,
29 flexDirection: 'column',
30 justifyContent: 'center',
31 alignItems: 'center'
32 }
33 });
34
35 AppRegistry.registerComponent('BookBrowser', () => BookBrowser);
___
BookBrowser
Find books containing:
In case we set flexDirection to row, the BookText element would still be placed at the center of
the screen, but the next JSX element we add would end up right to BookText, not below it. You
can verify this by simply setting flexDirection from column to row in your code.
.
Creating a first real app: BookBrowser 32
1 'use strict';
2
3 var React = require('react-native');
4 var {
5 AppRegistry,
6 View,
7 Text,
8 TextInput,
9 StyleSheet,
10 } = React;
11
12 var BookBrowser = React.createClass({
13 render: function() {
14 return (
15 <View style={styles.container}>
16 <Text>
17 BookBrowser
18 </Text>
19 <Text>
20 Find books containing:
21 </Text>
22 <TextInput/>
23 </View>
24 );
25 }
26 });
27
28 var styles = StyleSheet.create({
29 container: {
30 flex: 1,
31 flexDirection: 'column',
32 justifyContent: 'center',
33 alignItems: 'center'
34 }
35 });
36
37 AppRegistry.registerComponent('BookBrowser', () => BookBrowser);
Switch to the iOS simulator, hit -R, and - bummer. Looks like nothing has changed at all. And
visually, that is indeed true. The new element is there, but it isnt visible, because it doesnt have
any styling. A text input without styling is a text input that simply doesnt take up any space.
In order to make it visible, add the following stylesheet definition below the container block:
Creating a first real app: BookBrowser 33
textInput: {
height: 30,
borderWidth: 1,
}
<TextInput style={styles.textInput}/>
___
BookBrowser
Find books containing:
Well, to a certain degree at least. The input box takes up the whole width of the screen. Now, one
could assume that setting a width in the stylesheet would solve this easily, but the result is not what
one would expect:
textInput: {
height: 30,
width: 300,
borderWidth: 1,
}
results in
Creating a first real app: BookBrowser 34
___
BookBrowser
Find books containing:
With all flex orientations set to center, why does the input field end up on the left? My honest answer
is: I have no idea. It might even be a bug in React Native. The solution, however, is to not set a width
at all, but instead make the element keep some distance using margins:
textInput: {
height: 30,
borderWidth: 1,
marginLeft: 60,
marginRight: 60
}
Great. This doesnt exactly give us an award winning layout, but at least a working, correct one.
Now, Im not a designer and cannot teach you app layout design, but here is a more sophisticated
stylesheet which results in a screen that I would consider nice-looking:
1 'use strict';
2
3 var React = require('react-native');
4 var {
5 AppRegistry,
6 View,
7 Text,
8 TextInput,
9 StyleSheet,
10 } = React;
11
12 var BookBrowser = React.createClass({
13 render: function() {
14 return (
15 <View style={styles.container}>
Creating a first real app: BookBrowser 35
16 <Text style={styles.headline}>
17 BookBrowser
18 </Text>
19 <Text style={styles.label}>
20 Find books containing:
21 </Text>
22 <TextInput
23 placeholder="e.g. JavaScript or Mobile"
24 style={styles.textInput}/>
25 </View>
26 );
27 }
28 });
29
30 var styles = StyleSheet.create({
31 container: {
32 flex: 1,
33 flexDirection: 'column',
34 justifyContent: 'center',
35 alignItems: 'center',
36 backgroundColor: '#5AC8FA',
37 },
38 headline: {
39 fontSize: 36,
40 fontWeight: 'bold',
41 color: '#FFF',
42 marginBottom: 28,
43 },
44 label: {
45 fontSize: 24,
46 fontWeight: 'normal',
47 color: '#FFF',
48 marginBottom: 8,
49 },
50 textInput: {
51 borderColor: '#8E8E93',
52 borderWidth: 0.5,
53 backgroundColor: '#FFF',
54 height: 40,
55 marginLeft: 60,
56 marginRight: 60,
57 padding: 8,
58 }
59 });
60
61 AppRegistry.registerComponent('BookBrowser', () => BookBrowser);
Just like in HTML5, text input fields can have a placeholder text (line 23) which is presented to the
user as long as no text is put into the field.
1 point 4 pixels
--->
The borderWidth of the text input field is set to 0.5 - on the iPhone 6, it is therefore rendered with a
width of 1 physical pixel.
While this system might seem limiting at first, it really eases app development for multiple devices
a lot, because the logical resolution approach gives us a common coordinate system independent of
actual screen resolution. If we had to work with the physical resolution, we had to handle device
differences ourselves. This would probably feel a bit like this pseudo-code:
Creating a first real app: BookBrowser 37
Handling events
Ok, enough layout magic, at least for now. We need to build some logic into our app - when entering
text and submitting the input, we need to query the Google Books API for titles containing the
entered text, and we need to present a list of matching books to the user.
Thats three distinct steps we need to take. First, we need to react to the event of the text input being
submitted, or finished. As we will see, this can be done implicitly - we dont need to put a Go!
button into the UI. The input field itself is configurable enough for our needs.
Here are the changes we need to introduce in order to make our text input more intelligent:
1 'use strict';
2
3 var React = require('react-native');
4 var {
5 AppRegistry,
6 View,
7 Text,
8 TextInput,
9 StyleSheet,
10 } = React;
11
12 var BookBrowser = React.createClass({
13 render: function() {
14 return (
15 <View style={styles.container}>
16 <Text style={styles.headline}>
17 BookBrowser
18 </Text>
19 <Text style={styles.label}>
20 Find books containing:
21 </Text>
22 <TextInput
23 placeholder="e.g. JavaScript or Mobile"
24 returnKeyType="search"
Creating a first real app: BookBrowser 38
25 enablesReturnKeyAutomatically="true"
26 onEndEditing={ event => console.log(event.nativeEvent.text) }
27 style={styles.textInput}/>
28 </View>
29 );
30 }
31 });
32
33 var styles = StyleSheet.create({
34 container: {
35 flex: 1,
36 flexDirection: 'column',
37 justifyContent: 'center',
38 alignItems: 'center',
39 backgroundColor: '#5AC8FA',
40 },
41 headline: {
42 fontSize: 36,
43 fontWeight: 'bold',
44 color: '#fff',
45 marginBottom: 28,
46 },
47 label: {
48 fontSize: 24,
49 fontWeight: 'normal',
50 color: '#fff',
51 marginBottom: 8,
52 },
53 textInput: {
54 borderColor: '#8E8E93',
55 borderWidth: 0.5,
56 backgroundColor: '#fff',
57 height: 40,
58 marginLeft: 60,
59 marginRight: 60,
60 padding: 8,
61 }
62 });
63
64 AppRegistry.registerComponent('BookBrowser', () => BookBrowser);
The changes are on line 24, 25 and 26. We added three new properties to the TextInput element:
returnKeyType, enablesReturnKeyAutomatically, and onEndEditing.
The result of the first property is purely aesthetically. It changes the default return button of the iOS
software keyboard from return to Search.
In case you dont see the software keyboard when entering text into the input field, hit -K in the
simulator in order to activate it.
.
Creating a first real app: BookBrowser 39
<View style={styles.container}>
the style attribute is assigned the result of the JavaScript expression styles.container. That isnt a
very exciting expression, of course: its simply a reference to the attribute of an object. More complex
expression are possible, though.
Instead of referencing the stylesheet, we could also define the style settings inline:
<View style={{
flex: 1,
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#5AC8FA',
}}>
The first curly brace starts the expression, the second curly brace starts the object containing the
style attributes.
.
Want to randomly give the app one of two possible background colors? No problem. While full-on
if statements cannot be used inside JSX JavaScript expressions, the ternary operator works just fine:
Creating a first real app: BookBrowser 40
<View style={{
flex: 1,
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
backgroundColor: Math.random() > 0.5 ? '#5AC8FA' : '#4CD964',
}}>
Repeatedly reloading the app in the simulator will present the app with a green background 50% of
the time.
But back to onEndEditing. What exactly is our JavaScript expression here, and what does it do?
onEndEditing={ event => console.log(event.nativeEvent.text) }
The curly braces, of course, start the expression. Then, using the ES2015 arrow function syntax
(param => expression), we declare an anonymous function.
Remember, its just a new shorthand notation. Functionally, its identical to
function(event) { return console.log(event.nativeEvent.text); }
And if you once again open http://localhost:8081/index.ios.bundle and search for React.createElement(TextInput,
you will see that this is the ES5 code that React packager transpiled the ES2015 code to. This also
means that nothing stops us from putting the ES5 version into the JSX:
onEndEditing={ function(event) { return console.log(event.nativeEvent.text); } }
But of course, the arrow function syntax makes our code much leaner.
The actual logic we trigger when the event occurs is not very exciting yet. We simply log the value
of the text attribute of the event to the console.
Nevertheless, two things are interesting here. How does the event get here, and what does console
logging mean in a React Native JavaScript environment versus console logging in a web browser
JavaScript environment?
Lets look at the event first. When the user finishes editing and submits the text input field by
tapping Search on the software keyboard, the event occurs. This leads to React Native creating the
event object and passing it as the first and only parameter to the anonymous function we bind to
the onEndEditing property.
Because the event originates on a native iOS UI element, it first exists in the realm of compiled
Objective-C code. Again, its the RCTRootView bridge that takes care of connecting native code
and JavaScript code. It transforms the iOS event into a JavaScript event, so to speak. React Native
wraps the UI event into a SyntheticEvent (thats a class React Native ships with). The same happens
in the classical React library used for web development: browser events get translated into React
SyntheticEvents.
.
Creating a first real app: BookBrowser 41
In case you are curious, this is how the event object looks like:
{
"dispatchConfig": {
"phasedRegistrationNames": {
"bubbled":"onEndEditing",
"captured":"onEndEditingCapture"
}
},
"dispatchMarker":".r[1]{TOP_LEVEL}[0].2",
"nativeEvent": {
"target":7,
"text":"deedde"
},
"target":7,
"currentTarget":".r[1]{TOP_LEVEL}[0].2",
"timeStamp":1430810753006,
"_dispatchIDs":".r[1]{TOP_LEVEL}[0].2"
}
Once the event has been wrapped into a usable form and has been passed down into our own code,
we can react to it and do something useful with it. Right now, we log it. Were do log messages end
up?
You will also receive helpful warning messages from React Native itself, like this one:
Whoops, lets fix that. We pass a string to the enablesReturnKeyAutomatically attribute of our
TextInput field, but it expects a boolean. How can we pass a boolean? Thats simple, we just use
a JavaScript expression:
Creating a first real app: BookBrowser 42
<TextInput
placeholder="e.g. JavaScript or Mobile"
returnKeyType="search"
enablesReturnKeyAutomatically={true}
onEndEditing={ event => console.log(event.nativeEvent.text) }
style={styles.textInput}/>
Reading log messages in Xcode does the job, but its not really convenient. Good news: you can
get them into the development console of your browser, just as you would while developing for the
web.
Heres how:
Now a new tab is opened in Chrome, which tells us what to do: Hit --J (thats cmd-alt-J), and
the Chrome Developer Tools are opened. There, in the Console view, we can now see the debug log
messages from our React Native app.
The result of this setup should look like this:
Creating a first real app: BookBrowser 43
Once again its the React packager that makes things possible. Our app holds a connection to the
packager webserver on port 8081 and sends its log messages there, and the page that opened in
Chrome is also pointing at this server in order to retrieve the messages and pipe them into the
Developer Tools console.
App Chrome
^ ^
Receivelog messages
ReadJS
Sendlog messages
v
React packager
Note that when Chrome debugging is enabled, debug messages will be sent only to Chrome, and
will now longer appear inside Xcode.
.
In addition to logging with console.log, you can also log messages with other severities: use
console.debug, console.info, console.warn and console.error. Also, you can log objects in a more
structured way. In our example, replace
console.log(event.nativeEvent.text)
with
console.dir(event.nativeEvent)
on line 26 and the Developer Toolbar console will allow you to browse the structure and content
of the event.nativeEvent object - something that Xcode cant do at all.
.
Live reloading
When you opened the debug menu in the simulator using the virtual shake gesture, you probably
noticed another menu item: Enable Live Reload. This is a really nifty feature that classical iOS
developers can only dream of. When enabled, its enough to simply save the index.ios.js file in your
editor - the app will reload automatically. This really shines when working on the stylesheet - you
change a value, e.g. a margin, and upon save, you immediately see the layout change in the actual
app. This allows for very rapid app layouting in a very direct feedback loop.
Right now, BookBrowser is a React object that renders a View component. We need to rewrite it into
an object that renders a NavigatorIOS component instead, and this component needs to be set up
with our search screen view.
To do so, we first need to rename the variable that holds our search screen View, like so:
We can now recreate the BookBrowser object, making it carry a NavigatorIOS component that
references the SearchScreen object as its initial route:
Not too complicated, is it? Instead of rendering a <View> component, we render a <NavigatorIOS>
component, and we pass our SearchScreen object via the initialRoute attribute. We also define a title
for this route, which becomes the headline of our screen.
Like all React Native view components, the <NavigatorIOS> component needs a style, and of course
we need to declare it before accessing it, which results in the following code:
Creating a first real app: BookBrowser 46
1 'use strict';
2
3 var React = require('react-native');
4 var {
5 AppRegistry,
6 View,
7 Text,
8 TextInput,
9 StyleSheet,
10 NavigatorIOS,
11 } = React;
12
13 var SearchScreen = React.createClass({
14 render: function() {
15 return (
16 <View style={styles.container}>
17 <Text style={styles.headline}>
18 BookBrowser
19 </Text>
20 <Text style={styles.label}>
21 Find books containing:
22 </Text>
23 <TextInput
24 placeholder="e.g. JavaScript or Mobile"
25 returnKeyType="search"
26 enablesReturnKeyAutomatically={true}
27 onEndEditing={ event => console.log(event.nativeEvent.text) }
28 style={styles.textInput}/>
29 </View>
30 );
31 }
32 });
33
34 var BookBrowser = React.createClass({
35 render: function() {
36 return (
37 <NavigatorIOS
38 initialRoute={{
39 component: SearchScreen,
40 title: 'Search',
41 }}
42 style={styles.navContainer}
43 />
44 );
45 }
46 });
47
48 var styles = StyleSheet.create({
49 navContainer: {
50 flex: 1,
51 },
52 container: {
53 flex: 1,
54 flexDirection: 'column',
55 justifyContent: 'center',
56 alignItems: 'center',
Creating a first real app: BookBrowser 47
57 backgroundColor: '#5AC8FA',
58 },
59 headline: {
60 fontSize: 36,
61 fontWeight: 'bold',
62 color: '#fff',
63 marginBottom: 28,
64 },
65 label: {
66 fontSize: 24,
67 fontWeight: 'normal',
68 color: '#fff',
69 marginBottom: 8,
70 },
71 textInput: {
72 borderColor: '#8E8E93',
73 borderWidth: 0.5,
74 backgroundColor: '#fff',
75 height: 40,
76 marginLeft: 60,
77 marginRight: 60,
78 padding: 8,
79 }
80 });
81
82 AppRegistry.registerComponent('BookBrowser', () => BookBrowser);
Now that we began introducing a logical structure into our code, we can as well start to give it more
physical structure, too. It makes sense to keep each of our screens in its own file, and keep index.ios.js
as the entry point for the application that sets up the navigation structure.
Start by creating a new file called SearchScreen.js, in the root folder of the project:
1 'use strict';
2
3 var React = require('react-native');
4 var {
5 View,
6 Text,
7 TextInput,
8 StyleSheet,
9 } = React;
10
11 var SearchScreen = React.createClass({
12 render: function() {
13 return (
14 <View style={styles.container}>
15 <Text style={styles.headline}>
16 BookBrowser
17 </Text>
18 <Text style={styles.label}>
19 Find books containing:
20 </Text>
21 <TextInput
Creating a first real app: BookBrowser 48
This file only contains what is needed to render the search screen: We import the React Native
components that are used in the screen, we define the SearchScreen object with the JSX that describes
the structure of this screen, and we declare the stylesheet object that is referenced from the JSX.
Finally, we export the SearchScreen object, which allows to load and reference it from other files
via require. Which is what we do in our now much shorter index.ios.js:
Creating a first real app: BookBrowser 49
1 'use strict';
2
3 var React = require('react-native');
4 var {
5 AppRegistry,
6 StyleSheet,
7 NavigatorIOS,
8 } = React;
9
10 var SearchScreen = require('./SearchScreen');
11
12 var BookBrowser = React.createClass({
13 render: function() {
14 return (
15 <NavigatorIOS
16 initialRoute={{
17 component: SearchScreen,
18 title: 'Search',
19 }}
20 style={styles.navContainer}
21 />
22 );
23 }
24 });
25
26 var styles = StyleSheet.create({
27 navContainer: {
28 flex: 1,
29 },
30 });
31
32 AppRegistry.registerComponent('BookBrowser', () => BookBrowser);
If you restart the app, nothing has changed. Code now comes from two different source files, but
the React packager resolves this for us. It recognizes that index.ios.js requires a file named Search-
Screen.js, and pulls the content of this file into the bundle it serves at http://localhost:8081/index.ios.bundle.
1 'use strict';
2
3 var React = require('react-native');
4 var {
5 View,
6 Text,
7 StyleSheet,
8 } = React;
9
10 var ResultsScreen = React.createClass({
11 render: function() {
12 return (
13 <View style={styles.container}>
14 <Text style={styles.label}>
15 This is the results screen
16 </Text>
17 </View>
18 );
19 }
20 });
21
22 var styles = StyleSheet.create({
23 container: {
24 flex: 1,
25 flexDirection: 'column',
26 justifyContent: 'center',
27 alignItems: 'center',
28 backgroundColor: '#5AC8FA',
29 },
30 label: {
31 fontSize: 24,
32 fontWeight: 'normal',
33 color: '#fff',
34 },
35 });
36
37 module.exports = ResultsScreen;
The whole logic that brings us from the Search screen to the Results screen lives in the SearchScreen.js
file. We need to extend it with three parts: We need to require the ResultsScreen code, we need to
add a function that triggers the navigation to the next screen, and we need to wire this function to
the event that fires when the text input field is submitted. The result looks like this:
Creating a first real app: BookBrowser 51
1 'use strict';
2
3 var React = require('react-native');
4 var {
5 View,
6 Text,
7 TextInput,
8 StyleSheet,
9 } = React;
10
11 var ResultsScreen = require('./ResultsScreen');
12
13 var SearchScreen = React.createClass({
14
15 gotoResultsScreen: function() {
16 this.props.navigator.push({
17 title: 'Results',
18 component: ResultsScreen,
19 });
20 },
21
22 render: function() {
23 return (
24 <View style={styles.container}>
25 <Text style={styles.headline}>
26 BookBrowser
27 </Text>
28 <Text style={styles.label}>
29 Find books containing:
30 </Text>
31 <TextInput
32 placeholder="e.g. JavaScript or Mobile"
33 returnKeyType="search"
34 enablesReturnKeyAutomatically={true}
35 onEndEditing={ event => this.gotoResultsScreen() }
36 style={styles.textInput}/>
37 </View>
38 );
39 }
40
41 });
42
43 var styles = StyleSheet.create({
44 container: {
45 flex: 1,
46 flexDirection: 'column',
47 justifyContent: 'center',
48 alignItems: 'center',
49 backgroundColor: '#5AC8FA',
50 },
51 headline: {
52 fontSize: 36,
53 fontWeight: 'bold',
54 color: '#fff',
55 marginBottom: 28,
56 },
Creating a first real app: BookBrowser 52
57 label: {
58 fontSize: 24,
59 fontWeight: 'normal',
60 color: '#fff',
61 marginBottom: 8,
62 },
63 textInput: {
64 borderColor: '#8E8E93',
65 borderWidth: 0.5,
66 backgroundColor: '#fff',
67 height: 40,
68 marginLeft: 60,
69 marginRight: 60,
70 padding: 8,
71 }
72 });
73
74 module.exports = SearchScreen;
<NavigatorIOS
initialRoute={{
component: SearchScreen,
title: 'Search',
passProps: { placeholder: 'javascript' },
}}
style={styles.navContainer}
/>
With this, our SearchScreen component now has another prop named placeholder, with a value of
javascript. Within the SearchScreen component, we can access and use it like so:
<TextInput
placeholder={this.props.placeholder}
returnKeyType="search"
enablesReturnKeyAutomatically={true}
onEndEditing={ event => this.gotoResultsScreen() }
style={styles.textInput}/>
Of course, we can also access it outside the context of our JSX, as we did with the navigator prop
inside our gotoResultsScreen function.
Passing static values as props is only mildly interesting. Its much more useful when passing dynamic
values - the props mechanism is the perfect way to pass the search phrase from the Search screen to
the Results screen. To do so, change the gotoResultsScreen function as follows, in file SearchScreen.js
on lines 15 to 21:
gotoResultsScreen: function(searchPhrase) {
this.props.navigator.push({
title: 'Results',
component: ResultsScreen,
passProps: { 'searchPhrase': searchPhrase }
});
},
and on line 36, pass the text input content to the function:
With this, our ResultsScreen component receives a new prop named searchPhrase, which we can
use in the render JSX in file ResultsScreen.js like this:
Creating a first real app: BookBrowser 54
<View style={styles.container}>
<Text style={styles.label}>
This is the results screen
</Text>
<Text style={styles.label}>
You searched for: {this.props.searchPhrase}
</Text>
</View>
Reload once again, and the result is that not only can we navigate between screens, we can also pass
data between them.
fetch(url).then(function(response) {
return response.json();
}).then(function(jsonData) {
console.dir(jsonData);
}).catch(function(error) {
console.dir(error);
});
As we can use ES2015 features like arrow functions in React Native, we can shorten that even more:
fetch(url)
.then(response => response.json())
.then(jsonData => console.log(jsonData))
.catch(error => console.log(error));
fetch works asynchronously, and handles success cases as well as errors. If everything works fine, we
receive an HTTP response, which is passed to the first then block, and we can pull the JSON payload
from the response, which is passed to the second then block, where we can handle the received JSON
data as we see fit.
In case of network problems or other errors, the catch block triggers and receives an error object,
and we have the chance to handle this case, too.
Lets now see how we can integrate this pattern into our Results screen. We need to extend
file ResultsScreen.js by adding the buildUrl function, add the code that does the fetching, and
trigger the fetching operation when the screen is loaded. The natural place for the latter is the
componentDidMount function that every React Native component automatically triggers when it is
put on the screen.
1 'use strict';
2
3 var React = require('react-native');
4 var {
5 View,
6 Text,
7 StyleSheet,
8 } = React;
9
10 var buildUrl = function(q) {
11 return 'https://www.googleapis.com/books/v1/volumes?q='
12 + encodeURIComponent(q)
13 + '&langRestrict=en&maxResults=40';
14 };
15
16 var ResultsScreen = React.createClass({
17
18 componentDidMount: function() {
19 this.fetchResults(this.props.searchPhrase);
20 },
21
22 fetchResults: function(searchPhrase) {
Creating a first real app: BookBrowser 56
23 fetch(buildUrl(searchPhrase))
24 .then(response => response.json())
25 .then(jsonData => console.dir(jsonData))
26 .catch(error => console.dir(error));
27 },
28
29 render: function() {
30 return (
31 <View style={styles.container}>
32 <Text style={styles.label}>
33 This is the results screen
34 </Text>
35 <Text style={styles.label}>
36 You searched for: {this.props.searchPhrase}
37 </Text>
38 </View>
39 );
40 }
41 });
42
43 var styles = StyleSheet.create({
44 container: {
45 flex: 1,
46 flexDirection: 'column',
47 justifyContent: 'center',
48 alignItems: 'center',
49 backgroundColor: '#5AC8FA',
50 },
51 label: {
52 fontSize: 24,
53 fontWeight: 'normal',
54 color: '#fff',
55 },
56 });
57
58 module.exports = ResultsScreen;
Because the buildUrl function provides generic functionality that isnt specific to the UI, Ive made
it a generic function in the scope of the file, while the fetchResults function will be very specific to
its UI component, which is why Ive made it a method of the component.
.
When running the updated app with Chrome Debugging enabled, you will see a results object with
an array of found books in the items array in the Developer Tools Console tab after starting a search.
Lets take a moment to look at the data and its structure that we receive from the Google Books
API:
.
Creating a first real app: BookBrowser 57
As you can see, its relatively straightforward. We receive an object with an attribute items, which
is an array of objects, one for each found book.
Each item object has several attributes - for now, we are mostly interested in volumeInfo.title,
volumeInfo.subtitle, and volumeInfo.imageLinks.smallThumbnail.
Of course we dont want to log the results, we want to display a list of books on the screen.
The best way to present the results is by using a ListView element. A ListView is the scrollable table
of items we all know from iOS apps like Music or Mails.
Creating a first real app: BookBrowser 58
Component state
With the introduction of a ListView, we also need to introduce state into our application. While
props are used to initialize components, allowing us to pass data from one component to another
(e.g. from the search text input field to the search results screen), we need state in order to manage
data that exists and changes during the lifetime of a component.
In our case, the list of books we retrieve from the Google Books API makes up the state of the
ListView that is used to present that list to the user. When we switch to the results screen, this list
is empty, and it is filled with the results from the API call once that call is finished.
One other piece of state we need to manage in the results screen code is the information about the
status of the API fetch operation itself - in order to create a good user experience, we should display
a Loading, please wait message while results are retrieved, because on mobile devices in mobile
networks, the operation might take some time. The underlying state - are we currently loading, or
are we done loading? - needs to be managed. Depending on the loading status, we either render the
loading message, or we present the results.
Lets start with that.
State can be initialized, it can be changed, and it can be read. All of this is done in the following
code:
1 'use strict';
2
3 var React = require('react-native');
4 var {
5 View,
6 Text,
7 StyleSheet,
8 } = React;
9
10 var buildUrl = function(q) {
11 return 'https://www.googleapis.com/books/v1/volumes?q='
12 + encodeURIComponent(q)
13 + '&langRestrict=en&maxResults=40';
14 };
15
16 var ResultsScreen = React.createClass({
17 getInitialState: function() {
18 return {
19 isLoading: true,
20 };
21 },
22
23 componentDidMount: function() {
24 this.fetchResults(this.props.searchPhrase);
25 },
26
27 fetchResults: function(searchPhrase) {
28 fetch(buildUrl(searchPhrase))
Creating a first real app: BookBrowser 59
85 module.exports = ResultsScreen;
On line 17, we add the function getInitialState. This is where we can set starting values for all state
variables of the ResultsScreen component. In this case, we create a state variable named isLoading,
which defaults to true because when the screen appears, we have yet to load the books matching
the users search.
On line 31, in the anonymous function that is triggered when the fetch operation is done, we change
the value of the state variable to false.
Now we can react to the value of our state variable - we want to render different contents depending
on the value of isLoading.
To do so, two new dedicated rendering methods are created: on line 45, renderLoadingMessage has
the JSX that displays a waiting message to the user. On line 58, renderResults has been created - its
still just a placeholder for the actual results list which we will build in a moment.
Our actual render method on line 37 now simply refers to one of these depending on the value of
this.state.isLoading, our state variable. As a result, when initiating a search, we first see the Please
wait message, which is replaced with the placeholder results view when loading has been finished.
The Please wait message will disappear really quickly if you have a very fast internet connection.
In this case, you can fake a longer load time by utilizing a timeout:
fetchResults: function(searchPhrase) {
fetch(buildUrl(searchPhrase))
.then(response => response.json())
.then(jsonData => {
setTimeout(() => {
this.setState({isLoading: false});
}, 2000);
console.dir(jsonData);
})
.catch(error => console.dir(error));
},
The important takeaway here is that these state variables are not just ordinary variables. They come
with underlying mechanisms - our application reacts to changes in their value!
This becomes apparent when we try to achieve the same functionality with a plain old variable:
Creating a first real app: BookBrowser 61
1 'use strict';
2
3 var React = require('react-native');
4 var {
5 View,
6 Text,
7 StyleSheet,
8 } = React;
9
10 var buildUrl = function(q) {
11 return 'https://www.googleapis.com/books/v1/volumes?q='
12 + encodeURIComponent(q)
13 + '&langRestrict=en&maxResults=40';
14 };
15
16 var isLoading = true;
17
18 var ResultsScreen = React.createClass({
19 componentDidMount: function() {
20 this.fetchResults(this.props.searchPhrase);
21 },
22
23 fetchResults: function(searchPhrase) {
24 fetch(buildUrl(searchPhrase))
25 .then(response => response.json())
26 .then(jsonData => {
27 isLoading = false;
28 console.dir(jsonData);
29 })
30 .catch(error => console.dir(error));
31 },
32
33 render: function() {
34 if (isLoading) {
35 return this.renderLoadingMessage();
36 } else {
37 return this.renderResults();
38 }
39 },
40
41 renderLoadingMessage: function() {
42 return (
43 <View style={styles.container}>
44 <Text style={styles.label}>
45 Searching for "{this.props.searchPhrase}".
46 </Text>
47 <Text style={styles.label}>
48 Please wait...
49 </Text>
50 </View>
51 );
52 },
53
54 renderResults: function() {
55 return (
56 <View style={styles.container}>
Creating a first real app: BookBrowser 62
57 <Text style={styles.label}>
58 Finished searching.
59 </Text>
60 </View>
61 );
62 },
63
64 });
65
66 var styles = StyleSheet.create({
67 container: {
68 flex: 1,
69 flexDirection: 'column',
70 justifyContent: 'center',
71 alignItems: 'center',
72 backgroundColor: '#5AC8FA',
73 },
74 label: {
75 fontSize: 24,
76 fontWeight: 'normal',
77 color: '#fff',
78 },
79 });
80
81 module.exports = ResultsScreen;
On line 16, we define the variable, on line 27, we change its value, and on line 34, we read it.
But if you run this version of the code, the results screen will never switch to the Finished searching
view. React simply doesnt have an eye on the isLoading variable, because its not part of its state.
<ListView
dataSource={this.state.dataSource}
renderRow={this.renderBook}
style={styles.listView}
/>
With these three attributes, the ListView is able to do its job. Like other React Native UI elements, a
ListView has a style attribute. dataSource references a variable that holds all items of the list, one for
each row. renderRow references a rendering function that generates the view for one row of the list
- it is called for each row that is currently visible on the screen, and the item from the dataSource
list that belongs to that row is passed as a parameter:
Creating a first real app: BookBrowser 63
dataSource item #1
dataSource item #2
dataSource item #3
dataSource item #9
In the above illustration, the ListView holds a dataSource list with 9 items, but only items 4-8 are
currently within the borders of the device screen, and thus the renderRow function is called for
these items only. This way even a list with several thousand items can be rendered to the screen
efficiently.
The dataSource variable isnt a simple array - its a specialized object that needs to be created by
creating an instance of ListView.DataSource. As with our isLoading state variable, this needs to be
done in the getInitialState method:
getInitialState: function() {
return {
isLoading: true,
dataSource: new ListView.DataSource({
rowHasChanged: (row1, row2) => row1 !== row2,
}),
};
},
As you can see, we pass a rowHasChanged method definition. The ListView calls this in order to
find out if the content of a row has changed, and it only re-renders rows that did change, making
the process more efficient.
In the fetchResults function, where we currently only log the results of the API call, we can now use
the results to update the dataSource state variable:
Creating a first real app: BookBrowser 64
fetchResults: function(searchPhrase) {
fetch(buildUrl(searchPhrase))
.then(response => response.json())
.then(jsonData => {
this.setState({
isLoading: false,
dataSource: this.state.dataSource.cloneWithRows(jsonData.items)
});
})
.catch(error => console.dir(error));
},
Again, dataSource isnt a simple array, and we therefore cannot simply assign it a new value - we
need to do this via its cloneWithRows method.
Now that we have taken care of initializing and updating the ListView dataSource, we can replace the
existing renderResults method with one that renders the ListView, and add the renderBook method
that the ListView uses:
renderResults: function() {
return (
<ListView
dataSource={this.state.dataSource}
renderRow={this.renderBook}
style={styles.listView}
/>
);
},
renderBook: function(book) {
return (
<View style={styles.row}>
<Text style={styles.title}>
{book.volumeInfo.title}
</Text>
<Text style={styles.subtitle}>
{book.volumeInfo.subtitle}
</Text>
</View>
);
}
Last but not least, we should add some minimal styling for the new components:
Creating a first real app: BookBrowser 65
With all of these changes in place, the ResultsScreen.js file now looks like this:
1 'use strict';
2
3 var React = require('react-native');
4 var {
5 View,
6 ListView,
7 Text,
8 StyleSheet,
9 } = React;
10
11 var buildUrl = function(q) {
12 return 'https://www.googleapis.com/books/v1/volumes?q='
13 + encodeURIComponent(q)
14 + '&langRestrict=en&maxResults=40';
Creating a first real app: BookBrowser 66
15 };
16
17 var ResultsScreen = React.createClass({
18 getInitialState: function() {
19 return {
20 isLoading: true,
21 dataSource: new ListView.DataSource({
22 rowHasChanged: (row1, row2) => row1 !== row2,
23 }),
24 };
25 },
26
27 componentDidMount: function() {
28 this.fetchResults(this.props.searchPhrase);
29 },
30
31 fetchResults: function(searchPhrase) {
32 fetch(buildUrl(searchPhrase))
33 .then(response => response.json())
34 .then(jsonData => {
35 this.setState({
36 isLoading: false,
37 dataSource: this.state.dataSource.cloneWithRows(jsonData.items)
38 });
39 console.dir(jsonData.items);
40 })
41 .catch(error => console.dir(error));
42 },
43
44 render: function() {
45 if (this.state.isLoading) {
46 return this.renderLoadingMessage();
47 } else {
48 return this.renderResults();
49 }
50 },
51
52 renderLoadingMessage: function() {
53 return (
54 <View style={styles.container}>
55 <Text style={styles.label}>
56 Searching for "{this.props.searchPhrase}".
57 </Text>
58 <Text style={styles.label}>
59 Please wait...
60 </Text>
61 </View>
62 );
63 },
64
65 renderResults: function() {
66 return (
67 <ListView
68 dataSource={this.state.dataSource}
69 renderRow={this.renderBook}
70 style={styles.listView}
Creating a first real app: BookBrowser 67
71 />
72 );
73 },
74
75 renderBook: function(book) {
76 return (
77 <View style={styles.row}>
78 <Text style={styles.title}>
79 {book.volumeInfo.title}
80 </Text>
81 <Text style={styles.subtitle}>
82 {book.volumeInfo.subtitle}
83 </Text>
84 </View>
85 );
86 }
87
88 });
89
90 var styles = StyleSheet.create({
91 container: {
92 flex: 1,
93 flexDirection: 'column',
94 justifyContent: 'center',
95 alignItems: 'center',
96 backgroundColor: '#5AC8FA',
97 },
98 label: {
99 fontSize: 24,
100 fontWeight: 'normal',
101 color: '#fff',
102 },
103 listView: {
104 },
105 row: {
106 flex: 1,
107 flexDirection: 'column',
108 justifyContent: 'center',
109 alignItems: 'center',
110 backgroundColor: '#5AC8FA',
111 paddingTop: 20,
112 paddingBottom: 20,
113 paddingLeft: 20,
114 paddingRight: 20,
115 marginTop: 1,
116 },
117 title: {
118 fontSize: 20,
119 fontWeight: 'bold',
120 color: '#fff',
121 },
122 subtitle: {
123 fontSize: 16,
124 fontWeight: 'normal',
125 color: '#fff',
126 }
Creating a first real app: BookBrowser 68
127 });
128
129 module.exports = ResultsScreen;
This marks an important step on our way to a fully working app. We can now search for books and
get a list of matched titles as a result.
Displaying images
Lets try to improve the UI a bit by adding the image of the book cover next to each title and subtitle.
This requires the following changes:
Below line 7, add the Image component variable:
var {
View,
ListView,
Text,
Image,
StyleSheet,
} = React;
In the renderBook method on line 76 and following, add an <Image> element that references the
thumbnail URL from the API results, and put the title and subtitle text elements into a new View
block that references the rightContainer style:
renderBook: function(book) {
return (
<View style={styles.row}>
<Image
style={styles.thumbnail}
source={{uri: book.volumeInfo.imageLinks.smallThumbnail}}
/>
<View style={styles.rightContainer}>
<Text style={styles.title}>
{book.volumeInfo.title}
</Text>
<Text style={styles.subtitle}>
{book.volumeInfo.subtitle}
</Text>
</View>
</View>
);
}
Creating a first real app: BookBrowser 69
On line 111 and following, insert a style definition for rightContainer, change the flexDirection of
the row style from column to row, and remove the paddingTop, -Bottom, and -Left attributes:
rightContainer: {
flex: 1,
},
row: {
flex: 1,
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#5AC8FA',
paddingRight: 20,
marginTop: 1,
},
flex: 1 is all it needs for rightContainer because the default values for flexDirection, justifyContent
and alignItems are sufficient.
.
thumbnail: {
width: 70,
height: 108,
marginRight: 16,
}
With these changes in place, your results screen should look like this:
Creating a first real app: BookBrowser 70
To do so, we first create another method on the ResultsScreen object called showBookDetails:
showBookDetails: function(book) {
this.props.navigator.push({
title: book.volumeInfo.title,
component: BookDetails,
passProps: {book}
});
}
The concept put to use here is nothing new - we push a new screen on the navigator route hierarchy,
and we pass the book object as a prop. The BookDetails component is not yet in place, we will create
it in a moment.
The showBookDetails method needs to be called when the user taps on a row in the results list. In
order to handle touch events in our app, we need the TouchableHighlight component. Using the
<TouchableHighlight> element, we simply wrap the screen element that should be touchable, and
connect it to our showBookDetails method. The most logical place to do this is the renderBook JSX
definition:
renderBook: function(book) {
return (
<TouchableHighlight onPress={() => this.showBookDetails(book)}>
<View style={styles.row}>
<Image
style={styles.thumbnail}
source={{uri: book.volumeInfo.imageLinks.smallThumbnail}}
/>
<View style={styles.rightContainer}>
<Text style={styles.title}>
{book.volumeInfo.title}
</Text>
<Text style={styles.subtitle}>
{book.volumeInfo.subtitle}
</Text>
</View>
</View>
</TouchableHighlight>
);
},
As you can see, we made the connection between the <TouchableHighlight> element and the
showBookDetails method by defining an anonymous function call on the onPress attribute, and
we pass the book object that was previously passed into the renderBook method. Last but not least,
we need to define the TouchableHighlight component at the beginning of the file, and we need
to require the yet-to-be-written code file that contains the BookDetails component - with this, the
updated ResultsScreen.js file looks like this:
Creating a first real app: BookBrowser 72
1 'use strict';
2
3 var React = require('react-native');
4 var {
5 View,
6 ListView,
7 Text,
8 Image,
9 TouchableHighlight,
10 StyleSheet,
11 } = React;
12
13 var BookDetails = require('./BookDetails');
14
15 var buildUrl = function(q) {
16 return 'https://www.googleapis.com/books/v1/volumes?q='
17 + encodeURIComponent(q)
18 + '&langRestrict=en&maxResults=40';
19 };
20
21 var ResultsScreen = React.createClass({
22 getInitialState: function() {
23 return {
24 isLoading: true,
25 dataSource: new ListView.DataSource({
26 rowHasChanged: (row1, row2) => row1 !== row2,
27 }),
28 };
29 },
30
31 componentDidMount: function() {
32 this.fetchResults(this.props.searchPhrase);
33 },
34
35 fetchResults: function(searchPhrase) {
36 fetch(buildUrl(searchPhrase))
37 .then(response => response.json())
38 .then(jsonData => {
39 this.setState({
40 isLoading: false,
41 dataSource: this.state.dataSource.cloneWithRows(jsonData.items)
42 });
43 console.dir(jsonData.items);
44 })
45 .catch(error => console.dir(error));
46 },
47
48 render: function() {
49 if (this.state.isLoading) {
50 return this.renderLoadingMessage();
51 } else {
52 return this.renderResults();
53 }
54 },
55
56 renderLoadingMessage: function() {
Creating a first real app: BookBrowser 73
57 return (
58 <View style={styles.container}>
59 <Text style={styles.label}>
60 Searching for "{this.props.searchPhrase}".
61 </Text>
62 <Text style={styles.label}>
63 Please wait...
64 </Text>
65 </View>
66 );
67 },
68
69 renderResults: function() {
70 return (
71 <ListView
72 dataSource={this.state.dataSource}
73 renderRow={this.renderBook}
74 style={styles.listView}
75 />
76 );
77 },
78
79 renderBook: function(book) {
80 return (
81 <TouchableHighlight onPress={() => this.showBookDetails(book)}>
82 <View style={styles.row}>
83 <Image
84 style={styles.thumbnail}
85 source={{uri: book.volumeInfo.imageLinks.smallThumbnail}}
86 />
87 <View style={styles.rightContainer}>
88 <Text style={styles.title}>
89 {book.volumeInfo.title}
90 </Text>
91 <Text style={styles.subtitle}>
92 {book.volumeInfo.subtitle}
93 </Text>
94 </View>
95 </View>
96 </TouchableHighlight>
97 );
98 },
99
100 showBookDetails: function(book) {
101 this.props.navigator.push({
102 title: book.volumeInfo.title,
103 component: BookDetails,
104 passProps: {book}
105 });
106 }
107
108 });
109
110 var styles = StyleSheet.create({
111 container: {
112 flex: 1,
Creating a first real app: BookBrowser 74
The last missing piece is the BookDetails screen. Here is the full code, which is explained in detail
afterwards:
Creating a first real app: BookBrowser 75
1 'use strict';
2
3 var React = require('react-native');
4 var {
5 View,
6 ScrollView,
7 Text,
8 Image,
9 StyleSheet,
10 } = React;
11
12 var BookDetails = React.createClass({
13 render: function() {
14 console.log(this.props.book.volumeInfo.description);
15 return (
16 <ScrollView>
17 <View style={styles.topContainer}>
18 <Image
19 style={styles.thumbnail}
20 source={{uri: this.props.book.volumeInfo.imageLinks.smallThumbnail}}
21 />
22 <View style={styles.titlesContainer}>
23 <Text style={styles.title}>
24 {this.props.book.volumeInfo.title}
25 </Text>
26 <Text style={styles.subtitle}>
27 {this.props.book.volumeInfo.subtitle}
28 </Text>
29 </View>
30 </View>
31 <View style={styles.middleContainer}>
32 <Text style={styles.description}>
33 {this.props.book.volumeInfo.description}
34 </Text>
35 </View>
36 <View style={styles.bottomContainer}>
37 <Text style={styles.author}>
38 Author: {this.props.book.volumeInfo.authors[0]}
39 </Text>
40 </View>
41 </ScrollView>
42 );
43 }
44 });
45
46 var Dimensions = require('Dimensions');
47 var windowSize = Dimensions.get('window');
48
49 var styles = StyleSheet.create({
50 topContainer: {
51 flex: 1,
52 flexDirection: 'row',
53 justifyContent: 'flex-start',
54 alignItems: 'flex-start',
55 backgroundColor: '#5AC8FA',
56 },
Creating a first real app: BookBrowser 76
57 thumbnail: {
58 width: 70,
59 height: 108,
60 marginRight: 16,
61 },
62 titlesContainer: {
63 flex: 1,
64 flexDirection: 'column',
65 justifyContent: 'flex-start',
66 alignItems: 'flex-start',
67 backgroundColor: '#5AC8FA',
68 width: windowSize.width - 86,
69 paddingTop: 8,
70 paddingRight: 8,
71 },
72 title: {
73 fontSize: 20,
74 fontWeight: 'bold',
75 color: '#fff',
76 },
77 subtitle: {
78 fontSize: 16,
79 fontWeight: 'normal',
80 color: '#fff',
81 },
82 middleContainer: {
83 flex: 1,
84 flexDirection: 'column',
85 justifyContent: 'flex-start',
86 alignItems: 'flex-start',
87 backgroundColor: '#fff',
88 margin: 16,
89 },
90 description: {
91 fontFamily: 'Times',
92 fontSize: 16,
93 fontWeight: 'normal',
94 color: '#000',
95 marginBottom: 8,
96 },
97 bottomContainer: {
98 flex: 1,
99 flexDirection: 'column',
100 justifyContent: 'flex-start',
101 alignItems: 'flex-start',
102 backgroundColor: '#5AC8FA',
103 padding: 8,
104 },
105 author: {
106 fontSize: 16,
107 fontWeight: 'normal',
108 color: '#fff',
109 },
110 });
111
112 module.exports = BookDetails;
Creating a first real app: BookBrowser 77
Because this screen is relatively simple in that it only renders a static UI with contents from a
passed-in prop, the code shouldnt be too surprising by now.
However, two details need some explanation. First, we introduce yet another UI component, the
ScrollView. Because the description text of a book can be longer than the available screen space, we
need to wrap the whole UI into a special view that makes the UI scrollable - the ScrollView.
Second, within the UI, we display a slightly larger thumbail with the title and subtitle next to it
on the right. In contrast to very same layout within our ListView rows on the results screen, React
Native doesnt handle this as expected without our support. The problem is that titles and subtitles
that are longer than the available horizontal screen space wont break to the next line automatically.
You can demonstrate this if you delete line 68 and then, in the app, select a book with a long title.
In order to get the layout right, we need to calculate the available space for the View that wraps
the title and subtitle text (70 points for the thumbnail plus 16 points for its margin), and assign the
calculated space as the width of this view, via the titleContainer style definition.
To do so, we require the Dimensions component which can be used to retrieve the width and height
of the UI screen (which of course varies between different iPhone and iPad versions).
Investigation whether this is a bug in React Native or not is currently ongoing, and depending on
the outcome, this part of the book might receive an update in the future.
.
Conclusion
And with this, we have created a fully operational mobile app with remote API communication,
multiple screen navigation, touch handling, props and state, and much more.
But lets face it: There is still much work to be done - for now, our code very much depends on
happy path scenarios where the user doesnt try out strange things like entering search terms that
dont yield a result, where the network always works, the device is never rotated, and so on.
Thus, our next mission is to look into edge cases and make our app robust enough for actual use.
Improving the BookBrowser app
In order to make our app more robust, we need to look at the edge cases that might occur, and we
need to handle those gracefully.
The first and most obvious case the user might run into is that no network connection is available,
and therefore, the request to the Google Books API will fail.
Right now, this case has two effects: within the app itself, nothing happens - the user remains stuck
on the Please wait screen, without being notified about a problem (however, navigating back
to the search screen is still possible), and in Chrome Debug mode, a message like the following is
logged:
Lets see how and where in the code we can handle that.
In file ResultsScreen.js, on line 45, the method catch is used to define a function that is called if an
error occurs. Currently, we only use this to log the error. We need to integrate this error case into
our application workflow.
How exactly do we want to inform the user about the fact that an error occured? We could simply
add another text element to the screen, like this:
___
Searching for "foo".
Please wait...
A network error occured!
But thats not very sophisticated. An error scenario is a typical use case for a modal, that is, an
overlay on the screen that shows the error message, like this:
Also, deciding for a modal is a good excuse to learn how to add third-party React Native components
written by others.
In case you havent worked with NPM before, note that the above npm install command pulls
Brents code (and the dependencies of his code) into the node_modules folder of our project. The
save switch updates our package.json file, which now declares two dependencies, react-native
and react-native-modal:
{
"name": "BookBrowser",
"version": "0.0.1",
"private": true,
"scripts": {
"start": "node_modules/react-native/packager/packager.sh"
},
"dependencies": {
"react-native": "^0.8.0",
"react-native-modal": "^0.3.8"
}
}
His code is now available to the JavaScript side of our code, but we need to take some extra steps to
make the parts of his code that are related to Xcode known to our Xcode setup:
Switch to Xcode
Hit -1 or select View Navigators Show Project Navigator
In the leftmost pane of the main Xcode window, fold out the contents below BookBrowser*
by clicking on the triangular arrow
In the list of folders that appears, right-click on Libraries and select Add Files to Book-
Browser
In the file browser that opens, navigate to node_modules/react-native-modal, and select file
RNModal.xcodeproj
Improving the BookBrowser app 81
Now, in the leftmost pane, select the first entry (the one that reads BookBrowser and has a
blue icon in front of it)
In the center pane, switch from General to Build Phases
In the area below, fold out Link Binary With Libraries, and click the plus sign below the file
list
Thats it - the Modal component is now integrated with the Xcode part of our BookBrowser project,
and can be used from within the JavaScript part of the app. Which is what we are doing next.
We then initialize the showErrorModal parameter as a state variable on the ResultsScreen compo-
nent:
We now extend the catch branch of our fetch block, switching the modal to visible if an error occurs:
fetch(buildUrl(searchPhrase))
.then(response => response.json())
.then(jsonData => {
this.setState({
isLoading: false,
dataSource: this.state.dataSource.cloneWithRows(jsonData.items)
});
console.dir(jsonData.items);
})
.catch(error => {
this.setState({
showErrorModal: true
});
});
Last but not least, we extend the view that is returned by renderLoadingMessage with the modal
JSX element, which contains the error message:
renderLoadingMessage: function() {
return (
<View style={styles.container}>
<Text style={styles.label}>
Searching for "{this.props.searchPhrase}".
</Text>
<Text style={styles.label}>
Please wait...
</Text>
<Modal isVisible={this.state.showErrorModal}>
<Text>A network error occurred!</Text>
</Modal>
</View>
);
},
With all these changes in place, the complete file looks like this:
Improving the BookBrowser app 83
1 'use strict';
2
3 var React = require('react-native');
4 var Modal = require('react-native-modal');
5
6 var {
7 View,
8 ListView,
9 Text,
10 Image,
11 TouchableHighlight,
12 StyleSheet,
13 } = React;
14
15 var BookDetails = require('./BookDetails');
16
17 var buildUrl = function(q) {
18 return 'https://www.googleapis.com/books/v1/volumes?q='
19 + encodeURIComponent(q)
20 + '&langRestrict=en&maxResults=40';
21 };
22
23 var ResultsScreen = React.createClass({
24 getInitialState: function() {
25 return {
26 isLoading: true,
27 showErrorModal: false,
28 dataSource: new ListView.DataSource({
29 rowHasChanged: (row1, row2) => row1 !== row2,
30 }),
31 };
32 },
33
34 componentDidMount: function() {
35 this.fetchResults(this.props.searchPhrase);
36 },
37
38 fetchResults: function(searchPhrase) {
39 fetch(buildUrl(searchPhrase))
40 .then(response => response.json())
41 .then(jsonData => {
42 this.setState({
43 isLoading: false,
44 dataSource: this.state.dataSource.cloneWithRows(jsonData.items)
45 });
46 console.dir(jsonData.items);
47 })
48 .catch(error => {
49 this.setState({
50 showErrorModal: true
51 });
52 });
53 },
54
55 render: function() {
56 if (this.state.isLoading) {
Improving the BookBrowser app 84
57 return this.renderLoadingMessage();
58 } else {
59 return this.renderResults();
60 }
61 },
62
63 renderLoadingMessage: function() {
64 return (
65 <View style={styles.container}>
66 <Text style={styles.label}>
67 Searching for "{this.props.searchPhrase}".
68 </Text>
69 <Text style={styles.label}>
70 Please wait...
71 </Text>
72 <Modal isVisible={this.state.showErrorModal}>
73 <Text>A network error occurred!</Text>
74 </Modal>
75 </View>
76 );
77 },
78
79 renderResults: function() {
80 return (
81 <ListView
82 dataSource={this.state.dataSource}
83 renderRow={this.renderBook}
84 style={styles.listView}
85 />
86 );
87 },
88
89 renderBook: function(book) {
90 return (
91 <TouchableHighlight onPress={() => this.showBookDetails(book)}>
92 <View style={styles.row}>
93 <Image
94 style={styles.thumbnail}
95 source={{uri: book.volumeInfo.imageLinks.smallThumbnail}}
96 />
97 <View style={styles.rightContainer}>
98 <Text style={styles.title}>
99 {book.volumeInfo.title}
100 </Text>
101 <Text style={styles.subtitle}>
102 {book.volumeInfo.subtitle}
103 </Text>
104 </View>
105 </View>
106 </TouchableHighlight>
107 );
108 },
109
110 showBookDetails: function(book) {
111 this.props.navigator.push({
112 title: book.volumeInfo.title,
Improving the BookBrowser app 85
If you want to test these changes, you need to force the application to run into the catch branch
of the fetch operation. One way to do this is to disconnect the computer that runs the application
from the network. Another solution is to make the URL that is requested invalid. It is defined on
line 18 of file ResultsScreen.js. Simply change googleapis.com to googleapis.coom, for example.
.
renderModalButtons: function () {
return (
<View style={styles.modalButtonsContainer}>
<View style={styles.modalButton}>
<Text style={styles.modalButtonText}>< Go back</Text>
</View>
<View style={styles.modalButton}>
<Text style={styles.modalButtonText}>↻ Retry</Text>
</View>
</View>
);
},
Ok, thats relatively straight-forward - we return a <View> with two subviews for each of the two
buttons, which contain the text for the respective button.
Here is the styling for this component:
Improving the BookBrowser app 87
modalButtonsContainer: {
flex: 1,
flexDirection: 'row',
justifyContent: 'center',
top: 240
},
modalButton: {
borderColor: '#ffffff',
borderRadius: 4,
borderWidth: 1,
marginLeft: 20,
marginRight: 20,
paddingLeft: 20,
paddingRight: 20,
paddingTop: 10,
paddingBottom: 10,
},
modalButtonText: {
fontSize: 18,
color: '#ffffff',
}
This makes for a nice look, but we also need behaviour. When the user taps on the Go back button,
we should navigate her back to the previous screen, where a search keyword can be entered.
When the Retry button is tapped, we should start the current search anew - maybe the network
connectivity of the users device has returned because her train left the tunnel.
Lets first write the functions that perform these operations:
goBack: function() {
this.setState({
showErrorModal: false
});
this.props.navigator.pop();
},
retry: function() {
this.setState({
showErrorModal: false
});
this.fetchResults();
},
retry is really simple - after hiding the modal, the fetch operation is simply triggered again.
goBack is a bit more interesting. Remember how at the top level, our UI is wrapped in a NavigatorIOS
component, and how a reference to this component is passed as prop navigator to each sub-screen.
When adding and navigating to a sub-screen, it is pushed onto the internal stack of screens managed
by NavigatorIOS - thus, if we want to go back one screen, we simply pop from this stack.
How can we map the taps on the buttons to these functions? We already know the TouchableHigh-
light element - we use it to trigger behaviour when the user taps on a row in the list of results. This
Improving the BookBrowser app 88
could be used here, and it would work, but the highlight effect looks pretty ugly, rendering a black
box beneath the button that was tapped.
But luckily, TouchableHighlight has a sibling called TouchableOpacity. From the official documen-
tation:
On press down, the opacity of the wrapped view is decreased, dimming it.
Sounds good. Of course, TouchableOpacity also provides the onPress property we need to connect
our buttons with their methods:
renderModalButtons: function () {
return (
<View style={styles.modalButtonsContainer}>
<TouchableOpacity onPress={this.goBack}>
<View style={styles.modalButton}>
<Text style={styles.modalButtonText}>< Go back</Text>
</View>
</TouchableOpacity>
<TouchableOpacity onPress={this.retry}>
<View style={styles.modalButton}>
<Text style={styles.modalButtonText}>↻ Retry</Text>
</View>
</TouchableOpacity>
</View>
);
},
This results in the following ResultsScreen.js file (note that Ive also styled the modal body a bit):
1 'use strict';
2
3 var React = require('react-native');
4 var Modal = require('react-native-modal');
5
6 var {
7 View,
8 ListView,
9 Text,
10 Image,
11 TouchableHighlight,
12 TouchableOpacity,
13 StyleSheet,
14 } = React;
15
16 var BookDetails = require('./BookDetails');
17
18 var buildUrl = function(q) {
Improving the BookBrowser app 89
19 return 'https://www.googleapis.com/books/v1/volumes?q='
20 + encodeURIComponent(q)
21 + '&langRestrict=en&maxResults=40';
22 };
23
24 var ResultsScreen = React.createClass({
25 getInitialState: function() {
26 return {
27 isLoading: true,
28 showErrorModal: false,
29 dataSource: new ListView.DataSource({
30 rowHasChanged: (row1, row2) => row1 !== row2,
31 }),
32 };
33 },
34
35 componentDidMount: function() {
36 this.fetchResults(this.props.searchPhrase);
37 },
38
39 fetchResults: function(searchPhrase) {
40 fetch(buildUrl(searchPhrase))
41 .then(response => response.json())
42 .then(jsonData => {
43 this.setState({
44 isLoading: false,
45 dataSource: this.state.dataSource.cloneWithRows(jsonData.items)
46 });
47 console.dir(jsonData.items);
48 })
49 .catch(error => {
50 this.setState({
51 showErrorModal: true
52 });
53 });
54 },
55
56 goBack: function() {
57 this.setState({
58 showErrorModal: false
59 });
60 this.props.navigator.pop();
61 },
62
63 retry: function() {
64 this.setState({
65 showErrorModal: false
66 });
67 this.fetchResults();
68 },
69
70 render: function() {
71 if (this.state.isLoading) {
72 return this.renderLoadingMessage();
73 } else {
74 return this.renderResults();
Improving the BookBrowser app 90
75 }
76 },
77
78 renderModalButtons: function () {
79 return (
80 <View style={styles.modalButtonsContainer}>
81
82 <TouchableOpacity onPress={this.goBack}>
83 <View style={styles.modalButton}>
84 <Text style={styles.modalButtonText}>< Go back</Text>
85 </View>
86 </TouchableOpacity>
87
88 <TouchableOpacity onPress={this.retry}>
89 <View style={styles.modalButton}>
90 <Text style={styles.modalButtonText}>↻ Retry</Text>
91 </View>
92 </TouchableOpacity>
93
94 </View>
95 );
96 },
97
98 renderLoadingMessage: function() {
99 return (
100 <View style={styles.container}>
101 <Text style={styles.label}>
102 Searching for "{this.props.searchPhrase}".
103 </Text>
104 <Text style={styles.label}>
105 Please wait...
106 </Text>
107 <Modal isVisible={this.state.showErrorModal} customCloseButton={this.renderModalButtons()}>
108 <View style={styles.modalContainer}>
109 <Text style={styles.modalBody}>A network error occurred!</Text>
110 </View>
111 </Modal>
112 </View>
113 );
114 },
115
116 renderResults: function() {
117 return (
118 <ListView
119 dataSource={this.state.dataSource}
120 renderRow={this.renderBook}
121 style={styles.listView}
122 />
123 );
124 },
125
126 renderBook: function(book) {
127 return (
128 <TouchableHighlight onPress={() => this.showBookDetails(book)}>
129 <View style={styles.row}>
130 <Image
Improving the BookBrowser app 91
131 style={styles.thumbnail}
132 source={{uri: book.volumeInfo.imageLinks.smallThumbnail}}
133 />
134 <View style={styles.rightContainer}>
135 <Text style={styles.title}>
136 {book.volumeInfo.title}
137 </Text>
138 <Text style={styles.subtitle}>
139 {book.volumeInfo.subtitle}
140 </Text>
141 </View>
142 </View>
143 </TouchableHighlight>
144 );
145 },
146
147 showBookDetails: function(book) {
148 this.props.navigator.push({
149 title: book.volumeInfo.title,
150 component: BookDetails,
151 passProps: {book}
152 });
153 }
154
155 });
156
157 var styles = StyleSheet.create({
158 container: {
159 flex: 1,
160 flexDirection: 'column',
161 justifyContent: 'center',
162 alignItems: 'center',
163 backgroundColor: '#5AC8FA',
164 },
165 label: {
166 fontSize: 24,
167 fontWeight: 'normal',
168 color: '#fff',
169 },
170 listView: {
171 },
172 row: {
173 flex: 1,
174 flexDirection: 'row',
175 justifyContent: 'center',
176 alignItems: 'center',
177 backgroundColor: '#5AC8FA',
178 paddingRight: 20,
179 marginTop: 1,
180 },
181 rightContainer: {
182 flex: 1,
183 },
184 title: {
185 fontSize: 20,
186 fontWeight: 'bold',
Improving the BookBrowser app 92
Note how even though the modal covers the whole UI with its transparent dark background, the
< Search component on the navigator is still visible and touchable, resulting in the same behaviour
as clicking the < Go back button.
.
Improving the BookBrowser app 93
If you dont want this kind of duplication, pass the forceToFront={true} prop on the <Modal>
component, like this: