React Native By Example

3 (1 reviews total)
By Richard Kho
  • Instant online access to over 7,500+ books and videos
  • Constantly updated with 100+ new titles each month
  • Breadth and depth in over 1,000+ technologies
  1. First Project - Creating a Basic To-Do List App

About this book

React Native's ability to build performant mobile applications with JavaScript has resulted in its popularity amongst developers. Developers now have the luxury to create incredible mobile experiences that look and feel native to their platforms with the comfort of a well-known language and the popular React.js library.

This book will show you how to build your own native mobile applications for the iOS and Android platforms while leveraging the finesse and simplicity of JavaScript and React.

Throughout the book you will build three projects, each of increasing complexity. You will also link up with the third-party Facebook SDK, convert an app to support the Redux architecture, and learn the process involved in making your apps available for sale on the iOS App Store and Google Play.

At the end of this book, you will have learned and implemented a wide breadth of core APIs and components found in the React Native framework that are necessary in creating great mobile experiences.

Publication date:
April 2017
Publisher
Packt
Pages
414
ISBN
9781786464750

 

Chapter 1. First Project - Creating a Basic To-Do List App

Having set up our environment for React Native development in the preface, let's start developing the application. Throughout this book, I'll refer to this application by the project name I began with--Tasks. In this chapter, we will cover the following topics:

  • Planning the features that a to-do list app should have
  • Basic project architecture
  • Introducing StyleSheet, the React Native component for working with styles
  • An overview of Flexbox, a layout mode inspired by CSS for styling in React Native
  • Become acquainted with ES6, the new JavaScript syntax we will be writing our code in
  • Creating the building blocks of Tasks with TextInput, ListView, AsyncStorage, Input, state, and props
  • Learning about the iOS Simulator's Developer menu, which can help us during the writing of our app
 

Initializing a new project


With the React Native SDK already installed, initializing a new React Native project is as simple as using the following command line:

react-native init Tasks

 

 

Let the React Native command line interface do its work for a few moments, then open the directory titled Tasks once it is completed.

From there, running your app in iOS Simulator is as easy as typing the following command:

react-native run-ios

This will start a process to build and compile your React Native app, launch the iOS Simulator, import your app to the Simulator, and start it. Whenever you make a change to the app, you will be able to reload and see those changes immediately.

 

Feature planning


Before writing any code, I'd like to take the time to plan out what I want to accomplish in my project and scope out a minimum viable product (MVP) to aim for prior to building out any advanced functionalities. This helps with the prioritization of what key components of our app are necessary to have a functioning prototype so that we can have something up and running.

For me, the MVP is a fantastic way to quantify my ideas into something I can interact with and use to validate any assumptions I have, or catch any edge cases, while spending the minimum amount of time necessary on coming to those conclusions. Here's how I approach feature planning:

  • What does the product I'm building do?
  • Ideally, what are some of the highlighting features that make this application stand out?
  • Which of the features on the preceding list are necessary to have a working product? Once you know the necessary features, cut out everything that doesn't give you the bare-bones functionality.
  • Give some thought to its design, but don't stress on every single detail just yet.

With these intentions in mind, here's what I've come up with:

  • This is an application that will let me create and track a list of tasks that I have. These can be as small as a shopping list or as big as long-term goals.
  • I'd like to set a reminder for each unique task so that I can get to each one in an orderly fashion. Ideally, the items on the list can be grouped into categories. Category grouping could perhaps be simplified by something like icons. This way, I can also sort and filter my list by icons.
  • The only things that are necessary from the beginning are that I can use a text input field to type a task, have it rendered onto a list of items, and mark them off as they are completed; everything else is secondary.

Now that we've got a clearer picture of our app, let's break down some actionable steps we can take to make it a reality:

  1. Let's generate a list of default items. These don't have to be manually entered as we just want to see our list populated in the app itself.
  2. After that, your users should be able to input their own tasks using a text field and the native keyboard.
  3. Next, I'd like to make that list scrollable in case my list of tasks spans past an entire vertical screen's height.
  4. Then, we should let items be marked as complete with some sort of visual indicator.

That's it! These are the four goals we currently have. As I previously mentioned, everything else is secondary for the time being. For now, we just want to get an MVP up and running, and then we will tweak it to our hearts' content later.

Let's move ahead and start thinking about architecture.

 

Project architecture


The next important thing I'd like to tackle is architecture; this is about how our React Native app will be laid out. While the projects we build for this book are meant to be done individually, I firmly believe that it is important to always write and architect code in a manner that expects the next person to look at it to be an axe-murderer with a short temper. The idea here is to make it simple for anyone to look at your application's structure and be able to follow along.

First, let's take a look at how the React Native CLI scaffolds our project; comments on each relevant file are noted to the right-hand side of the double slashes (//):

