Build a To Do app with React Native and Expo

Introduction

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.

  • Node.js(>=8.12.0)
  • Expo CLI (>= 2.2.0) To install expo-cli, please run the following command.

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.

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:

1import React from 'react';
2import { StyleSheet, Text, View } from 'react-native';
3export default class App extends React.Component {
4  render() {
5    return (
6      <View style={styles.container}>
7        <Text>Open up App.js to start working on your app!</Text>
8      </View>
9    );
10  }
11}
12const styles = StyleSheet.create({
13  container: {
14    flex: 1,
15    backgroundColor: '#fff',
16    alignItems: 'center',
17    justifyContent: 'center'
18  }
19});

Once you run the app in its current state, you will see the following result. We will replace it with following:

1// App.js
2import React from 'react';
3import Main from './app/Main';
4export default class App extends React.Component {
5  render() {
6    return <Main />;
7  }
8}

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.

1// app/utils/Colors.js
2const primaryStart = '#f18a69';
3const primaryEnd = '#d13e60';
4export const primaryGradientArray = [primaryStart, primaryEnd];
5export const lightWhite = '#fcefe9';
6export const inputPlaceholder = '#f1a895';
7export const lighterWhite = '#f4e4e2';
8export const circleInactive = '#ecbfbe';
9export const circleActive = '#90ee90';
10export const itemListText = '#555555';
11export const itemListTextStrike = '#c4c4cc';
12export 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.

1// App.js
2import React from 'react';
3import { StyleSheet, Text, View, StatusBar } from 'react-native';
4import { LinearGradient } from 'expo';
5import { primaryGradientArray } from './utils/Colors';
6export default class Main extends React.Component {
7  render() {
8    return (
9      <LinearGradient colors={primaryGradientArray} style={styles.container}>
10        <StatusBar barStyle="light-content" />;
11        <Text>Open up App.js to start working on your app!</Text>
12      </LinearGradient>
13    );
14  }
15}
16const styles = StyleSheet.create({
17  container: {
18    flex: 1
19  }
20});

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.

1// app/components/Header.js
2import React from 'react';
3import { View, Text, StyleSheet } from 'react-native';
4const Header = ({ title }) => (
5  <View style={styles.headerContainer}>
6    <Text style={styles.headerText}>{title.toUpperCase()}</Text>
7  </View>
8);
9const styles = StyleSheet.create({
10  headerContainer: {
11    marginTop: 40
12  },
13  headerText: {
14    color: 'white',
15    fontSize: 22,
16    fontWeight: '500'
17  }
18});
19export default Header;

Import it in Main.js and add a title of your app.

1// app/Main.js
2// after all imports
3import Header from './components/Header';
4const headerTitle = 'To Do';
5// after status bar, replace the <Text> with
6<View style={styles.centered}>
7  <Header title={headerTitle} />
8</View>;
9// add styles
10centered: {
11  alignItems: 'center';
12}

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.

1// app/components/Input.js
2import React from 'react';
3import { StyleSheet, TextInput } from 'react-native';
4import { inputPlaceholder } from '../utils/Colors';
5const Input = ({ inputValue, onChangeText, onDoneAddItem }) => (
6  <TextInput
7    style={styles.input}
8    value={inputValue}
9    onChangeText={onChangeText}
10    placeholder="Type here to add note."
11    placeholderTextColor={inputPlaceholder}
12    multiline={true}
13    autoCapitalize="sentences"
14    underlineColorAndroid="transparent"
15    selectionColor={'white'}
16    maxLength={30}
17    returnKeyType="done"
18    autoCorrect={false}
19    blurOnSubmit={true}
20    onSubmitEditing={onDoneAddItem}
21  />
22);
23const styles = StyleSheet.create({
24  input: {
25    paddingTop: 10,
26    paddingRight: 15,
27    fontSize: 34,
28    color: 'white',
29    fontWeight: '500'
30  }
31});
32export 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.

1// app/Main.js
2import React from 'react';
3import { StyleSheet, Text, View, StatusBar } from 'react-native';
4import { LinearGradient } from 'expo';
5import { gradientStart, gradientEnd } from './utils/Colors';
6import Header from './components/Header';
7import Input from './components/Input';
8const headerTitle = 'To Do';
9export default class Main extends React.Component {
10  state = {
11    inputValue: ''
12  };
13  newInputValue = value => {
14    this.setState({
15      inputValue: value
16    });
17  };
18  render() {
19    const { inputValue } = this.state;
20    return (
21      <LinearGradient
22        colors={[gradientStart, gradientEnd]}
23        style={styles.container}
24      >
25        <StatusBar barStyle="light-content" />
26        <View style={styles.centered}>
27          <Header title={headerTitle} />
28        </View>
29        <View style={styles.inputContainer}>
30          <Input inputValue={inputValue} onChangeText={this.newInputValue} />
31        </View>
32      </LinearGradient>
33    );
34  }
35}
36const styles = StyleSheet.create({
37  container: {
38    flex: 1
39  },
40  centered: {
41    alignItems: 'center'
42  },
43  inputContainer: {
44    marginTop: 40,
45    paddingLeft: 15
46  }
47});

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.

