If working for a development agency for the last four years has taught me anything, it's that budget is king. Clients usually have grandiose plans but more often than not, they don't have the budget to execute on that plan. I've seen clients ditch second and third platforms, take a hatchet to their own feature sets, and compromise on UI/UX, all in the name of fitting into the budget.

After experiencing this process on numerous occasions, I set out to find a way to offer a more affordable solution to clients, specifically clients who need to build an application across several platforms. Clients often look to launch on iOS, Android, and the Web, which traditionally results in parallel development of three distinct code bases. Essentially, you build the same system in three locations, usually connected via one unified back end. I wanted a solution that could help minimize that overhead without sacrificing quality. I'd looked at other solutions in the past, but performance had always been an issue. It just didn't feel and respond like a native application. That's when I discovered React Native.

React Native, an open-source framework from Facebook, builds on the popular React JavaScript framework, allowing developers to create cross-platform iOS and Android applications using JavaScript. It differs from other cross-platform frameworks that use JavaScript (such as PhoneGap, Titanium, etc.) because, although the application logic runs in JavaScript, the resulting application UI is 100% native. This means that you don't have to deal with many of the compromises usually associated with managing an HTML5-based UI. In my experience, the result is often indistinguishable from a purely native application. In this article, you'll get an introduction to React Native and learn how to get a development environment up and running and ready for experimentation.

Native is Difficult but Necessary

Native development comes with its own set of challenges that other platforms, like the Web, largely don't have to deal with. For starters, you need different engineering talents to execute on iOS and Android platforms respectively, often resulting in multiple disjointed technology stacks. This creates additional overhead because you need to ensure consistency between both platforms.

Additionally, native development has a slow development cycle. When building on the Web, if a developer makes a change and wants to test the results, he simply saves the file and reloads the browser. However, a native developer has to recompile the project to view even the smallest of changes. This results in a slow cycle, especially on larger projects where compile time is significant.

Native development can also hinder your iteration time. Deploying to the Web can be done multiple times daily if desired, whereas updating a native mobile application often requires a submission and review process followed by needing the end user to physically download the updated version of the build. Instead of releasing or iterating daily, you're likely stuck iterating weekly or monthly.

So why build native apps to begin with? You do it because native allows you to create a better user experience and deliver a product that's consistent with each respective platform, much more so than a Web application or hybrid application could. You can tailor the experience on iOS and Android to create a product that the consumer has come to expect from the OS. You can do this because you have access to the platform-specific UI elements and tool sets. Consider Figure 1, for instance. Date and time pickers have a different look and feel on each platform and users of the respective platforms expect that style of input because the behavior and look is reinforced across the OS. Although it's possible to replicate these native components on the Web, something about them never quite feels the same as their native equivalents. Furthermore, they don't receive automatic updates when the platform OS changes.

Figure 1: The iOS date picker (left) compared to the Android date picker (right)
Figure 1: The iOS date picker (left) compared to the Android date picker (right)

You also have to take into account writing multithreaded applications that can handle heavy computational logic while maintaining a high level of performance and responsiveness. This is a difficult feat to achieve working off of the main thread in a browser.

So what if you could take the best of both worlds, combining the level of user experience you can only achieve through native development with the development experience of building Web applications? React Native aims to do this, letting you construct a rich mobile UI from declarative components using JavaScript.

React Native

React Native brings the power of the React programming model to mobile development. So how exactly does it work? With React Native, you build a native application using the same UI components that a standard native app requires, making it visually indistinguishable from a purely native counterpart. The difference between the two is that with React Native, you assemble the UI using JavaScript instead of traditional Objective-C, Java, or Swift.

Instead of running React in the browser, rendering standard HTML divs, it runs in an embedded instance of the JavaScriptCore, which then renders native platform UI components. What happens when you want to render a native component that looks or functions differently on iOS than it does on Android? First, understand that React Native isn't advertised as a build-once-deploy-everywhere sort of framework, but more as a learn-once-write-everywhere solution. It's unlikely that React Native will encompass everything you need to build a 100% cross-platform application. You'll need to write custom platform-specific code to accommodate both iOS and Android. This is demonstrated by the inclusion of iOS and Android versions of the index.js file that serves as the entry point for the React Native app. Additionally, when you simply can't achieve some desired feature or functionality from the React Native framework, you can write custom functionality in Objective-C, Swift, or Java and tie them back into the JavaScript base that you build.

Each application is different and the complexity of the feature set and design can impact the amount of platform-independent development that you'll need, but it isn't unreasonable to expect an 85% code share rate among the platforms.

Each application is different and the complexity of the feature set and design can impact the amount of platform-independent development that you'll need, but it isn't unreasonable to expect an 85% code share rate among the platforms.

Getting Started

