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
withTextInput
,ListView
,AsyncStorage
,Input
, state, and props - Learning about the iOS Simulator's
Developer
menu, which can help us during the writing of our app
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.
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:
- 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.
- After that, your users should be able to input their own tasks using a text field and the native keyboard.
- Next, I'd like to make that list scrollable in case my list of tasks spans past an entire vertical screen's height.
- 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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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);
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, theReload
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 bothReload
andEnable 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 aReload
(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 theInspector
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.
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
inTasksList
, 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 totrue
by defaultonChangeText
: 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 callbackonSubmitEditing
: This is a callback that is fired when a single-line input's submit button is pressedreturnKeyType
: This sets the title of the return key to one of many different strings;done
,go
,next
,search
, andsend
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 ourTasksList
component'srender
method - Create a submit handler for the
TextInput
component that will take the value of the text field and add it toListView
- 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.
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 theListView
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.
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.
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
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.
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.
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.
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.