This article was originally published on my personal blog
React Native is one of the most popular frameworks that allow you to create cross-platform apps using JavaScript. Using React Native, you'll be able to write one code for the web, iOS, and Android.
In this tutorial, you'll learn the basics of creating a React Native app with Expo. We'll create a to-do list app where we'll learn about implementing navigation in a React Native app and storing data in our app.
You can find the code for this tutorial in this GitHub Repository. You can also install the app using Expo Go. There is more info on how to install Expo Go below.
Prerequisites
Before you start going through the tutorial, you'll need to install Node.js which will install NPM with it.
You also need to install Expo. Expo provides a set of tools to make your mobile development with React Native easier.
To install Expo run:
npm install -g expo-cli
Finally, you'll need to install Expo Go on your phone. It's available for both Android and iOS.
By installing Expo Go on your phone, you'll be able to test your app directly on your phone as you make changes.
Setup Project
To create a new React Native project, run the following command in your terminal:
expo init todolist
You'll be asked to choose the kind of project you want to create, choose blank
.
After you choose blank
, the project will be set up and the minimal dependencies required to create an app with React Native will be installed.
After the setup is done, change to the directory of the project:
cd todolist
Project Structure
Let's take a quick look at the project's structure before we start coding.
We have the usual package.json
files that you find in every NPM project.
There's app.json
. This includes a set of configurations for our app. If you open it, you'll find key-value pairs related to the app name, version, icon, splash screen, and more.
App.js
is the entry point of our app. It's where we will start writing our app's code.
The assets
directory includes images like the app icon, splash screen, and more.
Understand First Components in React Native
If you open App.js
, you'll find content similar to this:
import { StatusBar } from 'expo-status-bar';
import React from 'react';
import { StyleSheet, Text, View } from 'react-native';
export default function App() {
return (
<View style={styles.container}>
<Text>Open up App.js to start working on your app!</Text>
<StatusBar style="auto" />
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
alignItems: 'center',
justifyContent: 'center',
},
});
This is our first component! As you can see, components like View
, Text
and others imported from react-native
are being used.
You should know that in React Native when displaying the text you need to do it inside a Text
component.
React Native provides a set of components that will be later transformed into native components in iOS or Android.
We also create stylesheets to add styling to our components using StyleSheet.create
, where StyleSheet
is imported from react-native
as well.
The create
method takes an object of properties, which act like class names in CSS, and their values are objects of CSS-like properties and their values. Styling your components in React Native is almost identical to styling using CSS, with a few changes in some behaviors of some CSS properties.
Screens and Navigation
Now, we'll start adding screens to our app. To add different screens and manage navigation between them, we'll use React Navigation.
Home Screen
Create the directories src/screens
. The screens
directory will hold all the screens we will create later on.
Then, create HomeScreen.js
inside screens
. This will be the first screen that the user will see when they open the app.
Add the following content inside HomeScreen.js
:
import React from 'react';
import { Text, View } from 'react-native';
export default function HomeScreen () {
return (
<View>
<Text>Welcome Home!</Text>
</View>
)
}
The home screen, at the moment, will just display the text "Welcome Home!".
Install React Navigation
Next, we'll see how to use multiple screens with React Navigation.
React Navigation allows us to move between screens backward and forwards, add buttons to the header, and more.
To install React Navigation, run the following commands:
npm install @react-navigation/native
expo install react-native-screens react-native-safe-area-context
npm install @react-navigation/native-stack
Once these commands are done executing, we'll have all the dependencies required to use React Navigation.
How React Navigation Works
To put things simply, React Navigation manages screens, navigation between them, and history as a Stack.
There's a default, initial screen that shows when the app is launched. Then, when you want to open a new screen, you can push it at the top of the stack or replace the current item in the stack.
Then, when you want to go backward, you pop the current item at the top of the stack and show the one below it, which was the previous screen, until you reach the home, initial screen.
If it sounds confusing at the moment, keep going through in the tutorial and you'll start understanding things more.
Create the Navigation Stack
Change the content of App.js
to the following:
import 'react-native-gesture-handler';
import React from 'react';
import { createStackNavigator } from '@react-navigation/stack';
import { NavigationContainer } from '@react-navigation/native';
import HomeScreen from './src/screens/HomeScreen';
const Stack = createStackNavigator();
export default function App() {
return (
<NavigationContainer>
<Stack.Navigator initialRouteName="Home">
<Stack.Screen name="Home" component={HomeScreen} />
</Stack.Navigator>
</NavigationContainer>
);
}
Let's go over things bit by bit.
We first need to import react-native-gesture-handler
at the top of the app. This allows navigating using gestures. The higher in the app it's placed the better.
Next, we're importing createStackNavigator
. This function returns a Stack object, which contains two components Screen
and Navigator
.
Screen
is used to display screen components that we create, define their options, and more. If you look at the example above, we provide a name
and component
props for a screen:
<Stack.Screen name="Home" component={HomeScreen} />
The name
prop can be used to navigate to that screen at any given point later. The component
prop will be used to define the screen component to render when the screen is navigated to.
Navigator
should contain Screen
components as children as it manages the routing between them. Navigator
also receives the initialRouteName
prop which determines the screen that should open when the app first launches.
Finally, we use NavigationContainer
to wrap the Navigator
components, as it manages the navigation tree and state.
So, in App.js
, which will contain the navigation routes for the screens in the app as we go forward, we should render the NavigationContainer
and inside it Stack.Navigator
which contains one or more Stack.Screen
components:
<NavigationContainer>
<Stack.Navigator initialRouteName="Home">
<Stack.Screen name="Home" component={HomeScreen} />
</Stack.Navigator>
</NavigationContainer>
Run the App
Let's run the app now. To do that, open the terminal and run the following:
npm start
This will start Expo. As mentioned earlier, with Expo you will be able to run the app on your phone, so make sure you've installed Expo Go as detailed in the Prerequisites section.
A web page will open that looks something like this:
There are multiple ways to run the app after this on your device. You can scan the QR code with your phone to open it in Expo Go. Alternatively, you can use one of the actions on the sidebar of the web page above like send link with email and so on.
Once you choose the best way to open the app on your phone and it opens, you should see the following screen:
We've run our first app! We'll start customizing the styles of the header next and add more screens to create a Todo List app.
Style the Header
With React Navigation, there are 2 ways to style the header of a screen.
Style Screen Headers Individually
The first way is to style it for each screen. This can be done by passing the options
prop to a Screen
component like this:
<Stack.Screen
name="Home"
component={HomeScreen}
options={{
headerStyle: {
backgroundColor: '#228CDB'
},
headerTintColor: '#fff'
}}
/>
The options
prop is an object of options for the screen. To style the header, we can use the following three properties:
-
headerStyle
: accepts an object of styles to apply to the header. To set the background color of the header, we pass it thebackgroundColor
prop with the value of the background color of the header. -
headerTintColor
: the color of the text or buttons that are in the header. -
headerTitleStyle
: accepts an object of font-related styles to make changes to the title in the header. For example, we can change thefontFamily
orfontWeight
.
Using this prop, we'll be able to style the header of a screen.
Style All Screen Headers
In general cases, styling each screen separately is tedious and leads to repeated code. Usually, you'd apply the same header style to all screens in the app.
In this case, we can use the screenOptions
prop on the Navigator
component. This prop accepts the same header options as the options
prop in the Screen
component and applies the styling to all screens.
Apply Header Styles in Our App
In our app, we'll apply the same header style to all the screens in the app. So, we'll use the second way to style a header.
In App.js, replace this line:
<Stack.Navigator initialRouteName="Home">
With the following:
<Stack.Navigator
screenOptions={{
headerStyle: {
backgroundColor: '#228CDB'
},
headerTintColor: '#fff'
}}
initialRouteName="Home">
This will change the background color of the header to #228CDB
and the color of the text and buttons in the header to #fff
.
If you save the changes and open the app again, you'll see that the header color changed.
Navigate Screens
Next, we'll see how to add another screen and navigate to it. We'll also see how to add a header button.
Add New Screen
We'll add a new screen, which we'll use later to add a new to-do list item.
Create src/screens/NewScreen.js
with the following content:
import React from 'react';
import { Text, View } from 'react-native';
export default function NewScreen () {
return (
<View>
<Text>This is New Screen</Text>
</View>
)
}
Similar to HomeScreen
, we're just showing the text "This is New Screen" for now.
Add Route for New Screen
Now, we need to add a new route in our navigation stack for the new route. In App.js
below the Screen
component for HomeScreen
add a new one for NewScreen
:
<Stack.Screen name="New" component={NewScreen} />
Add Header Button
Next, we'll add a header button on the Home screen. It will be a plus button that should take us to NewScreen
.
To add a button to the header of a screen, we do it using the headerRight
property of the options
prop passed to Screen
. The headerRight
property accepts a function that should return a component to render.
Instead of using React Native's Button component, we'll use React Native Elements' Icon component. Adding a plus icon looks better than an actual button.
So, let's first install React Native Elements in our project:
npm i react-native-elements
Then, change the following line in App.js
:
<Stack.Screen name="Home" component={HomeScreen} />
to this:
<Stack.Screen
name="Home"
component={HomeScreen}
options={{
headerRight: () => (
<Icon
name="plus"
type="feather"
color="#fff"
style={style.headerIcon}
/>
)
}}
/>
As you can see, we are using the Icon component, passing it the prop name
which is the name of the icon to be used. type
as React Native Elements allow us to choose from multiple icon sets. We are using Feather icons. color
indicates the color of the icon. And finally, we're passing it style
. Add the following to the end of App.js
to create a new stylesheet:
const style = StyleSheet.create({
headerIcon: {
marginRight: 10
}
});
This will add a margin-right to the icon, as it won't have any by default.
If you run the app now, you'll see that a new + icon has been added to the header but it does nothing at the moment.
Navigating to Another Screen
We need to navigate to NewScreen
when the plus icon is pressed.
In React Native, button's press events are handled by passing a listener in the onPress
prop of the button. So, we'll need to pass a handler for onPress
to Icon
.
To navigate to another screen, we can use the navigation
prop. The navigation
prop is passed to every screen in the stack navigation.
Another way we can use the navigation
prop is by changing the value that the options
prop accepts of a Screen
to a function. The function accepts as a parameter an object which contains navigation
, and the function should return an object of options.
Then, using the navigation
prop we can navigate to another screen with the navigate
method:
navigation.navigate('New')
Where navigate
accepts the name of the screen we're navigating to.
So, change the line for HomeScreen
in App.js
to the following:
<Stack.Screen
name="Home"
component={HomeScreen}
options={({navigation}) => ({
headerRight: () => (
<Icon
name="plus"
type="feather"
color="#fff"
style={style.headerIcon}
onPress={() => navigation.navigate('New')}
/>
)
})}
/>
If you open the app now and click on the plus icon, you'll be taken to NewScreen
.
You can also see that, by default, a back button is added to the header and you can use it to go backward in the navigation stack. If you click on it, you'll go back to the Home screen.
New Todo Item Form
Next, we'll add a form to add a new todo item in NewScreen
. To simplify creating a form, we'll use Formik.
If you're not familiar with Formik, it's a React and React Native library that aims to simplify the process of creating a form.
To install Formik, run the following command:
npm install formik --save
Then, change the content of src/screens/NewScreen.js
to the following:
import { Formik } from 'formik';
import React from 'react';
import { StyleSheet, View } from 'react-native';
import { Text } from 'react-native-elements';
import { Button } from 'react-native-elements/dist/buttons/Button';
import { Input } from 'react-native-elements/dist/input/Input';
export default function NewScreen () {
function newTask (values) {
//TODO save new task
}
return (
<Formik
initialValues={{title: ''}}
onSubmit={newTask}
>
{({handleChange, handleBlur, handleSubmit, values}) => (
<View style={style.container}>
<Text h4>New Todo Item</Text>
<Input
placeholder="Example: Cook, Clean, etc..."
onChangeText={handleChange('title')}
onBlur={handleBlur('title')}
style={style.input}
/>
<Button title="Add" onPress={handleSubmit} style={style.button} />
</View>
)}
</Formik>
)
}
const style = StyleSheet.create({
container: {
marginTop: 10,
padding: 10
},
input: {
marginTop: 10
},
button: {
backgroundColor: '#228CDB'
}
})
Let's go over everything we just added. We first define a new function newTask
inside the component, which we'll use later to handle saving a new task.
Then, we're creating a form with Formik. In Formik, you can use the Formik
component and pass it initialValues
to define the fields and their initial values. We just have one field title
and its initial value is just an empty string.
We also pass the Formik
component an onSubmit
prop which is the function that should be executed when the form is submitted. We're passing it newTask
.
Inside the Formik
component, you use a function that has a set of parameters but most importantly are handleChange
, handleBlur
, handleSubmit
, and values
. The function should return a component to render.
If you're familiar with Formik when using it with React, you'll notice that this is slightly different than how you'd use it with React. As inputs in React Native are not similar to inputs on the web since they don't have names, you need to clearly specify for each input the onChange
and onBlur
listeners passing them the name of the input.
So, for the title Input, which is a component we are using from React Native Elements, we pass for onChange
the listener handleChange('title')
and for onBlur
the listener handleBlur('title')
.
Then, we add a Button component, which is another component we are using from React Native Elements. We assign the listener for onPress
on the button to handleSubmit
. This means that when the button is pressed, the submit event will be triggered in the form, which will trigger newTask
since we assigned it as the listener to onSubmit
.
Notice that we are adding some styling to the screen and components in it. We use the styles
variable which is created with StyleSheet.create
, and we pass to each component a style prop with its value a property in the styles
variable. For example in View
:
<View style={style.container}>
If you open the app now, click on the plus button in the Home Screen which will open NewScreen
. You should see the form we just created.
Handle Submit
We'll now handle the submission of the form in the function newTask
. What we'll do is take the title and store it in the app.
There are many ways you can manage storage in React Native. We'll use Async Storage. It provides a simple API to store data in your app. For data that are not too complex, like settings related to the user or app, it's a great choice.
We'll use Async Storage to store the todo list items in our app. So, let's first install it:
expo install @react-native-async-storage/async-storage
AsyncStorage has 2 functions in its API. getItem and setItem. using setItem, you set items serialized as strings. So, if you have an array or object you need to stringify them with JSON.stringify
. Then, you can retrieve the item with getItem
and you'll have to parse the JSON if it's stringified with JSON.parse
.
Add the following import at the beginning of NewScreen.js
along with the rest of the imports added before:
import { useAsyncStorage } from '@react-native-async-storage/async-storage';
Then, inside the NewScreen
component, add the following line:
const { getItem, setItem } = useAsyncStorage('todo');
We are using the useAsyncStorage hook which allows us to pass the name of a key in the storage and retrieve a getter getItem
and a setter setItem
solely for that key in the storage.
Before we start implementing the functionality inside newTask
, we need 2 other dependencies: react-native-uuid to generate random IDs for every task in storage, and react-native-toast-message to show toast messages if an error occurs:
npm i react-native-uuid react-native-toast-message
For react-native-toast-message to work, we first need to add a Toast
component in one of the higher components rendered in the App. So, add the following line in App.js
before the closing of NavigationContainer
:
<Toast ref={(ref) => Toast.setRef(ref)} />
</NavigationContainer>
And, of course, add the necessary import at the beginning of App.js
:
import Toast from 'react-native-toast-message';
Back to NewScreen.js
. We'll now implement newTask
. We'll first validate that the user entered a value for title
. Then, we'll get the todo
items from the storage, which will be an array of objects where each object is a to-do item. Then, we'll push a new item into the array and set the item in storage again.
Change the newTask
function to the following:
function newTask (values) {
if (!values.title) {
Toast.show({
type: 'error',
text1: 'Title is required',
position: 'top'
});
return;
}
//get todo array from storage
getItem()
.then((todoJSON) => {
let todo = todoJSON ? JSON.parse(todoJSON) : [];
//add a new item to the list
todo.push({
id: uuid.v4(),
title: values.title
});
//set item in storage again
setItem(JSON.stringify(todo))
.then(() => {
//navigate back to home screen
navigation.goBack();
}).catch((err) => {
console.error(err);
Toast.show({
type: 'error',
text1: 'An error occurred and a new item could not be saved',
position: 'top'
});
});
})
.catch((err) => {
console.error(err);
Toast.show({
type: 'error',
text1: 'An error occurred and a new item could not be saved',
position: 'bottom'
});
});
}
As you can see, we're checking first if values.title
has been entered. If not, we show a toast message and return. To show a toast message using react-native-toast-message you need to pass it an object of options. There are a variety of options you can use, but the most important here are type
which can be error
, success
, or info
, position
which can be top
or bottom
, and text1
which will be the message to show in the toast.
After validating title
, we then use getItem
to retrieve todo
from the storage if it exists. getItem
returns a Promise as it is asynchronous, and the value of todo
is passed to then
function handler.
Inside then
we parse the JSON, then push a new to-do item. Each todo item will have an id
which is randomly generated, and a title
.
Finally, we set the todo
array in the storage again as JSON. Once it is set successfully, we navigate back to the Home screen with navigation.goBack
. As we mentioned earlier, all items in the navigation stack receive navigation
as a prop. So, make sure to add the prop for NewScreen
:
export default function NewScreen ({ navigation }) {
You can now try the form. Open the app and go to the NewScreen
. Try first to submit the form without entering a title. You should see then a message telling you that the Title is required.
Now try to enter a title and press "Add". You'll be navigated back to the Home Screen. That means that the item was added successfully!
What's left for us to do is display them on the Home Screen.
Display Tasks
We'll now change the content of the Home Screen to display the todo list items we are adding in NewScreen
.
To do so, we'll use the same function we used in NewScreen
from Async Storage to get the items. To display the items, we'll use FlatList, a component that allows us to display a list of items easily.
Change the content of HomeScreen
to the following:
import { useAsyncStorage } from '@react-native-async-storage/async-storage';
import React, { useEffect, useState } from 'react';
import { FlatList, StyleSheet, Text, View } from 'react-native';
import { Card } from 'react-native-elements';
import Toast from 'react-native-toast-message';
export default function HomeScreen ({ navigation }) {
const { getItem } = useAsyncStorage('todo');
const [items, setItems] = useState([]);
const [loading, setLoading] = useState(true);
function getTodoList () {
getItem()
.then((todoJSON) => {
const todo = todoJSON ? JSON.parse(todoJSON) : [];
setItems(todo);
setLoading(false);
})
.catch((err) => {
console.error(err);
Toast.show({
type: 'error',
text1: 'An error occurred',
position: 'top'
});
});
}
function renderCard ({item}) {
return (
<Card>
<Card.Title style={styles.cardTitle}>{item.title}</Card.Title>
</Card>
)
}
useEffect(() => {
const unsubscribe = navigation.addListener('focus', getTodoList);
return unsubscribe;
}, [])
return (
<View>
<FlatList refreshing={loading} onRefresh={getTodoList} style={styles.list} data={items}
renderItem={renderCard} keyExtractor={(item) => item.id} />
</View>
)
}
const styles = StyleSheet.create({
list: {
width: '100%'
},
cardTitle: {
textAlign: 'left'
}
})
Here's a breakdown of what this code is doing. First, we're using useAsyncStorage
hook just like we did before to get a getter function for todo
. We're also creating state variables items
and loading
. items
will be used to store the items after fetching them from the storage. loading
will be used to indicate that we're currently retrieving the items from the storage.
Then, we create the function getTodoList
. This function should be executed every time the screen is opened. In this function, we're just using the getter function getItem
like we did before and once we retrieve the todo list items from the storage, we're setting the state items
and loading
.
After that, we create the function renderCard
. This function will be used to render each item in the FlatList. We'll be using the Card component from React Native Element to display them.
Next is the important part. In useEffect
, we are adding an event listener to the focus
event for the navigator
object that the screens inside the navigation stack receive as a prop. The focus event is triggered every time the screen comes into focus. So, this listener for the focus event will run when the app launch and the Home screen is shown, and when we go back from the NewScreen
to the Home screen.
Finally, we're displaying the FlatList
component. It receives the refreshing
prop which indicates whether the list is currently refreshing. We're passing it the loading
state. We're also passing it an event handler for the refresh event in the prop onRefresh
. This event is triggered whenever the user refreshes the list by pulling it down.
The data
prop indicates the data that we're displaying in the list and should be an array. The renderItem
prop receives a function to render each item, and that function will be passed an object which includes the item
property, indicating the current item to be rendered in the list.
The keyExtractor
prop indicates how to assign the key
prop for each item in the list. In React and React Native, when rendering an array of items you should pass a key prop
to each item. We're setting the key for each item its id.
In the end, we're defining the stylesheet to style all elements.
If you open the app now, you'll see that on the Home Screen a list of items will appear which are items that you add in the NewScreen
. Try going to the NewScreen
again and adding more items. You'll see them added to the list.
Publish the App
The last step that comes when creating an app is to publish it. React Native's documentation has a guide on how to publish your app on Google Play Store and Apple App Store.
If, however, you want to publish the app but you don't have a developer account for either Google or Apple, you can publish the app on Expo, but that would require anyone to install Expo to be able to try or use your app.
Conclusion
You just created your first app! You were able to create an app with navigation, forms, storage, lists, and more!
If you want to keep practicing, try adding a delete or edit functionality. Make sure to check more of React Native's documentation as well as React Navigation's documentation on how to pass parameters as well.
Top comments (0)