If you already have experience with iOS or Android development, getting a React Native environment spun up should be relatively easy. For the sake of this example, you'll look at setting up a React Native environment for compiling an iOS application using Mac OS. Check the sidebar for a link to instructions for Android and other operating systems. To start, you'll need to install some dependencies

  • Homebrew: Used as the installer for Node and Watchman
  • Node.js: Comes with the Node Package Manager (npm) needed to install React Native command line interface
  • Watchman: A tool by Facebook for watching changes in the file system. Recommended for performance.
  • React Native command line interface: Used to generate, compile, and run projects
  • Xcode: Needed for iOS simulator and build tools

Start by installing Homebrew, if you don't already have it installed. You can grab the terminal command to install it at http://brew.sh/. Once it's installed, open Terminal and run the following two commands to install Node and Watchman:

brew install node
brew install watchman

Once that's done, you can use npm to install the React Native command line interface by executing the following command in Terminal. If you run into any permission errors, try preceding the command with “sudo”. If you get an error complaining about the npm module not being found, you may have to install npm directly (there's more info in the sidebar).

npm install -g react-native-cli

Next you need to install Xcode. For easiest access, download it from the Mac App Store. Installing Xcode also installs all the needed build tools and simulators you'll use to run your React Native application. Now you have all the tools in place, so you need to test them to make sure everything works. Go back to the terminal window, “cd” into the directory where you want to create your React Native sample project, and then execute the following command:

react-native init SampleProject

This command uses the React Native command line tools to generate a React Native project structure. After you run that command, npm installs a React Native package within a new folder called SampleProject. That folder resides in the directory you were in when you ran the command. If it worked properly, the contents of the folder should resemble Figure 2.

Figure 2: The React Native base project structure, as generated by the React Native command line tools
Figure 2: The React Native base project structure, as generated by the React Native command line tools

Notice two index files. These act as your entry point for iOS and Android respectively. It's important to note that when you need to write platform-specific code, you can use the .Android and .iOS extension prefixes. When you call on or import that specific code into another component, React Native decides which file to use at build time based on which OS you're currently compiling for. Furthermore, if you click into the iOS and Android folders, you'll notice standard native project structures. You'll use these projects if you decide to write custom native integrations and also when you go to compile/provision final builds for App Store submission.

The last step of testing your new project setup is to run the project. Go back to your terminal window and run the commands from the next snippet. This navigates you into the project directory and the second command tells React Native to compile and run the iOS version of your project. (Note that you can also open the Xcode project directly and run it from there.)

cd SampleProject
react-native run-ios

Figure 3 demonstrates what you see in your simulator if it all worked properly.

Figure 3: The React Native base project running in an iOS simulator
Figure 3: The React Native base project running in an iOS simulator

Live Reload

Notice in Figure 3 where the sample app screen points out that you can press Cmd+R to reload. Earlier in the article, I pointed out that native dev cycles are slower than Web cycles, and part of that has to do with native developers recompiling their projects to test even the smallest changes. With React Native, you can refresh the application in the simulator without needing to recompile it. You can make changes to your React components, click over to the simulator, press Cmd+R, and then watch the application refresh to show those changes in real time.

Implementation

You have the environment configured and you've generated and run the base project installation, but you probably still aren't sure what's happening. Let's take a closer look at the source. Navigate to your index.ios.js file and open it with your preferred code editor. Listing 1 demonstrates what you should see.

Listing 1: The index.ios.js base file contents

/**
* index.ios.js - Sample React Native App
*/

import React, { Component } from 'react';
import {
    AppRegistry,
    StyleSheet,
    Text,
    View
} from 'react-native';

export default class SampleProject extends Component {
    render() {
        return (
            /** JSX for view contents */
            <View style={styles.container}>
                <Text style={styles.welcome}>
                    Welcome to React Native!
                </Text>
                <Text style={styles.instructions}>
                    To get started, edit index.ios.js
                </Text>
                <Text style={styles.instructions}>
                    Press Cmd+R to reload,{'\n'}
                    Cmd+D or shake for dev menu
                </Text>
            </View>
        );
    }
}

/** array of style props */
const styles = StyleSheet.create({
    container: {
        flex: 1,
        justifyContent: 'center',
        alignItems: 'center',
        backgroundColor: '#F5FCFF',
    },
    welcome: {
        fontSize: 20,
        textAlign: 'center',
        margin: 10,
    },
    instructions: {
        textAlign: 'center',
        color: '#333333',
        marginBottom: 5,
    },
});

/** tells React Native what root component is */
AppRegistry.registerComponent('SampleProject', () => SampleProject);

The first thing you might notice is that aspects of the code don't resemble traditional JavaScript. React Native supports ES2015 (or ES6), which is a newer iteration of JavaScript that's become part of the official standard but isn't yet supported by most Web browsers. The use of import, class, extends, from, and the ()-> notation are all ES2015 features. Check the sidebar for more information on ES2015.

