Build a To Do app with React Native and Expo
To follow this tutorial you will need a basic understanding of JavaScript, Node.js and React Native
Build a to do app with React Native and Expo
React Native is a framework for building native mobile apps using JavaScript. React Native is based on the same core concepts as ReactJS, giving you, the developer, the power to compose a cross-platform mobile UI by writing JavaScript components.
React Native differs from other hybrid mobile app solutions. It does not use a WebView that renders HTML elements inside an app. It has its own API and by using it, you build mobile apps with native iOS/Android UI components. React Native apps are written in JavaScript. Behind the scenes, React Native is a bridge between JavaScript and other native platform specific components.
In this article, we are going to build a to do application to understand and get hands-on experience with React Native. This mobile application will be cross-platform meaning it will run both on Android and iOS devices. I am going to use Expo for faster development to generate and run the demo in no time. Expo will take care of all the behind the scenes things for us such adding native modules when using vector icons in the demo application. You are only going to focus on the development process for a deeper understanding.
Prerequisites
To get started you will need three things to follow this article.
npm install -g expo-cli
Why use Expo?
You should consider using Expo for a React Native application because it handles a lot of hard tasks itself and provides smooth APIs that work with a React Native app outside the box. It is open source and is free to use. It provides a client app and by downloading it from the respective stores based on the mobile platform your device runs, you can easily test applications on real devices.
That said, Expo also has some drawbacks. For example, Expo’s API currently does not have support for features like Bluetooth. It works fine with camera, maps, location tracking, analytics, push notifications and so on. Distributing an Expo app is easy too. You can complete the process just by running the command expo publish
and it will handle the build process and other tasks by running them behind the scene. It has a dedicated store where you can publish apps for others to use. Quite helpful in prototyping.
Side note: Why not Create-React-Native-App?
Just like React, React Native has its own boilerplate that depends on Expo for a faster development process, called create-react-native-app. It works with zero build configuration just like Expo. Recently, the CRNA project has been merged with expo-cli
project since both are identical in working.
What are we building?
Getting started
Write the following command in your terminal to start a project.
expo init rn 'To Do' s-example
When Expo’s command line interface completes running the package manager, it generates a directory with name you gave in the above command. Open your favorite text editor/IDE and go to a file called App.js
. This is what runs the application. You can test the content of the default app generated by running the following command.
expo-cli start
The below is what you will get in your terminal. It runs the bundler which further triggers the execution of the application. Depending on the OS you are on, you can either use iOS simulator or Android emulator to run this application in development mode. The third option is to install the Expo client on your real device and scan the QR code as shown.
By default, the code in App.js
looks like this:
import React from 'react';
import { StyleSheet, Text, View } from 'react-native';
export default class App extends React.Component {
render() {
return (
<View style={styles.container}>
<Text>Open up App.js to start working on your app!</Text>
</View>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
alignItems: 'center',
justifyContent: 'center'
}
});
Once you run the app in its current state, you will see the following result.
We will replace it with following:
// App.js
import React from 'react';
import Main from './app/Main';
export default class App extends React.Component {
render() {
return <Main />;
}
}
Take a look at the directory structure of our demo app.
In a more complex application, you will find a folder called screens
. Since we are using only one screen in the file Main.js
you do not have to define it explicitly.
Did you notice the other two directories: utils
and components
?
Inside the utils
directory, I am keeping all the global variables or API calls we need to make. Though in our demo there are no external API calls. I have defined some global variables. Name this file, Colors.js
.
// app/utils/Colors.js
const primaryStart = '#f18a69';
const primaryEnd = '#d13e60';
export const primaryGradientArray = [primaryStart, primaryEnd];
export const lightWhite = '#fcefe9';
export const inputPlaceholder = '#f1a895';
export const lighterWhite = '#f4e4e2';
export const circleInactive = '#ecbfbe';
export const circleActive = '#90ee90';
export const itemListText = '#555555';
export const itemListTextStrike = '#c4c4cc';
export const deleteIconColor = '#bc2e4c';
It contains all the hex values of colors that we can re-use in many different places of our application. Defining global variables for the purpose of re-using them is a common practice in React Native community.
The components
directory further contain re-usable components used in our to do application.
Building a header
To build the header for our application, we need three things: status bar, background color (we are going to use the same background for the whole screen instead of just header) and header title itself. Let’s start with the status bar. Notice the status bar of our application. We are changing it to white so that it will be acceptable once we add a background to our Main screen.
This can be done by importing the StatusBar
component from react-native
. We will be using barStyle
prop to change color. For only Android devices, you can change the height of the status bar by using currentHeight
prop. iOS does not allow this.
For the background, I am going to add a gradient style to our view component. Expo supports this out of the box and you can directly import the component and use it like below.
// App.js
import React from 'react';
import { StyleSheet, Text, View, StatusBar } from 'react-native';
import { LinearGradient } from 'expo';
import { primaryGradientArray } from './utils/Colors';
export default class Main extends React.Component {
render() {
return (
<LinearGradient colors={primaryGradientArray} style={styles.container}>
<StatusBar barStyle="light-content" />;
<Text>Open up App.js to start working on your app!</Text>
</LinearGradient>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1
}
});
LinearGradient
component is a wrapper over the React Native’s View
core component. It provides a gradient looking background. It takes at least two values in the array colors
as props. We are importing the array from utitls/Colors.js
. Next, we create re-usable Header
component inside the components
directory.
// app/components/Header.js
import React from 'react';
import { View, Text, StyleSheet } from 'react-native';
const Header = ({ title }) => (
<View style={styles.headerContainer}>
<Text style={styles.headerText}>{title.toUpperCase()}</Text>
</View>
);
const styles = StyleSheet.create({
headerContainer: {
marginTop: 40
},
headerText: {
color: 'white',
fontSize: 22,
fontWeight: '500'
}
});
export default Header;
Import it in Main.js
and add a title of your app.
// app/Main.js
// after all imports
import Header from './components/Header';
const headerTitle = 'To Do';
// after status bar, replace the <Text> with
<View style={styles.centered}>
<Header title={headerTitle} />
</View>;
// add styles
centered: {
alignItems: 'center';
}
Observe that we are passing the title of the app as a prop to Header
component. You can definitely use the same component again in the application if needed.
TextInput
In React Native, to record the user input we use TextInput
. It uses the device keyboard, or in case of a simulator, you can use the hardware keyboard too. It has several configurable props with features such as auto-correction, allow multi-line input, placeholder text, set the limit of characters to be entered, different keyboard styles and so on. For our to do app, we are going to use several of these features.
// app/components/Input.js
import React from 'react';
import { StyleSheet, TextInput } from 'react-native';
import { inputPlaceholder } from '../utils/Colors';
const Input = ({ inputValue, onChangeText, onDoneAddItem }) => (
<TextInput
style={styles.input}
value={inputValue}
onChangeText={onChangeText}
placeholder="Type here to add note."
placeholderTextColor={inputPlaceholder}
multiline={true}
autoCapitalize="sentences"
underlineColorAndroid="transparent"
selectionColor={'white'}
maxLength={30}
returnKeyType="done"
autoCorrect={false}
blurOnSubmit={true}
onSubmitEditing={onDoneAddItem}
/>
);
const styles = StyleSheet.create({
input: {
paddingTop: 10,
paddingRight: 15,
fontSize: 34,
color: 'white',
fontWeight: '500'
}
});
export default Input;
Ignore the props for now that are incoming from its parent component. For a while focus only on the props it has. Let us go through each one of them.
- value: the value of the text input. By default, it will be an empty string since we are using the local state to set it. As the state updates, the value of the text input updates.
- onChangeText: is a callback that is called when the text input’s text changes. Changed text is passed as an argument to the callback handler.
- placeholder: just like in HTML, placeholder is to define a default message in the input field indicating as if what is expected.
- placeholderTextColor: the custom text color of the placeholder string.
- returnKeyType: determines how the return key on the device’s keyboard should look. You can find more values or platform specific values here. Some of the values are specific to each platform.
- autoCorrect: this prop let us decide whether to show the autocorrect bar along with keyboard or not. In our case, we have set it to false.
- multiline: if true, the text input can be multiple lines. Like we have set in above.
- maxlength: helps you define the maximum number of characters that you can allow for the user to enter.
- autoCapitalize: to automatically capitalize certain characters. We are passing
sentences
as the default value. This means, every new sentence will automatically have its first character capitalized. - underlineColorAndroid: works only with android. It prompts sets a bottom border or an underline.
- blurOnSubmit: In case of multiline TextInput field, this behaves as when pressing return key, it will blur the field and trigger the
onSubmitEditing
event instead of inserting a newline into the field. - onSubmitEditing: contains the business the logic in form of a callback as to what to do when the return key or input’s submit button is pressed. We will be defining this callback in
Main.js
.
To add this component to Main.js
you will have to import it. The props we are passing to the Input
component at inputValue
are from the state of Main
. Other such as onChangeText
is a custom method. Define them inside the Main
component.
// app/Main.js
import React from 'react';
import { StyleSheet, Text, View, StatusBar } from 'react-native';
import { LinearGradient } from 'expo';
import { gradientStart, gradientEnd } from './utils/Colors';
import Header from './components/Header';
import Input from './components/Input';
const headerTitle = 'To Do';
export default class Main extends React.Component {
state = {
inputValue: ''
};
newInputValue = value => {
this.setState({
inputValue: value
});
};
render() {
const { inputValue } = this.state;
return (
<LinearGradient
colors={[gradientStart, gradientEnd]}
style={styles.container}
>
<StatusBar barStyle="light-content" />
<View style={styles.centered}>
<Header title={headerTitle} />
</View>
<View style={styles.inputContainer}>
<Input inputValue={inputValue} onChangeText={this.newInputValue} />
</View>
</LinearGradient>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1
},
centered: {
alignItems: 'center'
},
inputContainer: {
marginTop: 40,
paddingLeft: 15
}
});
Building the list component
To add the value from the Input
component and display it on the screen, we are going to use the below code. Create a new file called List.js
inside the components directory.
// app/components/List.js
import React, { Component } from 'react';
import {
View,
Text,
Dimensions,
StyleSheet,
TouchableOpacity,
Platform
} from 'react-native';
import { MaterialIcons } from '@expo/vector-icons';
import {
itemListText,
itemListTextStrike,
circleInactive,
circleActive,
deleteIconColor
} from '../utils/Colors';
const { height, width } = Dimensions.get('window');
class List extends Component {
onToggleCircle = () => {
const { isCompleted, id, completeItem, incompleteItem } = this.props;
if (isCompleted) {
incompleteItem(id);
} else {
completeItem(id);
}
};
render() {
const { text, deleteItem, id, isCompleted } = this.props;
return (
<View style={styles.container}>
<View style={styles.column}>
<TouchableOpacity onPress={this.onToggleCircle}>
<View
style={[
styles.circle,
isCompleted
? { borderColor: circleActive }
: { borderColor: circleInactive }
]}
/>
</TouchableOpacity>
<Text
style={[
styles.text,
isCompleted
? {
color: itemListTextStrike,
textDecorationLine: 'line-through'
}
: { color: itemListText }
]}
>
{text}
</Text>
</View>
{isCompleted ? (
<View style={styles.button}>
<TouchableOpacity onPressOut={() => deleteItem(id)}>
<MaterialIcons
name="delete-forever"
size={24}
color={deleteIconColor}
/>
</TouchableOpacity>
</View>
) : null}
</View>
);
}
}
const styles = StyleSheet.create({
container: {
width: width - 50,
flexDirection: 'row',
borderRadius: 5,
backgroundColor: 'white',
height: width / 8,
alignItems: 'center',
justifyContent: 'space-between',
marginVertical: 5,
...Platform.select({
ios: {
shadowColor: 'rgb(50,50,50)',
shadowOpacity: 0.8,
shadowRadius: 2,
shadowOffset: {
height: 2,
width: 0
}
},
android: {
elevation: 5
}
})
},
column: {
flexDirection: 'row',
alignItems: 'center',
width: width / 1.5
},
text: {
fontWeight: '500',
fontSize: 16,
marginVertical: 15
},
circle: {
width: 30,
height: 30,
borderRadius: 15,
borderWidth: 3,
margin: 10
},
button: {
marginRight: 10
}
});
export default List;
Our List
component uses TouchableOpactiy
from React Native that behaves like a button but responds to touch on a mobile rather than a normal button as we use in web. It also makes use of different colors that we defined earlier. We are also defining a method called toggleCircle
that will respond to the onPress
action on TouchableOpacity
that accordingly respond by checking or unchecking the to do list item.
@expo/vector-icons
is provided by Expo to add icons from different libraries such as FontAwesome, IonIcons, MaterialIcons, etc. This is where Expo comes in handy. You do not have to add most of the third party npm
packages manually in our app. The vector icons are also available as third party library called react-native-vector-icons and are already included in the Expo core.
Dimensions
is a component that helps us to set the initial width and height of a component before the application runs. We are using its get()
method to acquire any device’s width and height.
React Native provides an API module called Platform
that detects the platform on which the app is running. You can use the detection logic to implement platform-specific code for styling just like we did above or with any other component. To use Platform
module, we have to import it from React Native. We are using it to apply styles in the form of shadow that will appear under the every row component when a to do item is being add.
To make this work, we are going to use ScrollView
lists and import this component as a child in Main.js
.
<View style={styles.list}>
<ScrollView contentContainerStyle={styles.scrollableList}>
{Object.values(allItems)
.reverse()
.map(item => (
<List
key={item.id}
{...item}
deleteItem={this.deleteItem}
completeItem={this.completeItem}
incompleteItem={this.incompleteItem}
/>
))}
</ScrollView>
</View>
ScrollView
is a wrapper on the View
component that provides the user interface for scrollable lists inside a React Native app. It is a generic scrolling container that can host multiple other components and views. It works both ways, vertical by default and horizontal
by setting the property itself. We will be using this component to display the list of to do items, just after the Input
.
To provide styles to it, it uses a prop called contentContainerStyle
.
// app/Main.js
list: {
flex: 1,
marginTop: 70,
paddingLeft: 15,
marginBottom: 10
},
scrollableList: {
marginTop: 15
},
Don’t worry if you don’t understand all the code inside the ScrollView
component. Our next step is to add some custom methods and interact with realtime data, after that you will be able familiar with all the pieces.
Understanding AsyncStorage
According to the React Native documentation , AsyncStorage
is defined as:
a simple, unencrypted, asynchronous, persistent, key-value storage system that is global to the app. It should be used instead of LocalStorage.
On iOS, AsyncStorage is backed by native code that stores small values in a serialized dictionary and larger values in separate files. On Android, AsyncStorage will use either RocksDB or SQLite based on what is available.
The CRUD operations are going to be used in the application using AsyncStorage such that our application is able to perform these operations with realtime data on the device. We are going to associate multiple operations for each to do item in the list, such as adding, deleting, editing and so on, as basically these are CRUD operations. We are going to use objects instead of an array to store these items. Operating CRUD operations on an Object
is going to be easier in our case. We will be identifying each object through a unique ID. In order to generate unique IDs we are going to install a module called uuid
.
In order to proceed, first we need to run this command:
npm install
# after it runs successfully,
npm install --save uuid
The structure of each to do item is going to be like this:
232390: {
id: 232390, // same id as the object
text: 'New item', // name of the To Do item
isCompleted: false, // by default
createdAt: Date.now()
}
We are going to perform CRUD operations in our application to work on an object instead of an array. To read values from an object we are using Object.values(allItems)
, where allItems
is the object that stores all to do list items. We have to add it as an empty object in our local state. This also allows us to map()
and traverse each object inside it just like an array. Another thing we have to implement before we move on to CRUD operations is to add the new object of a to do item when created at the end of the list. For this we can use reverse()
method from JavaScript. This is how our complete Main.js
file looks like.
// app/Main.js
import React from 'react';
import {
StyleSheet,
View,
StatusBar,
ActivityIndicator,
ScrollView,
AsyncStorage
} from 'react-native';
import { LinearGradient } from 'expo';
import uuid from 'uuid/v1';
import { primaryGradientArray } from './utils/Colors';
import Header from './components/Header';
import SubTitle from './components/SubTitle';
import Input from './components/Input';
import List from './components/List';
import Button from './components/Button';
const headerTitle = 'To Do';
export default class Main extends React.Component {
state = {
inputValue: '',
loadingItems: false,
allItems: {},
isCompleted: false
};
componentDidMount = () => {
this.loadingItems();
};
newInputValue = value => {
this.setState({
inputValue: value
});
};
loadingItems = async () => {
try {
const allItems = await AsyncStorage.getItem('ToDos');
this.setState({
loadingItems: true,
allItems: JSON.parse(allItems) || {}
});
} catch (err) {
console.log(err);
}
};
onDoneAddItem = () => {
const { inputValue } = this.state;
if (inputValue !== '') {
this.setState(prevState => {
const id = uuid();
const newItemObject = {
[id]: {
id,
isCompleted: false,
text: inputValue,
createdAt: Date.now()
}
};
const newState = {
...prevState,
inputValue: '',
allItems: {
...prevState.allItems,
...newItemObject
}
};
this.saveItems(newState.allItems);
return { ...newState };
});
}
};
deleteItem = id => {
this.setState(prevState => {
const allItems = prevState.allItems;
delete allItems[id];
const newState = {
...prevState,
...allItems
};
this.saveItems(newState.allItems);
return { ...newState };
});
};
completeItem = id => {
this.setState(prevState => {
const newState = {
...prevState,
allItems: {
...prevState.allItems,
[id]: {
...prevState.allItems[id],
isCompleted: true
}
}
};
this.saveItems(newState.allItems);
return { ...newState };
});
};
incompleteItem = id => {
this.setState(prevState => {
const newState = {
...prevState,
allItems: {
...prevState.allItems,
[id]: {
...prevState.allItems[id],
isCompleted: false
}
}
};
this.saveItems(newState.allItems);
return { ...newState };
});
};
deleteAllItems = async () => {
try {
await AsyncStorage.removeItem('ToDos');
this.setState({ allItems: {} });
} catch (err) {
console.log(err);
}
};
saveItems = newItem => {
const saveItem = AsyncStorage.setItem('To Dos', JSON.stringify(newItem));
};
render() {
const { inputValue, loadingItems, allItems } = this.state;
return (
<LinearGradient colors={primaryGradientArray} style={styles.container}>
<StatusBar barStyle="light-content" />
<View style={styles.centered}>
<Header title={headerTitle} />
</View>
<View style={styles.inputContainer}>
<SubTitle subtitle={"What's Next?"} />
<Input
inputValue={inputValue}
onChangeText={this.newInputValue}
onDoneAddItem={this.onDoneAddItem}
/>
</View>
<View style={styles.list}>
<View style={styles.column}>
<SubTitle subtitle={'Recent Notes'} />
<View style={styles.deleteAllButton}>
<Button deleteAllItems={this.deleteAllItems} />
</View>
</View>
{loadingItems ? (
<ScrollView contentContainerStyle={styles.scrollableList}>
{Object.values(allItems)
.reverse()
.map(item => (
<List
key={item.id}
{...item}
deleteItem={this.deleteItem}
completeItem={this.completeItem}
incompleteItem={this.incompleteItem}
/>
))}
</ScrollView>
) : (
<ActivityIndicator size="large" color="white" />
)}
</View>
</LinearGradient>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1
},
centered: {
alignItems: 'center'
},
inputContainer: {
marginTop: 40,
paddingLeft: 15
},
list: {
flex: 1,
marginTop: 70,
paddingLeft: 15,
marginBottom: 10
},
scrollableList: {
marginTop: 15
},
column: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between'
},
deleteAllButton: {
marginRight: 40
}
});
```
Let us take a look at the custom CRUD methods. onDoneAddItem()
starts by invoking this.setState
that has access to a prevState
object if the input value is not empty. It gives us any to do item that has been previously added to our list. Inside its callback, we will first create a new ID using uuid
and then create an object called newItemObject
which uses the ID as a variable for the name. Then, we create a new object called newState
which uses the prevState
object, clears the TextInput
for newInputValue
and finally adds our newItemObject
at the end of the other to do items list. It might sound overwhelming since a lot is going on but try implementing the code, you will understand it better.
To delete an item from the to do list object, we have to get the id of the item from the state. In Main.js
we have deleteItem
.
// app/Main.js
deleteItem = id => {
this.setState(prevState => {
const allItems = prevState.allItems;
delete allItems[id];
const newState = {
...prevState,
...allItems
};
this.saveItems(newState.allItems);
return { ...newState };
});
};
This is further passed as a prop to our List
component as deleteItem={this.deleteItem}
. We are adding the id
of an individual to do item since we are going to use this id
to delete the item from the list.
The completeItem
and incompleteItem
track which items in the to do list have been marked completed by the user or have been unmarked. In AsyncStorage
the items are saved in strings. It cannot store objects. So when saving the item if you are not using JSON.stringify()
your app is going to crash. Similarly, when fetching the item from the storage, we have to parse it using JSON.parse()
like we do above in loadingItems()
method.
const saveTo Dos = AsyncStorage.setItem('ToDos', JSON.stringify(newTo Dos));
Here, you can say that ToDos
is the name of the collection. setItem()
function from AsyncStorage
is similar to any key-value paired database. The first item ToDos
is the key, and newItem
is going to be the value, in our case the to do list items as different objects. I have already described the structure of data we are using to create each to do list item.
To verify that the data is getting saved on the device, we can restart the application. But how is our application fetching the data from device’s storage? This is done by an asynchronous function we have defined called loadingItems
. Since it is asynchronous, we have to wait till the application is done reading data from the device’s storage. Usually, nowadays smartphones do not take much time to perform this action. To run this asynchronous function we use React’s lifecycle hook componentDidMount
which is called immediately after a component is initialized.
// app/Main.js
componentDidMount = () => {
this.loadingItems();
};
loadingItems = async () => {
try {
const allItems = await AsyncStorage.getItem('ToDos');
this.setState({
loadingItems: true,
allItems: JSON.parse(allItems) || {}
});
} catch (err) {
console.log(err);
}
};
loadingItems
is then used inside a conditional operator which can be defined as if the data is read from storage, you can render the List
component or otherwise just render a loading component provided by ActivityIndicator
which again comes as a React Native core module.
Lastly, AsyncStorage
also provides a function to clear all application data in one touch by executing removeItem()
function.
deleteAllItems = async () => {
try {
await AsyncStorage.removeItem('To Dos');
this.setState({ allItems: {} });
} catch (err) {
console.log(err);
}
};
Running the app
Now that we have connected all of our components, go to the terminal and run the command expo-cli start
if the app isn’t already running in the iOS simulator or Android emulator. The start
command starts or restarts a local server for your app and gives you a URL or QR code. You can press a
for Android emulator or i
for iOS simulator.
After you have successfully started the application, you can start playing with it by adding to do items in the WHAT'S NEXT?
section. Items successfully added will appear under the heading Recent Notes
as shown below.
Conclusion
I leave the SubTitle
component for you to customize. It is the same as Header
but it is being used twice in our application. Refer to Main.js
file to see where it is used.
This completes our tutorial for building a React Native Application from scratch using Expo. You can add more functionality such as updating the list item by making use of the created Date
field we added to our data model. The possibilities to enhance this application are endless. For example, you can add another functionality for updating the text of a list item. You can add an icon next to the delete item and then let the user select which item they want to edit.
You now have an in-depth understanding of how things work in React Native and why there is much less difference between React Native and Expo. You can find the complete code for this project here: GitHub.
18 October 2018
by Aman Mittal