When news broke that React Native was open sourced on Friday, I felt elated. This was the moment I had been waiting for. Ever since the React.js conference videos introduced React Native in late January, I couldn't wait to get my hands on it.

If you don't know React Native, it's a new open source framework by Facebook that allows you to write iOS (and, eventually, Android) apps using HTML-like code (called JSX) and JavaScript. It's based on the popular React.js JavaScript framework, but you can't run normal React.js code directly as an iOS app. Instead, once you are familiar with the basics of React.js, you can use the same knowledge, but slightly different components, to assemble iOS and Android apps. They refer to it as "Learn once, write anywhere", as opposed to the "Write once, run anywhere" approach advocated by other frameworks, like PhoneGap. The paradigm employed by React Native allows it to use native iOS components, and bring about changes to the UI through a JavaScript thread running in the background. In theory, this means the experience should feel native and appear very smooth, even though it's powered by JavaScript. And I was about to find out whether that is true in practice.

I downloaded a copy, followed the instructions for creating an app, and within minutes after learning about the launch, I had my first React Native iOS app running.

iOS Simulator Screenshot - My first React Native iOS app

If the rest of the process were to be as easy as the start, I was in for a grand time. And React Native did not disappoint.

The first screen

I set out to build a simple app for learning simple Chinese phrases, a very basic spaced-repitition flashcard app. Using Sketch, I quickly came up with what I wanted the study screen to look like:

Card Revealed.png

With only that in hand, I started assembling components in React. First, the card:

var Card = React.createClass({
  render: function() {
    var card = this.props.card;

    return (
      <View style={styles.card}>
        <Text numberOfLines={2} ref="definition" style={styles.definition}>{card.definition}</Text>
        <Text numberOfLines={1} style={styles.chinese}>{this.props.simplified ? card.simplified : card.traditional}</Text>
        <Text ref="pinyin" numberOfLines={1} style={styles.pinyin}>{card.pinyin}</Text>
      </View>
    );
  },
});

I had never worked with flexbox before, but I found these tutorials particularly useful. React Native's flexbox implementation departs from the browser-equivalent in some small ways:

  • The shorthand form, e.g. flex: 1 100%, is not valid in React Native. Instead, specify the properties separately: flex: 1; width: 100%. Flex values can be integers or doubles (0, 0.3, 1.0 etc), indicating the relative size of the box. So, if you have multiple elements they will fill the relative proportion of the view based on their flex value. If you had two views, one with flex 0.25 and the other with flex 0.75, it would split into quarters. Or you could also get an identical effect using flex 25 and flex 75.
  • Item-based styling is limited: flex-basis, flex-shrink and flex-grow are not available. I hope this will change in the future, but as of now the workaround is to use container properties to achieve the same effects.

For the card, this is what my styles looked like at first:

  card: {
    flexDirection: 'column',
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: 'white',
    shadowColor: "black",
    shadowOpacity: 0.3,
    shadowRadius: 3,
    shadowOffset: {
      height: 0,
      width: 0
    },
    width: 290,
    height: 290,
  },
  definition: {
    fontSize: 30,
    textAlign: 'center',
  },
  chinese: {
    flex: 1,
    fontSize: 80,
  },
  pinyin: {
    flex: 1,
    fontSize: 40,
  },

No big surprises there.

There is a bug in the current version of React Native that causes shadows to not work on iPhone 5 and up, but hopefully that will be fixed soon. I might even take a stab at it myself.

The rest of the items on the screen were similarly created; buttons, progress bar, all were simple React components drawn onto the screen and styled by flexbox. If you are somewhat fimiliar with React.js, this should prove to be very easy.

Adding a ListView

With the study screen cleared, I decided to move on to adding a ListView, for choosing which deck of cards you would like to study. I didn't even design this in Sketch, I had a look at the Movies example to see how they did it there, and used a similar approach.

The important part is the render function, which returns something that looks like this:

return (
      <View style={{flex: 1, flexDirection: 'column'}}>
          {header}
          <ListView
            ref="listview"
            dataSource={this.state.dataSource}
            renderFooter={this.renderFooter}
            renderRow={this.renderRow}
            onEndReached={this.onEndReached}
            automaticallyAdjustContentInsets={false}
            keyboardDismissMode="onDrag"
            keyboardShouldPersistTaps={true}
            showsVerticalScrollIndicator={true}
            style={styles.list}
          />
      </View>
    );

Notice the this.renderRow and other functions. These I had to implement, but it was as easy as creating elements on the study screen. Here is what the renderRow method looks like:

  renderRow: function(deck: Object)  {
    return (
      <DeckCell
        onSelect={() => this.selectDeck(deck)}
        deck={deck}
      />
    );
  },

DeckCell is a simple component I defined to display the deck's title and the progress percentage, like this:

iOS Simulator Screen Shot Apr 1, 2015, 14.04.17.png

Upon selecting the DeckCell row, we call this.selectDeck(deck), which looks like this:

  selectDeck: function(deck){
    // onPress is what we do when the deck is selected. We bind it to the current scope.
    var onPress = function(){
        this.setState({loaded: false});
        DatabaseManager.loadDecks(this.loadDecks); // we will talk about this in a second
        DatabaseManager.loadProgress(this.loadProgress);
    }.bind(this);
    this.props.navigator.push({
      title: deck.title,
      component: StudyScreen, // the screen with the cards we created earlier
      passProps: {deck: deck, onPress: onPress},
    });
  }

With this in place, clicking on a deck in the list will load the study screen, sending it a deck object so that we know which deck is intended to be used. So far so good. The last step that remained was to hook up a database. This was the part I was really curious about!

Hooking up a database

After clearing the first hurdle of drawing some items onto the screen and setting up a ListView with dummy objects, my attention turned to hooking up a database and having it communicate with React Native. I noticed that there is an AsyncStorage class, but this is intended for simple key-value storage (like LocalStorage on the web). I needed to do some heavy lifting, so I turned to my trusty friend, SQLite. The question was just, how do I hook SQLite up to the React app? I already alluded to the answer in the previous code snippet.

I made use of the RCTBridgeModule interface described in the React Native docs. First, this meant defining a DatabaseManager.h header file:

#ifndef ReactCards_DatabaseManager_h
#define ReactCards_DatabaseManager_h

#import "RCTBridgeModule.h"
#import "DBManager.h"

@interface DatabaseManager : NSObject <RCTBridgeModule>
@property (nonatomic, strong) DBManager *dbManager;
@end

#endif

Next, I implemented an initialization function to load up the SQLite database and save the connection as an instance variable. The SQLite-specific code was kept in another library, SQLiteManager, which could easily be replaced with a wrapper for Core Data or Realm.


#import <Foundation/Foundation.h>
#import "SQLiteManager.h"
#import "RCTLog.h"
#import "SuperMemo.h"

@implementation DatabaseManager

- (id)init {
    return [self initWithDB:@"collections.db"];
}

- (id)initWithDB:(NSString*)databaseName
{
    self = [super init];
    if (self) {
        self.dbManager = [[SQLiteManager alloc] initWithDatabaseFilename:databaseName];
    }
    return self;
}

Now, I could define a loadDecks method that takes a callback function, and export it to the JavaScript code by putting a line at the top of the file

RCT_EXPORT_MODULE();

and adding a call to RCT_EXPORT(); on the first line the RCT_EXPORT_METHOD macro (RCT_EXPORT is now deprecated in favor of RCT_EXPORT_METHOD):


