React Native: Build a Blog Reader App

Login to Access Code

React Native makes it incredibly simple to build highly-performant mobile applications for Android and iOS. With the mantra of “learn once, write anywhere” all of the skills you acquire involving React translate well to both mobile and web.

In this tutorial, we build out a basic reading app that digests the Unicorn.TV API for the user to read. You will learn how to:

  • Use Tab Bar controllers for navigation
  • Organize controllers and views in your project
  • Create a UITableView (ListView) in React
  • Make HTTP requests to digest a web API

Prerequisites

Read the React Native: Getting Started Guide, all the install instructions are there and should be pretty straightforward. Let’s get started:

react-native init ReactNativeBlog

Setting up the project

If you have initiated with the react-native command line tool (above), then you should have an iOS folder containing the ReactNativeBlog.xcodeproj project file. Let’s start by opening this Xcode project and building with command+r.

The first thing we want to do is to create some separate files for our various pages to live in. For this app we are going to feature a tab-based application with three sections: articles, podcasts, and screencasts. Each of these files will point to a single detail view for viewing a piece of content. Lets create those files in an organized manner:

  • Create a folder called components
  • Create the following (empty) files inside the components folder. These will serve as our controller views:
    • articles.js
    • podcasts.js
    • screencasts.js
    • detail.js

Media

Lets copy over the assets that we will use for our tab bar items and typography within the application. You can find all the image and font assets needed for this app, inside the media folder on github.

To add the images:

  • Open the Xcode project and select the Images.xcassets folder within the Navigator.
  • Select all of the images and drag them into the folder.

To add the fonts: Check out our little React Native Font Tutorial.

Building our main application

Open up index.ios.js.

Start by gutting the default hello world app from React. The render() should just be defined as an empty return and the styles var should be an empty object. Next, under the React require, you will add in your custom components:

var React = require('react-native');
var Articles = require('./components/articles');
var Podcasts = require('./components/podcasts');
var Screencasts = require('./components/screencasts');

Now let’s grab the React components that we will use to build our application. You probably already have:

  • AppRegistry: the entry point and shares global properties and definitions
  • StyleSheet: an abstraction similar to CSS which holds and binds component properties
  • Text: component for displaying text which supports nesting, styling, and touch handling
  • View: component that supports layout, style, touch handling accessibility controls, and nesting

In addition to these, we will be using:

  • ListView: component designed for efficient display of vertically scrolling lists of changing data
  • TouchableHighlight: a wrapper for making views respond properly to touches
  • NavigatorIOS: wraps UIKit navigation and allows back-swipe functionality
  • TabBarIOS: UITabBarViewController

Your component/module use list should look like this:

var {
  AppRegistry,
  ListView,
  StyleSheet,
  Text,
  View,
  ActivityIndicatorIOS,
  TouchableHighlight,
  NavigatorIOS,
  TabBarIOS,
} = React;

Using “styles” we can basically assign values to various component properties as they would exist in iOS SDKs. For our index, we don’t need many styles, as we are only rendering a Tab Bar Controller. Therefore, we can just define a single attribute for our page controllers and give them dimension by using flexbox:

var styles = StyleSheet.create({
  container: {
    flex: 1,
  }
});

Creating a Tab Bar

Now that we’ve defined everything we need, we can start creating our interface. We will use a <TabBarIOS> component to wrap our other controller views. Look at the documentation for TabBar and you will find that TabBarIOS.Item is used to define a tab. Let’s create those now:

<TabBarIOS>
  <TabBarIOS.Item icon={{uri: 'articles'}} title='Articles'></TabBarIOS.Item>
  <TabBarIOS.Item icon={{uri: 'screencasts'}} title='Screencasts'></TabBarIOS.Item>
  <TabBarIOS.Item icon={{uri: 'podcasts'}} title='Podcasts'></TabBarIOS.Item>
</TabBarIOS>

Notice that we only have to specify the image path with {uri: 'podcast'}. This makes it very easy to reverence and use various media assets throughout your application!

A quick note: Some object properties are supported as React’s “styles” vs component properties. Rather than defining everything as a property component, try to learn which attributes can be defined in styles instead.

Reload and you should see an empty application with a bottom tab navigation. These tabs don’t go anywhere just yet!

Screenshot of empty TabBarController

