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:
StyleSheet
, the React Native component for working with stylesTasks
with TextInput
, ListView
, AsyncStorage
, Input
, state, and propsDeveloper
menu, which can help us during the writing of our appWith 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:
With these intentions in mind, here's what I've come up with:
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:
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, 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
).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.
The second goal for building out an MVP was as follows:
To successfully create this input, we have to break down the problem into some necessary requirements:
dataSource
in TasksList
, which is stored in its stateThis 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 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
, and send
are the five that work across both the platformsWe can break down the task at hand into a couple of bite-sized steps:
index.ios.js
so that its contents take up the entire screen and not just the centerTextInput
component to our TasksList
component's render
methodTextInput
component that will take the value of the text field and add it to ListView
TextInput
once submitted, leaving a blank field for the next task to be addedTake 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
:
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.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.
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:
TasksListCell
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 completeddata
object, so this persists through application reloadsLet'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:
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.
Where there is an eBook version of a title available, you can buy it from the book details for that title. Add either the standalone eBook or the eBook and print book bundle to your shopping cart. Your eBook will show in your cart as a product on its own. After completing checkout and payment in the normal way, you will receive your receipt on the screen containing a link to a personalised PDF download file. This link will remain active for 30 days. You can download backup copies of the file by logging in to your account at any time.
If you already have Adobe reader installed, then clicking on the link will download and open the PDF file directly. If you don't, then save the PDF file on your machine and download the Reader to view it.
Please Note: Packt eBooks are non-returnable and non-refundable.
Packt eBook and Licensing When you buy an eBook from Packt Publishing, completing your purchase means you accept the terms of our licence agreement. Please read the full text of the agreement. In it we have tried to balance the need for the ebook to be usable for you the reader with our needs to protect the rights of us as Publishers and of our authors. In summary, the agreement says:
If you want to purchase a video course, eBook or Bundle (Print+eBook) please follow below steps:
Our eBooks are currently available in a variety of formats such as PDF and ePubs. In the future, this may well change with trends and development in technology, but please note that our PDFs are not Adobe eBook Reader format, which has greater restrictions on security.
You will need to use Adobe Reader v9 or later in order to read Packt's PDF eBooks.
Packt eBooks are a complete electronic version of the print edition, available in PDF and ePub formats. Every piece of content down to the page numbering is the same. Because we save the costs of printing and shipping the book to you, we are able to offer eBooks at a lower cost than print editions.
When you have purchased an eBook, simply login to your account and click on the link in Your Download Area. We recommend you saving the file to your hard drive before opening it.
For optimal viewing of our eBooks, we recommend you download and install the free Adobe Reader version 9.