RCT_EXPORT_METHOD(loadDecks:(RCTResponseSenderBlock)callback
{    
    // returns an array of NSDictionary objects, with fields id, title, count and percentage
    NSArray *decks = [self.dbManager loadDecks];

    callback(@[[NSNull null], decks]);
}

I didn't expect this to work at all, but I'd gone this far, so there was no turning back. In the JavaScript code, I added lines to first import the DataManager class, and then call the loadDecks function, giving it a callback function the logs the result.

var DatabaseManager = require('NativeModules').DatabaseManager;
...
DatabaseManager.loadDecks(this.loadDecks);
...
loadDecks: function(error, decks){
  if (error) {
    console.error(error);
  } else {
    console.log(decks);
  }
}

To my own astonishment, it worked! On first try! In the logs, I could see:

RCTJSLog> [{"title":"Descriptions","id":"1","count":"0","percentage":"0"},{"title":"Phrases","id":"2","count":"0","percentage":"0"},{"title":"Time","id":"3","count":"0","percentage":"0"},{"title":"Actions","id":"4","count":"0","percentage":"0"}]

I now had all the basic components necessary to make a React Native app backed by a SQLite database. It was plain sailing up to this point, and it was plain sailing from there on out. React Native is awesome.

Submitting the app

I spent some free hours here and there over the next couple of days to finish the app and bring all the parts together. There were no big surprises, and that's a good thing.

But before I could submit my app to the app store, I needed to turn off developer mode and enable performance optimizations. It took me some digging to find out how to do this, but this turned out to be quite easy, once you know how. First, inside the project AppDelegate.m, uncomment the line below the OPTION 2 comment:

jsCodeLocation = [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"];

This causes the app to load from pre-bundled file on disk. Next, with the development server running (started with npm start), you can curl the URL in the comment, adding an extra dev query parameter:

curl http://localhost:8081/index.ios.bundle\?dev\=0 -o main.jsbundle

The dev query parameter may be set to either 0 or false (or similarly, 1 and true). When you next build the app, the log should indicate that development mode is now turned off and performance optimizations are turned on:

RCTJSLog> "Running application "HSK1 Chinese" with appParams: {"rootTag":1,"initialProps":{}}. __DEV__ === false, development-level warning are OFF, performance optimizations are ON"

With this out of the way, I could build an archive and submit it to the app store. Again, easy as pie.

I had some friends beta test the app on different platforms through TestFlight, and all seemed well. And today, one week after the launch of React Native, I submitted the app to be reviewed for the App Store. Wish me luck!

Closing remarks

As a web application developer who was already somewhat familiar with both React.js and iOS, React Native was a total breeze to work with. The things I did not know before I started were easy to learn, thanks to these attributes of React Native:

  • Error messages are clear and descriptive. I can't stress the importance of this enough. The React Native error messages would point to specific lines in the JavaScript code, describe the problem exactly, and sometimes even suggest ways to fix it. Once, I did something stupid (I forget what exactly), and it even warned me that what I was doing is an anti-pattern, and should be avoided. Amazing.
  • Instant reload with Cmd+R. Yup, no need to compile again and again, wasting precious minutes and breaking your concentration. With React Native you can press Cmd+R, and the simulator immediately reloads. This is a huge boon for development.
  • No more storyboards. Storyboards are great, but they have their downsides. For one, they don't play well with version control systems like Git. For another, I never really liked dragging lines from UI components to lines in the code to indicate a relationship, only to later accidentally remove that line and be faced with an incomprehensible error message. By allowing UIs to be designed with something similar to HTML+CSS, building these just got a whole lot easier and less painful.
  • It's smoooothhh. Even on my iPhone 4S, the app runs as smooth as the flight of a dream. By running JavaScript in a background thread that doesn't cause the UI to hang, React Native delivers on its promise to make coding iOS apps as easy as writing JavaScript, without the performance problems associated with running it in a WebView. It totally feels just like a native app.

React Native is truly one of the most exciting developments in technology I have seen in a long time, and the wait was worth it. +1, will use again.

fcc5954a6f36e702b590c17a37190922.gif


PS If you'd like to check out the app, it's on the app store right now for $0.99. Or if HSK1 is too easy for you, you can sign up to be notified when other levels become available. Update: Since this was published years ago, iOS introduced some breaking changes that wasn't worthwhile for me to address, and I decided to take the app down. It was fun working on this, thanks for all your support and feedback over the years!

Thanks to Spencer Ahrens from the React Native core team for corrections to this post.