Now we probably want to set a default tab and some state that will track which tab is selected. Let’s create a tab property in getInitialState() and have it default to the first tab: articles.

getInitialState: function() {
    return {
      tab: 'articles'
    }
}

This will allow us to conditionally check on our TabBarIOS.Item and set selected conditionally based upon state. We can update this state with the onPress event listener:

<TabBarIOS.Item
  icon={{uri: 'articles'}}
  onPress={() => {this.setState({ tab: 'articles' }); }}
  selected={this.state.tab === 'articles'}
  title='Articles'
></TabBarIOS.Item>

Now we are ready to render something within these tabs. We need to tell the tab bar controller which views to display when a tab is pressed. Lets do this by creating a Navigation Controller and embedding it within our TabBarIOS.Item. This component also needs some dimension, so lets user our generic container style rule:

<NavigatorIOS
  barTintColor='#3D728E'
  style={styles.container}
  initialRoute={{title: 'Articles', component: Articles}}
/>

We’re done here for now, we need to go build a controller to actually display now. If you build right now, errors will be thrown as we haven’t yet defined Articles.

Sanity Check

Your index.ios.js file should look similar to this:

'use strict';

var React = require('react-native');
var Articles = require('./components/articles');
var Podcasts = require('./components/podcasts');
var Screencasts = require('./components/screencasts');

var {
  AppRegistry,
  ListView,
  StyleSheet,
  Text,
  View,
  TouchableHighlight,
  NavigatorIOS,
  TabBarIOS,
} = React;


var styles = StyleSheet.create({
  container: {
    flex: 1,
  },
});

var ReactNativeBlog = React.createClass({
  getInitialState: function() {
    return {
      tab: 'articles',
    }
  },

  render: function() {
    return (
      <TabBarIOS>
        <TabBarIOS.Item
          icon={{uri: 'articles'}}
          title='Articles'
          onPress={() => {this.setState({ tab: 'articles' }); }}
          selected={this.state.tab === 'articles'}
        >
          <NavigatorIOS
            barTintColor='#3D728E'
            style={styles.container}
            initialRoute={{title: 'Articles', component: Articles}}
          />
        </TabBarIOS.Item>

        <TabBarIOS.Item
          icon={{uri: 'podcasts'}}
          title='Podcasts'
          onPress={() => {this.setState({ tab: 'podcasts' }); }}
          selected={this.state.tab === 'podcasts'}
        >
          <NavigatorIOS
            barTintColor='#3D728E'
            style={styles.container}
            initialRoute={{title: 'Podcasts', component: Podcasts}}
          />
        </TabBarIOS.Item>

        <TabBarIOS.Item
          icon={{uri: 'screencasts'}}
          title='Screencasts'
          onPress={() => {this.setState({ tab: 'screencasts' }); }}
          selected={this.state.tab === 'screencasts'}
        >
          <NavigatorIOS
            barTintColor='#3D728E'
            style={styles.container}
            initialRoute={{title: 'Screencasts', component: Screencasts}}
          />
        </TabBarIOS.Item>
      </TabBarIOS>
    );
  }
});

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

Setup for the parent page controllers

Since our process for podcasts and screencasts will be almost identical in every way except variable names, we will just walk through and share the code for creating the articles list and detail views. Lets get started!

Open components/articles.js.

As usual to React and React Native, we need to pull in modules that we will use, and export our own custom module component. Let’s default to showing a little loading text. Note also, that we are defining detail as a module even though it is an empty file.

'use strict';

var React = require('react-native');
var Detail = require('./detail');

var {
  StyleSheet,
  View,
  TouchableHighlight,
  ListView,
  Text,
} = React;

var styles = StyleSheet.create({});

var Articles = React.createClass({

  render: function() {
    return (
      <View style={styles.loading}>
        <Text>
          Loading Articles...
        </Text>
      </View>
    );
  }

});

module.exports = Articles;

Alright, most of that should be pretty straightforward if you know React. Let’s jump into some real app code!

Default to a loading view

We probably want to display the loading view until we have fetched our content to render the table list. Let’s put the rendering into a new method called renderLoadingView() and conditionally call this render:

renderLoadingView: function() {
  return (
    <View style={styles.loading}>
      <Text>
        Loading Articles...
      </Text>
    </View>
  );
},