1// app/components/List.js
2import React, { Component } from 'react';
3import {
4  View,
5  Text,
6  Dimensions,
7  StyleSheet,
8  TouchableOpacity,
9  Platform
10} from 'react-native';
11import { MaterialIcons } from '@expo/vector-icons';
12import {
13  itemListText,
14  itemListTextStrike,
15  circleInactive,
16  circleActive,
17  deleteIconColor
18} from '../utils/Colors';
19const { height, width } = Dimensions.get('window');
20class List extends Component {
21  onToggleCircle = () => {
22    const { isCompleted, id, completeItem, incompleteItem } = this.props;
23    if (isCompleted) {
24      incompleteItem(id);
25    } else {
26      completeItem(id);
27    }
28  };
29  render() {
30    const { text, deleteItem, id, isCompleted } = this.props;
31    return (
32      <View style={styles.container}>
33        <View style={styles.column}>
34          <TouchableOpacity onPress={this.onToggleCircle}>
35            <View
36              style={[
37                styles.circle,
38                isCompleted
39                  ? { borderColor: circleActive }
40                  : { borderColor: circleInactive }
41              ]}
42            />
43          </TouchableOpacity>
44          <Text
45            style={[
46              styles.text,
47              isCompleted
48                ? {
49                    color: itemListTextStrike,
50                    textDecorationLine: 'line-through'
51                  }
52                : { color: itemListText }
53            ]}
54          >
55            {text}
56          </Text>
57        </View>
58        {isCompleted ? (
59          <View style={styles.button}>
60            <TouchableOpacity onPressOut={() => deleteItem(id)}>
61              <MaterialIcons
62                name="delete-forever"
63                size={24}
64                color={deleteIconColor}
65              />
66            </TouchableOpacity>
67          </View>
68        ) : null}
69      </View>
70    );
71  }
72}
73const styles = StyleSheet.create({
74  container: {
75    width: width - 50,
76    flexDirection: 'row',
77    borderRadius: 5,
78    backgroundColor: 'white',
79    height: width / 8,
80    alignItems: 'center',
81    justifyContent: 'space-between',
82    marginVertical: 5,
83    ...Platform.select({
84      ios: {
85        shadowColor: 'rgb(50,50,50)',
86        shadowOpacity: 0.8,
87        shadowRadius: 2,
88        shadowOffset: {
89          height: 2,
90          width: 0
91        }
92      },
93      android: {
94        elevation: 5
95      }
96    })
97  },
98  column: {
99    flexDirection: 'row',
100    alignItems: 'center',
101    width: width / 1.5
102  },
103  text: {
104    fontWeight: '500',
105    fontSize: 16,
106    marginVertical: 15
107  },
108  circle: {
109    width: 30,
110    height: 30,
111    borderRadius: 15,
112    borderWidth: 3,
113    margin: 10
114  },
115  button: {
116    marginRight: 10
117  }
118});
119export 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.

1<View style={styles.list}>
2  <ScrollView contentContainerStyle={styles.scrollableList}>
3    {Object.values(allItems)
4      .reverse()
5      .map(item => (
6        <List
7          key={item.id}
8          {...item}
9          deleteItem={this.deleteItem}
10          completeItem={this.completeItem}
11          incompleteItem={this.incompleteItem}
12        />
13      ))}
14  </ScrollView>
15</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.

1// app/Main.js
2list: {
3    flex: 1,
4    marginTop: 70,
5    paddingLeft: 15,
6    marginBottom: 10
7  },
8  scrollableList: {
9    marginTop: 15
10  },

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:

1232390: {
2  id: 232390,           // same id as the object
3  text: 'New item',     // name of the To Do item
4  isCompleted: false,   // by default
5  createdAt: Date.now()
6}

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.