|Tasks // root folder
|__Android*
|__ios*
|__node_modules
|__.buckconfig
|__.flowconfig
|__.gitignore
|__.watchmanconfig
|__index.android.js // Android entry point
|__index.ios.js // iOS entry point
|__package.json // npm package list

The Android and iOS folders will go several layers deep, but this is all part of its scaffolding and something we will not need to concern ourselves with at this point.

Based on this layout, we see that the entry point for the iOS version of our app is index.ios.js and that a specific iOS folder (and Android for that matter) is generated.

Rather than using these platform-specific folders to store components that are only applicable to one platform, I'd like to propose a folder named app alongside these which will encapsulate all the logic that we write.

Within this app folder, we'll have subfolders that contain our components and assets. With components, I'd like to keep its style sheet coupled alongside the JS logic within its own folder.

Additionally, component folders should never be nested--it ends up being way too confusing to follow and search for something. Instead, I prefer to use a naming convention that makes it immediately obvious what one component's relation to its parent/child/sibling happens to be.

Here's how my proposed structure will look:

|Tasks 
|__app 
|____components 
|______TasksList 
|________index.js 
|________styles.js 
|______TasksListCell 
|________index.js 
|________styles.js 
|______TasksListInput 
|________index.js 
|________styles.js 
|____images 
|__Android 
|__ios 
|__node_modules 
|__.buckconfig 
|__.flowconfig 
|__.gitignore 
|__.watchmanconfig 
|__index.android.js 
|__index.ios.js 
|__package.json 

From just a quick observation, you might be able to infer that TasksList is the component that deals with our list of tasks shown on the screen. TasksListCell will be each individual row of that list, and TasksListInput will deal with the keyboard input field.

This is very bare-bones and there are optimizations that we can make. For example, we can think about things such as platform-specific extensions for iOS and Android, as well as building in further architecture for Redux; but for the purpose of this specific app, we will just start with the basics.

 

StyleSheet


React Native's core visual components accept a prop called style and the names and values more or less match up with CSS's naming conventions, with one major exception--kebab-case is swapped out for camelCase, similar to how things are named in JavaScript. For example, a CSS property of background-color will translate to backgroundColor in React Native.

For readability and reuse, it's beneficial to break off inline styling into its own styles object by defining all of our styles into a styles object using React Native's StyleSheet component to create a style object and reference it within our component's render method.

Taking it a step further, with larger applications, it's best to separate the style sheet into its own JavaScript file for readability's sake. Let's take a look at how each of these compare, using a very annotated version of the Hello World sample that's generated for us. These samples will contain only the code necessary to make my point.

Inline styles

An inline style is one that is defined within the markup of your code. Check this sample out:

class Tasks extends Component { 
  render () { 
    return ( 
      <View style = {{ flex: 1, justifyContent: 'center',  
        alignItems: 'center', backgroundColor: '#F5FCFF'  
      }}> 
        <Text style = {{ fontSize: 20, textAlign:  
          'center', margin: 10 }}> 
          Welcome to React Native! 
        </Text> 
      </View> 
    ) 
  } 
} 

In the preceding code, you can see how inline style can create a very convoluted and confusing mess, especially when there are several style properties that we want to apply to each component. It's not practical for us to write our styles like this in a large-scale application, so let's break apart the styles into a StyleSheet object.

With StyleSheet, within the same file

This is how a component accesses a StyleSheet created in the same file:

class Tasks extends Component { 
  render () { 
    return ( 
      <View style = { styles.container }> 
        <Text style = { styles.welcome }> 
          Welcome to React Native! 
        </Text> 
      </View> 
    ) 
  } 
} 

const styles = StyleSheet.create({ 
  container: { 
    flex: 1, 
    justifyContent: 'center', 
    alignItems: 'center', 
    backgroundColor: '#F5FCFF' 
  }, 
  welcome: { 
    fontSize: 20, 
    textAlign: 'center', 
    margin: 10 
  } 
)}; 

 

 

This is much better. We're moving our styles into an object we can reference without having to rewrite the same inline styles over and over. However, the problem we face is an extraordinarily long file with a lot of application logic, where a future maintainer might have to scroll through lines and lines of code to get to the styles. We can take it one step further and separate the styles into their own module.

StyleSheet as an imported module

In your component, you can import your styles, as shown:

import styles from './styles.js'; 

class Tasks extends Component { 
  render(){ 
    return ( 
      <View style = { styles.container }> 
        <Text style = { styles.welcome }> 
          Welcome to React Native! 
        </Text> 
      </View> 
    ) 
  } 
} 

Then, you can define them in a separate file:

const styles = StyleSheet.create({ 
  container: { 
    flex: 1, 
    justifyContent: 'center', 
    alignItems: 'center', 
    backgroundColor: '#F5FCFF' 
  }, 
  welcome: { 
    fontSize: 20, 
    textAlign: 'center', 
    margin: 10 
  } 
)}; 