render: render: function() {
  if (!this.state.loaded) {
    return this.renderLoadingView();
  }

  <View>
    <Text>
      This will be a list!
    </Text>
  </View>
}

Since this lives within a Navigation Controller, the top bar will cover part of the content. Style your loading page to make it nice and pretty:

var styles = StyleSheet.create({
  loading: {
    alignItems: 'center',
    backgroundColor: '#F5FCFF',
    flex: 1,
    fontFamily: 'Rokkitt',
    justifyContent: 'center',
    padding: 5,
    paddingTop: 40,
  },
});

Now we should probably define an initial state for the list so that loaded defaults to false:

getInitialState: function() {
  return {
    loaded: false,
  };
}

If you build and run, you should now see your loading page by default!

Making http requests with React Native

It’s time to deep dive and really kick things up a notch. Here we go!

First we need to define a request URL constant. You can use our API if you’d like:

var REQUEST_URL = 'https://unicorn.tv/api/articles?topic=swift&api_key=l9461I3z9XhP983b14P25JSjZvuBJ6BI';

Now we need one other state to track, our data source. In iOS, you use TableView Delegates and Controllers together to define a source and render rows from that source. This process is made very simple in React Native.

First, we will set our data source in the initial state so that our ListView begins tracking change events:

getInitialState: function() {
  return {
    dataSource: new ListView.DataSource({
      rowHasChanged: (row1, row2) => row1 !== row2,
    }),
    loaded: false,
  };
},

Next, let’s set some styles for all the elements we will build on this page:

var styles = StyleSheet.create({
  // Default Loading View
  loading: {
    alignItems: 'center',
    backgroundColor: '#F5FCFF',
    flex: 1,
    fontFamily: 'Rokkitt',
    justifyContent: 'center',
    padding: 5,
    paddingTop: 40,
  },
  // Table
  listView: {
    backgroundColor: '#F4F0E8',
    paddingTop: 60,
  },
  // Table Row
  rowContainer: {
    flexDirection: 'row',
    padding: 20,
  },
  // Text wrapper within row
  textContainer: {
    flex: 1
  },
  // Row separator
  separator: {
    height: 1,
    backgroundColor: '#E3E0D7'
  },
  // Row Post Title
  title: {
    color: '#3D728E',
    fontFamily: 'Rokkitt',
    fontSize: 20,
  },
  // Row Post Description
  description: {
    color: '#7C705F',
    fontFamily: 'Josefin Sans',
    fontSize: 14,
    lineHeight: 20,
    marginTop: 8,
    textAlign: 'left',
  },
});

Next, we will define a method for actually making the request, and call it by default when the Article component is finished mounting. Let’s log the request response as well as update our dataSource state:

componentDidMount: function() {
  this.fetchData();
},

fetchData: function() {
  fetch(REQUEST_URL)
    .then((response) => response.json())
    .then((responseData) => {
      console.log(responseData);

      this.setState({
        dataSource: this.state.dataSource.cloneWithRows(responseData),
        loaded: true,
      });
    })
    .done();
},

Great, so currently we have a loading graphic as a default view, and a request made to fetch data from an API when the Articles component is mounted. Let’s create a rendering method renderRow to instruct the ListView component how to create the table rows:

renderRow: function(data) {
  return (
    <TouchableHighlight onPress={() => this.rowPressed(data)} underlayColor='#e3e0d7'>
      <View>
        <View style={styles.rowContainer}>
          <View  style={styles.textContainer}>
            <Text style={styles.title}>{data.title}</Text>
            <Text style={styles.description} numberOfLines={0}>{data.description}</Text>
          </View>
        </View>
        <View style={styles.separator}/>
      </View>
    </TouchableHighlight>
  );
},

Now we’re ready to add a ListView component to the render() method and replace the placeholder junk we had before:

render: function() {
  if (!this.state.loaded) {
    return this.renderLoadingView();
  }

  return (
    <ListView
      dataSource={this.state.dataSource}
      renderRow={this.renderRow}
      style={styles.listView}
    />
  );
},

React Native ListView

Creating the Detail View

Our Articles component is nearly finished, but we need to listed to row presses and then push the parent navigation controller to the detail. We will first define a rowPressed method on the list component:

rowPressed: function (data) {
  console.log('row press');

  // Comment this out if you'd like to test without defining the detail view
  this.props.navigator.push({
    title: undefined,
    component: Detail,
    passProps: {data: data}
  });
}

