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.
To get started you will need three things to follow this article.
npm install -g expo-cli
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.
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.
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.
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.
sentences
as the default value. This means, every new sentence will automatically have its first character capitalized.onSubmitEditing
event instead of inserting a newline into the field.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});
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.
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};
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.
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.