You'll also notice that there are no traditional HTML or div tags of any type. What you see, <Text>text content</Text>, is JSX. JSX embeds XML in JavaScript, allowing you to write markup language inside your code. In this case, “Text” represents a built-in component used for displaying some text. A number of other stock components exist as well. You can refer to the documentation at https://reactnative.dev/docs/getting-started#! for a full list.

At the top of the file, two separate import statements bring in the React and React Native dependencies that you'll need. The next line of code below the imports declares the main component that represents the initial screen shown when the application launches. It has a render function that returns the view. The component is made up of a React Native “View” component that contains several formatted “Text” components. The “Text” components have a style parameter (called a prop). This style prop is assigned using a “styles” constant that's declared after the SampleProject class. It's an array of styles created using the React Native StyleSheet .create method. The style code closely resembles traditional CSS, except instead of the dash notation, it uses camel case. For instance, instead of the standard CSS attribute “background-color”, React Native uses the attribute “backgroundColor”.

If you look at the styling code, you'll see the flex keyword. React Native uses a CSS layout style called FlexBox (see the sidebar for additional details). It uses the FlexBox algorithm, allowing you to define the layout of a component's children using a combination of:

  • flex: A number that defines the proportional size of the component. A component with flex = 2 takes twice the space as a component with flex = 1.
  • flexDirection: Determines the primary axis of the component's layout. Should the component's children be organized horizontally or should they get stacked vertically?
  • justifyContent: Sets alignment of children on the primary axis. You can use this to distribute the child elements at the start, end, or center, or space them evenly within the parent component.
  • alignItems: Sets alignment of children on the secondary axis.

You'll also notice a call to registerComponent at the end of the file. This registers the component with the AppRegistry. The AppRegistry tells React Native which component is the root for the whole application. When you build with React Native, you make a lot of components, in fact, everything that displays on screen is some sort of component. You're likely to have only one call to registerComponent in your entire application. Once the root gets established, you can navigate between components without needing to make additional calls to the AppRegistry.

This may seem overwhelming at first but the React Native website has extensive documentation that covers each of the above concepts in great detail, with examples (StyleSheet, Props, Components, Flexbox, etc.). Once you grasp the general composition and approach, it becomes relatively easy to start composing native views with JavaScript. The next section walks you through another example to help you get a better idea of the framework's capabilities.

Practical Example

Let's work through a simple example to get a better understanding of how components work and how you can create a native-looking experience with this framework. Figure 4 depicts a directory of employees for the company where I work, Rocksauce Studios, using a React Native list view. I also compiled the project for Android so that you can see how similar they look using 100% of the same source code.

Figure 4: The demo application running on iOS (left) and Android (right)
Figure 4: The demo application running on iOS (left) and Android (right)

To start on this example, you first want to create a component to use as the main view for the application. In the first sample of code generated by the React Framework, the default component was constructed inside the index file. However, you're likely to want to modularize your components because they'll often be larger than a few lines of code. So let's create a new file. Create a new folder within the SampleProject directory titled Components. Inside that folder, create a new JavaScript file called InitialScreen.js. Add the code from Listing 2 to the file and save. This should look similar to the example from the previous section because you imported the dependencies and created a base component with a render function. The render function currently returns an empty view that consumes 100% of the screen as defined by "flex: 1". This InitialScreen eventually holds your list view, but before you can display the list view, you need to create the corresponding component.

Listing 2: First iteration of InitialScreen.js

import React, { Component } from 'react';
import {
    AppRegistry,
    View
} from 'react-native';

export default class InitialScreen extends Component {
    render() {
        return (
            <View style={{ flex: 1, }}></View>
        );
    }
}

Create a new JavaScript file in your components directory and call it DirectoryList.js. Here, you'll construct a custom employee component to represent each row in the list as well as the list component that assembles the rows of employees, which eventually gets rendered by InitialScreen.js. Listing 3 demonstrates the first half of the DirectoryList.js file, containing the component that represents each row in the list.

Listing 3: Employee component from DirectoryList.js

import React, { Component } from 'react';
import {
AppRegistry,
ListView,
Text,
View,
Image
} from 'react-native';

// Represents each row in the table
class Employee extends Component {
render() {

    let pic = {
        uri: this.props.pic
    };

    return (
        {/* FYI this is how you comment in JSX*/}
        <View style={{flex: 1, height:120}}>
            <View style={{flex:1, flexDirection:'row'}}>
                {/* profile pic */}
                <Image source={pic} style={{
                    width: 100,
                    height: 100,
                    borderRadius:50,
                    marginTop:20,
                    marginLeft:12}}/>
                <View style={{
                    flex:1,
                    flexDirection:'column',
                    marginLeft:12}}>
                    {/* using empty views as spacers */}
                    <View style={{flex:1}}></View>
                    <View style={{flex:1}}>
                        <View style={{height:10}}></View>
                        {/* user component properties for row data */}
                        <Text>{this.props.name}</Text>
                        <Text style={{color:'#999999', fontSize:12}}>
                            {this.props.position}
                        </Text>
                    </View>
                    <View style={{flex:1}}></View>
                </View>
            </View>
        </View>
        );
    }
}