1// app/Main.js
2import React from 'react';
3import {
4  StyleSheet,
5  View,
6  StatusBar,
7  ActivityIndicator,
8  ScrollView,
9  AsyncStorage
10} from 'react-native';
11import { LinearGradient } from 'expo';
12import uuid from 'uuid/v1';
13import { primaryGradientArray } from './utils/Colors';
14import Header from './components/Header';
15import SubTitle from './components/SubTitle';
16import Input from './components/Input';
17import List from './components/List';
18import Button from './components/Button';
19const headerTitle = 'To Do';
20export default class Main extends React.Component {
21  state = {
22    inputValue: '',
23    loadingItems: false,
24    allItems: {},
25    isCompleted: false
26  };
27  componentDidMount = () => {
28    this.loadingItems();
29  };
30  newInputValue = value => {
31    this.setState({
32      inputValue: value
33    });
34  };
35  loadingItems = async () => {
36    try {
37      const allItems = await AsyncStorage.getItem('ToDos');
38      this.setState({
39        loadingItems: true,
40        allItems: JSON.parse(allItems) || {}
41      });
42    } catch (err) {
43      console.log(err);
44    }
45  };
46  onDoneAddItem = () => {
47    const { inputValue } = this.state;
48    if (inputValue !== '') {
49      this.setState(prevState => {
50        const id = uuid();
51        const newItemObject = {
52          [id]: {
53            id,
54            isCompleted: false,
55            text: inputValue,
56            createdAt: Date.now()
57          }
58        };
59        const newState = {
60          ...prevState,
61          inputValue: '',
62          allItems: {
63            ...prevState.allItems,
64            ...newItemObject
65          }
66        };
67        this.saveItems(newState.allItems);
68        return { ...newState };
69      });
70    }
71  };
72  deleteItem = id => {
73    this.setState(prevState => {
74      const allItems = prevState.allItems;
75      delete allItems[id];
76      const newState = {
77        ...prevState,
78        ...allItems
79      };
80      this.saveItems(newState.allItems);
81      return { ...newState };
82    });
83  };
84  completeItem = id => {
85    this.setState(prevState => {
86      const newState = {
87        ...prevState,
88        allItems: {
89          ...prevState.allItems,
90          [id]: {
91            ...prevState.allItems[id],
92            isCompleted: true
93          }
94        }
95      };
96      this.saveItems(newState.allItems);
97      return { ...newState };
98    });
99  };
100  incompleteItem = id => {
101    this.setState(prevState => {
102      const newState = {
103        ...prevState,
104        allItems: {
105          ...prevState.allItems,
106          [id]: {
107            ...prevState.allItems[id],
108            isCompleted: false
109          }
110        }
111      };
112      this.saveItems(newState.allItems);
113      return { ...newState };
114    });
115  };
116  deleteAllItems = async () => {
117    try {
118      await AsyncStorage.removeItem('ToDos');
119      this.setState({ allItems: {} });
120    } catch (err) {
121      console.log(err);
122    }
123  };
124  saveItems = newItem => {
125    const saveItem = AsyncStorage.setItem('To Dos', JSON.stringify(newItem));
126  };
127  render() {
128    const { inputValue, loadingItems, allItems } = this.state;
129    return (
130      <LinearGradient colors={primaryGradientArray} style={styles.container}>
131        <StatusBar barStyle="light-content" />
132        <View style={styles.centered}>
133          <Header title={headerTitle} />
134        </View>
135        <View style={styles.inputContainer}>
136          <SubTitle subtitle={"What's Next?"} />
137          <Input
138            inputValue={inputValue}
139            onChangeText={this.newInputValue}
140            onDoneAddItem={this.onDoneAddItem}
141          />
142        </View>
143        <View style={styles.list}>
144          <View style={styles.column}>
145            <SubTitle subtitle={'Recent Notes'} />
146            <View style={styles.deleteAllButton}>
147              <Button deleteAllItems={this.deleteAllItems} />
148            </View>
149          </View>
150          {loadingItems ? (
151            <ScrollView contentContainerStyle={styles.scrollableList}>
152              {Object.values(allItems)
153                .reverse()
154                .map(item => (
155                  <List
156                    key={item.id}
157                    {...item}
158                    deleteItem={this.deleteItem}
159                    completeItem={this.completeItem}
160                    incompleteItem={this.incompleteItem}
161                  />
162                ))}
163            </ScrollView>
164          ) : (
165            <ActivityIndicator size="large" color="white" />
166          )}
167        </View>
168      </LinearGradient>
169    );
170  }
171}
172const styles = StyleSheet.create({
173  container: {
174    flex: 1
175  },
176  centered: {
177    alignItems: 'center'
178  },
179  inputContainer: {
180    marginTop: 40,
181    paddingLeft: 15
182  },
183  list: {
184    flex: 1,
185    marginTop: 70,
186    paddingLeft: 15,
187    marginBottom: 10
188  },
189  scrollableList: {
190    marginTop: 15
191  },
192  column: {
193    flexDirection: 'row',
194    alignItems: 'center',
195    justifyContent: 'space-between'
196  },
197  deleteAllButton: {
198    marginRight: 40
199  }
200});
201```

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.

1// app/Main.js
2deleteItem = id => {
3  this.setState(prevState => {
4    const allItems = prevState.allItems;
5    delete allItems[id];
6    const newState = {
7      ...prevState,
8      ...allItems
9    };
10    this.saveItems(newState.allItems);
11    return { ...newState };
12  });
13};

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.

1const 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.

1// app/Main.js
2componentDidMount = () => {
3  this.loadingItems();
4};
5loadingItems = async () => {
6  try {
7    const allItems = await AsyncStorage.getItem('ToDos');
8    this.setState({
9      loadingItems: true,
10      allItems: JSON.parse(allItems) || {}
11    });
12  } catch (err) {
13    console.log(err);
14  }
15};

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.

1deleteAllItems = async () => {
2  try {
3    await AsyncStorage.removeItem('To Dos');
4    this.setState({ allItems: {} });
5  } catch (err) {
6    console.log(err);
7  }
8};

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.