React Native Web View

Sanity Check: Your articles.js file should look like this:

'use strict';

var React = require('react-native');
var Detail = require('./detail');

var {
  StyleSheet,
  View,
  TouchableHighlight,
  ListView,
  Text,
} = React;

var styles = StyleSheet.create({
  // Default Loading View
  loading: {
    alignItems: 'center',
    backgroundColor: '#F5FCFF',
    flex: 1,
    fontFamily: 'Rokkitt',
    justifyContent: 'center',
    padding: 5,
    paddingTop: 40,
  },
  // Table
  listView: {
    backgroundColor: '#F4F0E8',
    paddingTop: 60,
  },
  // Table Row
  rowContainer: {
    flexDirection: 'row',
    padding: 20,
  },
  // Text wrapper within row
  textContainer: {
    flex: 1
  },
  // Row separator
  separator: {
    height: 1,
    backgroundColor: '#E3E0D7'
  },
  // Row Post Title
  title: {
    color: '#3D728E',
    fontFamily: 'Rokkitt',
    fontSize: 20,
  },
  // Row Post Description
  description: {
    color: '#7C705F',
    fontFamily: 'Josefin Sans',
    fontSize: 14,
    lineHeight: 20,
    marginTop: 8,
    textAlign: 'left',
  },
});

// var REQUEST_URL = 'https://unicorn.tv/api/articles?topic=swift&api_key=l9461I3z9XhP983b14P25JSjZvuBJ6BI';
var REQUEST_URL = 'http://unicorn.local:3000/api/articles?topic=swift&api_key=l9461I3z9XhP983b14P25JSjZvuBJ6BI';

var Articles = React.createClass({

  getInitialState: function() {
    return {
      dataSource: new ListView.DataSource({
        rowHasChanged: (row1, row2) => row1 !== row2,
      }),
      loaded: false,
    };
  },

  fetchData: function() {
    fetch(REQUEST_URL)
      .then((response) => response.json())
      .then((responseData) => {
        console.log(responseData);

        this.setState({
          dataSource: this.state.dataSource.cloneWithRows(responseData),
          loaded: true,
        });
      })
      .done();
  },

  componentDidMount: function() {
    this.fetchData();
  },

  render: function() {
    if (!this.state.loaded) {
      return this.renderLoadingView();
    }

    return (
      <ListView
        dataSource={this.state.dataSource}
        renderRow={this.renderRow}
        style={styles.listView}
      />
    );
  },

  renderRow: function(data) {
    return (
      <TouchableHighlight onPress={() => this.rowPressed(data)} underlayColor='#e3e0d7'>
        <View>
          <View style={styles.rowContainer}>
            <View  style={styles.textContainer}>
              <Text style={styles.title}>{data.title}</Text>
              <Text style={styles.description} numberOfLines={0}>{data.description}</Text>
            </View>
          </View>
          <View style={styles.separator}/>
        </View>
      </TouchableHighlight>
    );
  },

  rowPressed: function (data) {
    console.log('row press');

    // Comment this out if you'd like to test without defining the detail view
    this.props.navigator.push({
      title: undefined,
      component: Detail,
      passProps: {data: data}
    });
  },

  renderLoadingView: function() {
    return (
      <View style={styles.loading}>
        <Text style={styles.loading}>
          Fetching articles, please wait...
        </Text>
      </View>
    );
  },
});

module.exports = Articles;

Creating a Detail View

We’ve built a pretty cool list view already, so let’s take some short cuts now. There are many ways we could create a detail view from the API data, but the beauty of using Unicorn.TV as an example is that the site is already beautifully responsive! Let’s just display the page corresponding to a row within a webview. This is pretty straightforward:

'use strict';

var React = require('react-native');

var {
  StyleSheet,
  WebView,
} = React;

var styles = StyleSheet.create({
  body: {
    flex: 1,
    flexDirection: 'column',
  },
});

var DetailView = React.createClass({

  render: function() {
    return (
      <WebView style={styles.body} url={this.props.data.url + '?display=embedded'} />
    );
  }

});

module.exports = DetailView;

Congrats, you did it! Now see if you can rinse and repeat for the Podcast and Screencast pages without looking back at this tutorial!