You can see that the employee component is rendering a collection of View, Image, and Text components. For the sake of simplicity, the example uses inline styling to position and theme the components; in a production application, you'd likely want to break that styling out. Notice the call to "this.props" throughout the class. When you construct an employee object, you'll do so using several parameters like pic, name, and position. The object can then summon the parameters that coordinate to itself in order to build its own view. So from a high level, this component takes in a series of parameters and renders out a single row, as shown in Figure 4, using that data.

Next, you want to use the employee class to compose the list view. Listing 4 demonstrates the code you can add to DirectoryList.js following what you just wrote from Listing 3.

Listing 4: DirectoryList compoment from DirectoryList.js

// the employee list component
export default class DirectoryList extends Component {
    // Initialize the hardcoded data
    constructor(props) {
        super(props);
        const ds = new ListView.DataSource({
            rowHasChanged: (r1, r2) => r1 !== r2
        });

        this.state = {
            // use hardcoded data for sample purposes
            // real pic URLs removed for readability
            dataSource: ds.cloneWithRows([
                <Employee
                    name='Q Manning'
                    position='Chief Executive Officer'
                    pic='insert pic url here'/>,
                <Employee
                    name='Peter Yoder'
                    position='Chief Operating Officer'
                    pic='insert pic url here'/>,
                <Employee
                    name='Michael Manning'
                    position='Chief Relationship Officer'
                    pic='insert pic url here'/>,
                <Employee     
                    name='Chris Lindenmayer'
                    position='Creative Director'
                    pic='insert pic url here'/>,
                <Employee
                    name='Jason Bender'
                    position='Director of Development'
                    pic='insert pic url here'/>,
                <Employee
                    name='John Gholson'
                    position='Director of Creative Development'
                    pic='insert pic url here'/>,
            ])
        };
    }
    render() {
        return (
            <ListView
                dataSource={this.state.dataSource}
                renderRow={(rowData) => rowData}
                style={{marginTop:20}}
            />
        );
    }
}

Notice that the render function returns a React Native ListView component. This component works well for long lists of similarly formatted data, only rendering the elements currently on screen and not all of the elements at once, like a traditional scroll view does. The ListView component requires three things:

  • dataSource prop: The source of information that powers the list
  • renderRow prop: Returns a component to render for each entry in the dataSouce
  • rowHasChanged function: Tells the list view how to identify whether the row has actually changed. For the sake of this example, a row has changed if the current row is not the same as the previous row.

You can see in Listing 4 that the data source is initialized as an array of hardcoded Employee components. Each component has the necessary props to get used by its render function to create the custom view for each employee. Because your data source contains components, you can tell the renderRow function to return the rowData, which, in turn, renders the corresponding employee from the dataSource. If your data source was an array of strings or some other data type that wasn't going to render its own views, you could instead use renderRow like this:

<ListView dataSource={this.state.dataSource}
renderRow={(rowData) => <Text>{rowData}</Text>}/>

Now that DirectoryList.js is complete, you need to loop back to InitialScreen.js and import the directory list by adding the following line to the top of the file:

import DirectoryList from './DirectoryList';

You also need to tell the InitialScreen component to render a copy of the DirectoryList component by adding the following line inside the View component that currently resides in the render function of InitialScreen.js:

<DirectoryList/>

The only thing left to do is modify the index file to show the InitialScreen component when it loads. Listing 5 demonstrates what the new index.ios.js file should now look like. Once you've done that, run the application to test the results. You should see something resembling the screenshots from Figure 4.

Listing 5: index.ios.js for the directory list example

import React, { Component } from 'react';
import {
    AppRegistry,
    View
} from 'react-native';

import InitialScreen from './components/InitialScreen';

export default class SampleProject extends Component {
    render() {
        return (
            <View style={{flex: 1,}}>
            <InitialScreen/>
            </View>
        );
    }
}

AppRegistry.registerComponent('SampleProject', () => SampleProject);

Wrapping Up

Over the course of this article, you got a glimpse of the potential power of React Native. As it continues to gain in popularity, I can see many developers and agencies adopting it into their toolset, opting for a more efficient and unified stack. To learn more about the technology and to get access to additional examples, be sure to visit the React Native website for full API documentation. The sidebars for this article have links to additional resources that may be of use as well.