export default styles; 

This is much better. By encapsulating our style logic into its own file, we are separating our concerns and making it easier for everyone to read it.

 

Flexbox


One thing you might have noted in our StyleSheet is a property called flex. This pertains to Flexbox, a CSS layout system that provides consistency in your layout across different screen sizes. Flexbox in React Native works similar to its CSS specification, with only a couple of differences. The most important differences to be noted are that the default flex direction has been flipped to column on React Native, as opposed to row on the Web, aligning items, by default, to the stretch property for React Native instead of flex-start in the browser, and the flex parameter only supports a single number as its value in React Native.

We will pick up a lot on Flexbox as we go through these projects; we'll start by taking a look at just the basics.

flex

The flex property of your layout works a bit differently from how it operates in CSS. In React Native, it accepts a single digit number. If its number is a positive number (meaning greater than 0), the component that has this property will become flexible.

flexDirection

Your layout also accepts a property called flexDirection. There are four options for this: row, row-reverse, column, and column-reverse. These options dictate the direction that the children of your flex container will be laid out in.

 

Writing in ES6


ECMAScript version 6 (ES6) is the latest specification of the JavaScript language. It is also referred to as ES2016. It brings new features and syntax to JavaScript, and they are the ones you should be familiar with to be successful in this book.

Firstly, require statements are now import statements. They are used to import functions, object, and so on from an external module or script. In the past, to include React in a file, we would write something like this:

var React = require('react'); 
var Component = React.Component; 

Using ES6 import statements, we can rewrite it to this:

import React, { Component } from 'react'; 

The importing of Component around a curly brace is called destructuring assignment. It's an assignment syntax that lets us extract specific data from an array or object into a variable. With Component imported through destructuring assignment, we can simply call Component in our code; it's automatically declared as a variable with the exact same name.

Next up, we're replacing var with two different statements: let and const. The first statement, let, declares a block-scoped variable whose value can be mutated. The second statement, const, declares another block-scoped variable whose value cannot change through reassignment nor redeclaration.

In the prior syntax, exporting modules used to be done using module.exports. In ES6, this is done using the export default statement.

 

Building the app


Going back to our list from a few pages back, this is the first thing I'd like to do with the app:

  • Let's generate a list of default items. These don't have to be manually entered; we just want to see our list populated in the app itself.
 

ListView


While looking at the documentation for React Native's components, you may note a component named ListView. This is a core component that is meant to display vertically scrolling lists of data.

Here's how ListView works. We will create a data source, fill it up with an array of data blobs, create a ListView component with that array as its data source, and pass it some JSX in its renderRow callback, which will take the data and render a row for each blob within the data source.

On a high level, here is how it looks:

class TasksList extends Component { 
  constructor (props) { 

    super (props); 

    const ds = new ListView.DataSource({ 
      rowHasChanged: (r1, r2) => r1 !== r2 }); 

    this.state = { 
      dataSource: ds.cloneWithRows(['row 1', 'row 2']) 
    }; 
  } 

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

Let's look at what's going on. In the constructor of our component, we create an instance of ListViewDataSource. The constructor for a new ListViewDataSource accepts, as a parameter, an argument that can contain any of these four:

  • getRowData(dataBlob, sectionID, rowID)
  • getSectionHeaderData(dataBlob, sectionID)
  • rowHasChanged(previousRowData, nextRowData)
  • sectionHeaderHasChanged(previousSectionData, nextSectionData)

The getRowData is a function that gets the data required to render the row. You can customize the function however you like as you pass it in to the constructor of ListViewDataSource, but ListViewDataSource will provide a default if you don't specify.

The getSectionHeaderData is a function that accepts a blob of data and a section ID and returns just the data needed to render a section header. Like getRowData, it provides a default if not specified.

The rowHasChanged is a function that serves as a performance optimization designed to only re-render any rows that have their source data changed. Unlike getRowData and getSectionHeaderData, you will need to pass your own version of rowHasChanged. The preceding example, which takes in the current and previous values of the row and returns a Boolean to show if it has changed, is the most common implementation.

The sectionHeaderHasChanged is an optional function that compares the section headers' contents to determine whether they need to be re-rendered.

Then, in our TasksView constructor, our state receives a property of dataSource whose value is equal to calling cloneWithRows on the ListViewDataSource instance we created earlier. cloneWithRows takes in two parameters: a dataBlob and rowIdentities. The dataBlob is any arbitrary blob of data passed to it, and rowIdentities represents a two-dimensional array of row identifiers. The rowIdentities is an optional parameter--it isn't included in the preceding sample code. Our sample code passes a hardcoded blob of data--two strings: 'row 1' and 'row 2'.

It's also important to mention right now that the data within our dataSource is immutable. If we want to change it later, we'll have to extract the information out of the dataSource, mutate it, and then replace the data within the dataSource.

The ListView component itself, which is rendered in our TasksList, can accept a number of different properties. The most important one, which we're using in our example, is renderRow.

The renderRow function takes data from the dataSource of your ListView and returns a component to render for each row of data in your dataSource. In our preceding example, renderRow takes each string inside our dataSource and renders it in a Text component.

With the preceding code, here is how TasksList will render. Because we have not yet styled it, you will see that the iOS Status Bar overlaps the first row:

Great! There's not much to see, but we accomplished something: we created a ListView component, passed it some data, and got that data to be rendered on our screen. Let's take a step back and create this component in our application properly.

 

Creating the TasksList component


Going back to the proposed file structure from earlier, your project should look like this:

Let's start by writing our first component--the TasksList module.

The first thing we will need to do is import our dependency on React:

import React, { Component } from 'react'; 

Then, we'll import just the building blocks we need from the React Native (react-native) library:

import { 
  ListView, 
  Text 
} from 'react-native'; 

 

 

Now, let's write the component. The syntax for creating a new component in ES6 is as follows:

export default class TasksList extends Component { 
  ... 
} 

From here, let's give it a constructor function to fire during its creation:

export default class TasksList extends Component { 
  constructor (props) { 
    super (props); 
    const ds = new ListView.DataSource({ 
     rowHasChanged: (r1, r2) => r1 !== r2 
    }); 

    this.state = { 
     dataSource: ds.cloneWithRows([ 
        'Buy milk', 
        'Walk the dog', 
        'Do laundry', 
        'Write the first chapter of my book' 
      ]) 
    }; 
  } 
} 

Our constructor sets up a dataSource property in the TasksList state as equal to an array of hardcoded strings. Again, our first goal is to simply render a list on the screen.

Next up, we'll utilize the render method of the TasksList component to do just that:

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

Consolidated, the code should look like this:

// Tasks/app/components/TasksList/index.js 

import React, { Component } from 'react'; 

import { 
  ListView, 
  Text 
} from 'react-native'; 

export default class TasksList extends Component { 
  constructor (props) { 
    super (props); 

    const ds = new ListView.DataSource({ 
      rowHasChanged: (r1, r2) => r1 !== r2 
    }); 

    this.state = { 
      dataSource: ds.cloneWithRows([ 
        'Buy milk', 
        'Walk the dog', 
        'Do laundry', 
        'Write the first chapter of my book' 
      ]) 
    }; 
  } 

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

Great! That should do it. However, we need to link this component over to our application's entry point. Let's hop over to index.ios.js and make some changes.

 

Linking TasksList to index


Our iOS app's entry point is index.ios.js and everything that it renders starts from here. Right now, if you launch iOS Simulator using the react-native run-ios command, you will see the same Hello World sample application that we were acquainted with in the preface.

What we need to do right now is link the TasksList component we just built to the index and remove all the unnecessary JSX automatically generated for us. Let's go ahead and clear nearly everything in the render method of our Tasks component, except the top layer View container. When you're done, it should look like this:

class Tasks extends Component { 
  render () { 
    return ( 
      <View style={styles.container}> 
      </View> 
    ); 
  } 
} 

We'll want to insert TasksList within that View container. However, before we do that, we have to give the index file access to that component. Let's do so using an import statement:

import TasksList from './app/components/TasksList'; 

While this import statement just points to the folder that our TasksList component is in, React Native intelligently looks for a file named index and assigns it what we want.

Now that TasksList is readily available for us to use, let's include it in the render method for Tasks:

export default class Tasks extends Component { 
  render () { 
    return ( 
      <View style={styles.container}> 
        <TasksList /> 
      </View> 
    ); 
  } 
} 

 

 

If you don't have an iOS Simulator running anymore, let's get it back up and running using the react-native run-ios command from before. Once things are loaded, this is what you should see:

This is awesome! Once it's loaded, let's open up the iOS Simulator Developer menu by pressing Command+D on your keyboard and search for an option that will help us save some time during the creation of our app.

At the end of this section, your index.ios.js file should look like this:

// Tasks/index.ios.js 

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

import TasksList from './app/TasksList'; 

export default class Tasks extends Component { 
  render() { 
    return ( 
      <View style={styles.container}> 
        <TasksList /> 
      </View> 
    ); 
  } 
} 

The following code renders the TasksList component:

const styles = StyleSheet.create({ 
  container: { 
    flex: 1, 
    justifyContent: 'center', 
    alignItems: 'center', 
    backgroundColor: '#F5FCFF', 
  } 
}); 

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

 

 

 

The iOS Simulator Developer menu


When you open the Developer menu, you'll see the following options:

I would like to go through some of the options available in this menu, which will help you make the development of your applications a lot smoother. Some of the options are not covered here, but are available for you to read about in the React Native documentation.

First, we will cover the options for reloading:

  • Reload: This reloads your application code. Similar to using Command + R on the keyboard, the Reload option takes you to the beginning of your application flow.
  • Enable Live Reload: Turning Live Reload on will result in your application automatically performing a reload action whenever your code has changed while you save a file in your project. Live Reload is great because you can enable it once and have your app show you its latest changes whenever you save your file. It's important to know that both Reload and Enable Live Reload perform a full reload of your application, including resetting your application state.
  • Enable Hot Reloading: Hot Reloading is a new feature introduced in React Native in March 2016. If you've worked with React on the Web, this term might be familiar to you. The idea of a Hot Reload is to keep your app running and to inject new code at runtime, which prevents you from losing your application state like with a Reload (or, by extension, Enable Live Reload).
    • One of the bottlenecks of building a feature with Live Reload turned on is when you work on a feature several layers deep and rely on your application's state to properly note changes to it. This adds several seconds to the feedback loop of writing and reloading your application. A Hot Reload solves this issue, letting your feedback loop be reduced to less than a second or two.
    • Something to be aware of with Hot Reloading is that, in its current iteration, it's not perfect. The React Native documentation notes that, in some instances, you will need to use a regular Reload to reset your app when Hot Reloading fails.

It's equally important to know that if you ever add new assets to your application or modify native Objective-C/Swift or Java/C++ code, your application will need to be fully rebuilt before the changes will take effect.

The next set of options have to do with debugging:

  • Debug JS Remotely: Enabling this will open up Chrome on your machine and take you to a Chrome tab that will allow you to use Chrome Developer Tools to debug your application.
  • Show Inspector: Similar to inspecting an element on the Web, you can use the Inspector in React Native development to inspect any element of your application and have it open up parts of your code and the source code that affect that element. You can also view the performance of each specific element this way.

Using the Developer menu, we will enable Hot Reloading. It will give us the quickest feedback loop on the code we're writing, allowing us to move efficiently.

Now that we've got Hot Reloading enabled and a basic list of tasks rendering to the screen, it's time to think about an input--we'll come back to styling later.

 

TextInput


The second goal for building out an MVP was as follows:

  • Our users should be able to input their own tasks using a text field and the native keyboard

To successfully create this input, we have to break down the problem into some necessary requirements:

  • We need to have an input field that will spring up our keyboard to type with
  • The keyboard should hide itself when we tap outside of it
  • When we successfully add a task, it needs to be added to dataSource in TasksList, which is stored in its state
  • The list of tasks needs to be stored locally in the application so that a state reset doesn't delete the entire list of tasks we've created
  • There're also a couple of forks in the road we should address:
    • What happens when the user hits return on the keyboard? Does that automatically create a task? Alternatively, do we implement and support a line break?
    • Is there a dedicated Add this task button?
    • Does the successful act of adding a task cause the keyboard to go away, requiring the user to tap on the input field again? Alternatively, do we allow the user to keep adding tasks until they tap outside the keyboard?
    • How many characters do we support? How long is too long for a task? What kind of feedback is presented to the user of our software if they exceed that limit?

This is a lot to take in, so let's take it one step at a time! I will propose that we ignore the big decisions for now and have the simple act of having an input on the screen, and then having that input be added to our list of tasks.

Since input should be saved to state and then rendered in the ListView, it makes sense for the input component to be a sibling of the ListView, allowing them to share the same state.

Architecturally, this is how the TasksList component will look:

|TasksList 
|__TextInput 
|__ListView 
|____RowData 
|____RowData 
|____... 
|____RowData 

React Native has a TextInput component in its API that fulfills our need for a keyboard input. Its code is customizable and will allow us to take input and add it to our list of tasks.

This TextInput component can accept a multitude of props. I have listed the ones we will use here, but the documentation for React Native will provide much more depth:

  • autoCorrect: This is a Boolean that turns autocorrection on and off. It is set to true by default
  • onChangeText: This is a callback that is fired when the input field's text changes. The value of the component is passed as an argument to the callback
  • onSubmitEditing: This is a callback that is fired when a single-line input's submit button is pressed
  • returnKeyType: This sets the title of the return key to one of many different strings; done, go, next, search, and send are the five that work across both the platforms

We can break down the task at hand into a couple of bite-sized steps:

  • Update container styling in index.ios.js so that its contents take up the entire screen and not just the center
  • Add a TextInput component to our TasksList component's render method
  • Create a submit handler for the TextInput component that will take the value of the text field and add it to ListView
  • Clear the contents of the TextInput once submitted, leaving a blank field for the next task to be added

Take some time to try and add this first feature into our app! In the next section, I will share some screenshots of my results and break down the code I wrote for it.

Here's a screen to show how my input looks at this stage:

It meets the four basic requirements listed in the preceding section: the contents aren't centered on the screen, a TextInput component is rendered at the top, the submit handler takes the value of the TextInput component and adds it to the ListView, and the contents of the TextInput are emptied once that happens.

Let's look at the code to see how I tackled it--yours may be different!:

// Tasks/index.ios.js 

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

import TasksList from './app/components/TasksList'; 

export default class Tasks extends Component { 
  render() { 
    return ( 
      <View> 
        <TasksList /> 
      </View> 
    ); 
  } 
} 

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

This is the updated styling for TasksList:

// Tasks/app/components/TasksList/styles.js

import { StyleSheet } from 'react-native'; 

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

export default styles; 

What I did here was remove the justifyContent and alignItems properties of the container so that items weren't constrained to just the center of the display.

Moving on to the TasksList component, I made a couple of major changes:

// Tasks/app/components/TasksList/index.js 

import React, { Component } from 'react'; 

import { 
  ListView, 
  Text, 
  TextInput, 
  View 
} from 'react-native'; 

import styles from './styles'; 

export default class TasksList extends Component { 
  constructor (props) { 
    super (props); 

    const ds = new ListView.DataSource({ 
      rowHasChanged: (r1, r2) => r1 !== r2 
    }); 

    this.state = { 
      ds: new ListView.DataSource({ 
        rowHasChanged: (r1, r2) => r1 !== r2 
      }), 
      listOfTasks: [], 
      text: '' 
    }; 
  } 

The constructor now saves three things to state: our local instance of ListView.DataSource, an empty string to keep track of the value of TextInput, and an array to store the list of tasks.

The render function creates a reference to a dataSource that we will use for our ListView component, cloning the listOfTasks array stored in state. Once again, the ListView just presents plain text:

  render () { 
    const dataSource = 
    this.state.ds.cloneWithRows(this.state.listOfTasks); 

The TextInput component has a couple of options. It binds the value of its input field to the text value of our state, changing it repeatedly as the field is edited. On submitting it by pressing the done key on the keyboard, it fires a callback called _addTask:

    return ( 
      <View style={ styles.container }> 
        <TextInput 
          autoCorrect={ false } 
          onChangeText={ (text) => this._changeTextInputValue(text) } 
          onSubmitEditing={ () => this._addTask() } 
          returnKeyType={ 'done' } 
          style={ styles.textInput } 
          value={ this.state.text } 
        /> 

It renders a ListView component with the _renderRowData method being responsible for returning each individual row of the component:

        <ListView 
          dataSource={ dataSource } 
          enableEmptySections={ true } 
          renderRow={ (rowData) => this._renderRowData(rowData) } 
        /> 
      </View> 
    ); 
  } 

I like to start the name of methods that I personally create in a React component with an underscore so that I can visually distinguish them from the default life cycle methods.

The _addTask method uses the array spread operator introduced in ES6 to create a new array and copy over an existing array's values, adding the newest task to the list at the end. Then, we assign it to the listOfTasks property in state. Remember that we have to treat our component state as an immutable object and simply pushing to it will be an anti-pattern:

_addTask () { 
    const listOfTasks = [...this.state.listOfTasks, this.state.text]; 

    this.setState({ 
      listOfTasks 
    }); 

    this._changeTextInputValue('' 
  } 

Finally, we call _changeTextInputValue so that the TextInput box is emptied:

  _changeTextInputValue (text) { 
    this.setState({ 
      text 
    }); 
  } 
  _renderRowData (rowData) { 
    return ( 
      <Text>{ rowData }</Text> 
    ) 
  } 
} 

For now, just returning the name of the to-do list item is fine.

When setting the listOfTasks property in the _addTask method and the text property in _changeTextInputValue, I'm using a new notation feature of ES6, called shorthand property names, to assign a value to a key with the same name as the value. This is the same as if I were to write as follows:

this.setState({ 
  listOfTasks: listOfTasks, 
  text: text 
}) 

Moving on, you might note that, as you refresh the application, you lose your state! This is impractical for a to-do list app, since we should never expect the user to re-enter the same list whenever they re-open the app. What we want is to store this list of tasks locally in the device so that we can access it whenever needed. This is where AsyncStorage comes into play.

 

AsyncStorage


The AsyncStorage component is a simple key-value store that is globally available to your React Native application. It's persistent, meaning that data within AsyncStorage will continue to exist through quitting or restarting the application or your phone. If you've worked with HTML LocalStorage and SessionStorage, AsyncStorage will seem familiar. It's powerful for light usage, but Facebook recommends that you use an abstraction layer on top of AsyncStorage for anything more than that.

As the name implies, AsyncStorage is asynchronous. If you haven't yet been introduced to asynchronous JavaScript, this means the methods of this storage system can run concurrently with the rest of your code. The methods of AsyncStorage return a Promise--an object that represents an operation that hasn't yet completed, but is expected to in the future.

Each of the methods in AsyncStorage can accept a callback function as an argument, and will fire that callback once the Promise is fulfilled. This means that we can write our TasksList component to work around these promises, saving and retrieving our array of tasks when needed.

One final thing about AsyncStorage though--it's a simple key-value store. It expects a string for both its key and value, which means that we'll need to transform the data we send using JSON.stringify to turn the array into a string when sending it into storage and JSON.parse to transform it back into an array when retrieving it.

Play with AsyncStorage and update your TasksList component to support it. Here are some goals you'll want to have with AsyncStorage:

  • Once TasksList is loaded, we want to see whether any tasks exist locally in storage. If they do, present this list to the user. If they don't, start off with an empty array for storage. Data should always persist through a restart.
  • When a task is entered, we should update the list of tasks, save the updated list into AsyncStorage, and then update the ListView component.

Here's the code I ended up writing:

// TasksList/app/components/TasksList/index.js

... 
import { 
  AsyncStorage, 
  ... 
} from 'react-native'; 
... 

Import the AsyncStorage API from the React Native SDK.

export default class TasksList extends Component { 
  ... 
  componentDidMount () { 
    this._updateList(); 
  } 

Call the _updateList method during the componentDidMount life cycle.

  ... 
  async _addTask () { 
    const listOfTasks = [...this.state.listOfTasks, this.state.text]; 

    await AsyncStorage.setItem('listOfTasks', 
    JSON.stringify(listOfTasks)); 

    this._updateList(); 
  } 

 

 

Update _addTask to use the async and await keywords as well as AsyncStorage. Refer to the following for details on using async and await:

  ... 
  async _updateList () { 
    let response = await AsyncStorage.getItem('listOfTasks'); 
    let listOfTasks = await JSON.parse(response) || []; 

    this.setState({ 
      listOfTasks 
    }); 

    this._changeTextInputValue(''); 
  } 
} 

What we are doing with AsyncStorage in _updateTask is grabbing the value locally stored using the listOfTasks key. From here, we parse the result, transforming the string back into an array. Then, we check to see whether the array exists and set it to an empty array if it returns null. Finally, we set the state of our component by updating listOfTasks and firing _changeTextInputValue to reset TextInput value.

The preceding example also uses the new async and await keywords that are part of the ES7 specification proposal and readily available to use with React Native.

Using the Async and Await keywords

Normally, to deal with an asynchronous function, we would chain some promises to it in order to grab our data. We can write _updateList, like this:

_updateList () { 
  AsyncStorage.getItem('listOfTasks'); 
    .then((response) => {fto 
      return JSON.parse(response); 
    }) 
    .then((parsedResponse) => { 
      this.setState({ 
        listOfTasks: parsedResponse 
      }); 
    }); 
} 

 

 

However, this can become quite complicated. Instead, we will use the async and await keywords to create a simpler solution:

async _updateList () { 
  let response = await AsyncStorage.getItem('listOfTasks'); 
  let listOfTasks = await JSON.parse(response) || []; 

  this.setState({ 
    listOfTasks 
  }); 

  this._changeTextInputValue(''); 
} 

The async keyword in front of _updateList declares it as an asynchronous function. It automatically returns promises for us and can take advantage of the await keyword to tell the JS interpreter to temporarily exit the asynchronous function and resume running when the asynchronous call is completed. This is great for us because we can express our intent in a sequential order in a single function and still receive the exact same results that we would enjoy with a promise.

 

Custom RenderRow component


The final thing on our list to have a usable minimum viable product is to allow each task to be marked as complete. This is where we'll create the TasksListCell component and render that in our renderRow function of ListView instead of just the text.

Our goals for this component should be as follows:

  • Accept text from the parent component as a prop, rendering it in TasksListCell
  • Update listOfTasks to take in an array of objects rather than an array of strings, allowing each object to track the name of the task and whether or not it's completed
  • Provide some sort of visual indicator when a task is tapped, marking it as complete both visually and within the task's data object, so this persists through application reloads

Custom RenderRow example

Let's look at how I created this component:

// Tasks/app/components/TasksList/index.js 

... 
import TasksListCell from '../TasksListCell'; 
... 
export default class TasksList extends Component { 
  ... 
  async _addTask () { 
    const singleTask = { 
      completed: false, 
      text: this.state.text 
    } 

Firstly, tasks are now represented as objects within the array. This allows us to add properties to each task, such as its completed state, and leaves room for future additions.

    const listOfTasks = [...this.state.listOfTasks, singleTask]; 
    await AsyncStorage.setItem('listOfTasks', 
    JSON.stringify(listOfTasks)); 
    this._updateList(); 
  } 
  ... 
  _renderRowData (rowData, rowID) { 
    return ( 
      <TasksListCell 
        completed={ rowData.completed } 
        id={ rowID } 
        onPress={ (rowID) => this._completeTask(rowID) } 
        text={ rowData.text } 
      /> 
    ) 
  } 
  ... 
} 

The _renderRowData method is also updated to render a new TasksListCell component. Four props are shared to TasksListCell: the task's completed state, its row identifier (provided by renderRow), a callback to alter the task's completed state, and the details of that task itself.

Here's how that TasksListCell component was written:

// Tasks/app/components/TasksListCell/index.js 

import React, { Component, PropTypes } from 'react'; 

import { 
  Text, 
  TouchableHighlight, 
  View 
} from 'react-native'; 

export default class TasksListCell extends Component { 
  static propTypes = { 
    completed: PropTypes.bool.isRequired, 
    id: PropTypes.string.isRequired, 
    onLongPress: PropTypes.func.isRequired, 
    onPress: PropTypes.func.isRequired, 
    text: PropTypes.string.isRequired 
  } 

Use PropTypes to explicitly declare the data this component expects to be given. Read on for an explanation on prop validation in React.

  constructor (props) { 
    super (props); 
  } 

  render () { 
    const isCompleted = this.props.completed ? 'line-through' : 'none'; 
    const textStyle = { 
      fontSize: 20, 
      textDecorationLine: isCompleted 
    }; 

Use a ternary operator to calculate styling for a task if it is completed.

    return ( 
      <View> 
        <TouchableHighlight 
          onPress={ () => this.props.onPress(this.props.id) } 
          underlayColor={ '#D5DBDE' } > 
          <Text style={ textStyle }>{ this.props.text }</Text> 
        </TouchableHighlight> 
      </View> 
    ) 
  } 
} 

The preceding component provides a TouchableHighlight for each task on the list, giving us visual opacity feedback when an item is tapped on. It also fires the _completeTask method of TasksListCell, which subsequently calls the onPress prop that was passed to it and makes a visual change to the style of the cell, marking it completed with a line through the horizontal center of the task.

 

Prop validation in React


By declaring a propTypes object for a component, I can specify the expected props and their types for a given component. This is helpful for future maintainers of our code and provides helpful warnings when props are incorrectly entered or missing.

To take advantage of prop validation, first import the PropTypes module from React:

import { PropTypes } from 'react'; 

Then, in our component, we give it a static property of propTypes:

class Example extends Component { 
  static propTypes = { 
    foo: PropTypes.string.isRequired, 
    bar: PropTypes.func, 
    baz: PropTypes.number.isRequired 
  } 
} 

In the preceding example, foo and baz are the required props for the Example component. foo is expected to be a string, while baz is expected to be a number. bar, on the other hand, is expected to be a function but is not a required prop.

 

Moving beyond MVP


Now that we have a very bare-bones MVP completed, the next goal is to add some features to the application so that it's fully-fledged.

Here's what I wrote earlier regarding some nice-to-have features:

Note

I'd like to set a reminder for each unique task so that I can get to each one in an orderly fashion. Ideally, the items on the list can be grouped into categories. Category grouping could perhaps be simplified by something like icons. This way, I can also sort and filter my list by icons.

In addition to the features, we should tweak the styling of the application so that it looks better. In my sample code, the app's components conflict with the iOS's status bar and the rows aren't formatted at all. We should give the app its own identity.

The next chapter will dive deeper into our MVP and transform it into a fully-featured and styled application. We'll also look at things we would do differently if the app were written for Android instead.

 

Summary


In this chapter, you started out strong by planning a minimum viable product version of a to-do list app, complete with adding tasks to the list and marking them as completed. Then, you learned about basic styling in React Native with Flexbox and became acquainted with new syntax and functionalities of the ES6 specification. You also discovered the iOS simulator debugging menu, which is a helpful tool for writing apps.

Afterward, you created a ListView component to render an array of items, and then implemented a TextInput component to save user input and render that into the Listview. Then, you used AsyncStorage to persist the data added to the app by the user, utilizing the new async and await keywords to write clean asynchronous functions. Finally, you implemented a TouchableHighlight cell that marks tasks as completed.

About the Author

  • Richard Kho

    Richard Kho is a software engineer living in San Francisco. He taught himself how to code in 2014 and has lived a past life as a photographer and cinematographer.

    He currently works for Capital One and has taught software engineers at Hack Reactor in the past. Richard is also a technical advisor to Code Chrysalis, an advanced software engineering immersive in Tokyo.

    Browse publications by this author

Latest Reviews

(1 reviews total)
it's book need an updates tbh

Recommended For You

Book Title
Access this book, plus 7,500 other titles for FREE
Access now