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.

For an elaborate tutorial on Navigation, check out my tutorial React Native Navigation Tutorial.

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:

  1. headerStyle: accepts an object of styles to apply to the header. To set the background color of the header, we pass it the backgroundColor prop with the value of the background color of the header.
  2. headerTintColor: the color of the text or buttons that are in the header.
  3. headerTitleStyle: accepts an object of font-related styles to make changes to the title in the header. For example, we can change the fontFamily or fontWeight.

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.

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.

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.