An In-depth Guide on How To Build Mobile Apps With React Native

As a web developer, there’s only one platform where you may struggle to create applications, and that is mobile.

Web developers long have the ability to develop not just web applications, but chrome extensions, bookmarklets, and even desktop applications with nothing more, but web technologies. The only place where we haven’t ventured into is mobile app development.

So far mobile applications have been written in Java or C#, which means you would not only need to learn about native mobile APIs, but you would also need to gain knowledge in other programming languages.

In this tutorial, we will take a look at how you can develop a mobile application with technologies you already know. We will use React Native to generate native code for us while writing everything in JavaScript.

Table of Contents

  1. Introduction
    1. What is React Native?
    2. Prerequisites
  2. What Will be Building?
  3. Setting up the Project
  4. The Structure of a React Native Project
    1. .expo-shared
    2. .expo
    3. app.json
  5. How to Start and Test Your App
    1. Enable USB debugging
    2. Launching the app on your mobile
    3. Making your first changes
  6. Configuring the Splash Screen
  7. Adding a Header for the App
  8. Displaying Data Inside the App
    1. Setting up a data storage
    2. Displaying the stored data
  9. Adding Styles to the App
    1. Using custom fonts
  10. Adding Interactions
  11. How to Add Routing
  12. Making the Details View
    1. Adding the remaining elements
    2. Adding interactivity to the view
    3. Displaying information inside a modal
  13. Reflect Changes on Different Routes
  14. Adding extra Functionality to Headers
    1. Adding action buttons inside the header
    2. Removing checklists
  15. How to Build the App
    1. Publishing the app
  16. What are the Drawbacks of Expo
    1. What are the alternatives?
  17. Summary

What is React Native?

React Native is a framework that provides you the tools necessary to write native mobile apps with technologies you already know; namely React. It makes it super easy to share code across different platforms, and lets you also expand your web app to the world of mobile.

Prerequisites

There are some prerequisites for this tutorial. It is assumed that you already familiar with React, and know, how JSX works. We will also be using React hooks. If you have no experience in using hooks, I have a tutorial covering how you can fetch data with them. It gives you the concepts that we will use in this tutorial.

How To Easily Fetch Data With React Hooks

Apart from that, you don’t need prior knowledge of mobile development. Everything will be written in JavaScript. So let’s see what we are going to build.


What Will be Building?

We will be building the following application:

Preview of the app

The classical todo app, where you can create and manage different todo lists. We will take a look at a number of things, which will help you understand, how to tackle common problems when building React Native mobile apps. Here’s a list of what will be covered in this tutorial:

Without further ado, let’s jump into setting up the project.


Setting up the Project

To get faster into coding, we will make use of Expo. It is a set of tools for building apps without much configuration. It can be used both for Android and IOS. We will take a more throughout look at Expo at the end of this tutorial. For now, start by installing the expo-cli globally with:

npm i -g expo-cli

Once Expo is installed, go ahead and bootstrap an empty React Native project with expo init [TheNameOfYourProject]. Expo will ask you which template you’ll want to use. Choose “blank”.

Choosing an Expo template

Once installed, Expo will also download the necessary node dependencies for you so you don’t need to run npm i separately inside your project.

Expo will also download dependencies for you.

CD into your project and you will be greeted with the following folder structure:

The project structure of React Native

The Structure of a React Native Project

The files and folders that are worth nothing are the two folders at the top, and your app.json file. Let’s start with the .expo-shared folder.

.expo-shared

Inside this folder, you will only find one file, called assets.json. This file is required for asset optimization. It contains information on which asset was optimized by Expo before. This guarantees that if you run expo optimize on an image, the same image can’t be optimized again, unless it changes.

This is useful if multiple people are working on the same project. It ensures that optimized assets are synced across collaborators.

.expo

You may don’t have this folder right away, but Expo will generate one, once you start developing. This folder contains settings and cached assets. If you experience problems with Expo, you may try to delete this folder to clear the cache.

assets

Your assets folder is where you should store your static assets, such as images and fonts.

app.json

This is where you can configure your app. It can be used for configuring your app’s overall feel and look, such as your app’s name, description, it’s icon or splash screen.

For the full list of options available, you can refer to Expo’s official documentation.

The rest of the files are not specifically related to Expo and React Native, you can find them in other projects. Therefore, they are pretty self-explanatory.

Note: your folder structure will slightly differ if you don’t use Expo for your project.

How to Start and Test Your App

The next step is to actually start the project. Run npm run android to start the server. It will start Metro bundler for you, the official JavaScript bundler for React Native.

The dashboard of Metro bundler

It lets you run your app in a browser, in an emulator, or on your actual device. Right now, it couldn’t find any Android device, so let’s fix that.

Head over to Google Play Store, and download the Expo client for Android. This is used for previewing the app. Next, you need to enabled USB debugging on your phone, to allow Expo to communicate with it.

Enable USB debugging

If you already know, how to enable USB debugging on your phone, or it is already enabled, you can skip this step.

First, head over to your “Settings“, click on “About Phone“, and scroll down to “Build Number“. Tap the build number 7 times to enable developer options.

Inside your settings, now you should see a “Developer options” menu. Click on it and look for “USB debugging” and enable it.

Depending on your Android version, the steps may differ a bit for you. For example, on Android 8.0, you should look for “System” rather than “About Phone“. For a more throughout step-by-step tutorial on how to enable USB debugging, please refer to Android Studio’s official documentation:

Configure on-device developer options

Launching the app on your mobile

Now, connect your phone to your PC through USB, and relaunch Metro bundler with npm run android. It should now open the app on your phone.

Metro bundler successfully connected to Android device
The bundler successfully attached the debugger to the device.

If you are seeing a blank white screen or you are getting a “Network response timed out” error, you can try the following steps:

Set connection setting to either tunnel or local in metro bundler if you have problems with the connection.

This issue has also been brought up on the expo-cli repo. If none of the above options solved your issue, you can read through the thread for additional solutions.

Expo-cli repo: Network response timed out.

If you experience problems with Expo after development, you can also try to run Expo with a clean cache, using expo r -c, or you can reinstall packages by first deleting your node_modules folder.

Making your first changes

If everything was set up correctly, you should see a message on your phone!

Open up App.js to start working on your app!

So go ahead as the message says and open your App.js file. This is where your app bootstraps from. Change the message and you’ll see that changes are reflected instantly.

Expo comes with built-in live reload

Configuring the Splash Screen

The splash screen for your app is configured through the app.json file in your project’s root. However, Expo comes with a default splash screen, so all you need to do, is change the image inside assets/splash.png.

To make the resolution XHDPI, make it at least 720px x 1280px. For a full list of dimensions, you can see Phonegap’s documentation.

White borders around splash screen

However, this alone won’t look too good. As you can see, there are white borders around the splash screen. To change it, head over to your app.json file and change resizeMode to cover and also change the background color to a similar one to the splash screen to make it blend in.

"splash": {
  "image": "./assets/splash.png",
  "resizeMode": "cover",
  "backgroundColor": "#6300ED"
},
app.json
Copied to clipboard!

While here, you can also custimize your app’s icon

No more white borders shown around slash screen.
No more white borders

Adding a Header for the App

Let’s start building the app, by adding the header first. To avoid messing around with styles, I will be using a UI library for this project, called React Native Paper.

The homepage of React Native Paper

Install React Native Paper as a dependency by running yarn add react-native-paper inside your console.

To keep things organized, create a new folder called components and create a Header.js file inside for the Header component.

import * as React from 'react';
import { Appbar } from 'react-native-paper';

const Header = () => {
    return (
        <Appbar.Header>
            <Appbar.Content title="Checklists" />
            <Appbar.Action icon="plus-circle" />
        </Appbar.Header>
    );
};

export default Header;
Header.js
Copied to clipboard!

Import Ąppbar from react-native-paper and export the above component. Inside your App.js file, you can remove everything for now, and import the Header.

import React from 'react';

import Header from './components/Header';

export default function App() {
    return (
        <Header />
    );
}
App.js
Copied to clipboard!

This will create a header similar to the one below.

A Header component created in React Native

Displaying Data Inside the App

To display any data, we need a way to store the data created by users, that we can later reuse inside their app. For this purpose, I will be using the async-storage package, which provides a global state for the app.

Async Storage is an easy solution for small applications where you need unencrypted data stored in key-value pairs. It stores information on your device and stays there until the app is deleted. It works like a Local Storage on the web.

Setting up a data storage

Install the package with yarn add @react-native-community/async-storage, and create a file called Storage.js at the root of your project. This file will be responsible for getting, setting, and removing data. This means we will need to export a function with three methods for now: get, set, and remove. Add the following to your Storage.js:

import AsyncStorage from '@react-native-community/async-storage';

const Storage = {
    async get() {
        try {
            const json = await AsyncStorage.getItem('@data');
            
            return json != null ? JSON.parse(json) : [];
        } catch(e) {
            // Error reading value
        }
    },

    async set(checklist) {
        try {
            const currentState = await this.get();
            const newState = currentState.map(state => checklist.id === state.id ? checklist : state);

            await AsyncStorage.setItem('@data', JSON.stringify(newState));
        } catch (e) {
            // Saving error
        }
    },

    async remove(id) {
        try {
            const currentState = await this.get();
            const newState = currentState.filter(state => state.id !== id);

            await AsyncStorage.setItem('@data', JSON.stringify(newState));
        } catch (e) {
            // Saving error
        }
    }
};

export default Storage;
Storage.js
Copied to clipboard!

AsyncStorage — as the name suggests — works with async/await so we need async functions. It’s only capable of storing string values, so if you need to work with complex objects, it has to be converted with JSON.stringify and JSON.parse.

The param you pass to getItem and setItem can be a string of your choice. In case we already have some user-generated data, we return that inside get, otherwise, we can return an empty array.

For setting a checklist, we will pass the whole object to the function, and create a newState based on the passed checklist’s id, then rewrite it to @data.

You can import this file into your App.js and create a new variable to make the data globally available.

import React from 'react';

import Header from './components/Header';

import Storage from  './Storage';

const checklists = Storage.get();

export default function App() {
    return (
        <Header />
    );
}
App.js
Copied to clipboard!

Based on this data, we can generate a list and display it to the user. But before doing that, let’s see what kind of data do we need exactly.

The required fields for the app

As you can see from the diagram above, we have two views. One for the lists and one for each individual todo list.

Inside the list, we will need to display:

Inside each checklist, we can have any number of items and they will have:

Here we will also show the total number of items, above the remaining items. This means, we can get away with a data structure similar to this:

[{
  "id": 0,
  "name": "Shopping list",
  "date": {
    "year": 2020,
    "monthAndDay": "June 23"
  },
  "items": [
    {
      "id": 0,
      "name": "Pasta",
      "checked": false
    },
    {
      "id": 1,
      "name": "Rice",
      "checked": true
    }
  ]
}, { ... }, { ... }]
data.json
Copied to clipboard!

There’s a couple of things you can notice. For example, I’ve broken down the date to year and month/day into separate nodes. This is because they will get different styles, so they need to be handled separately.

Another thing is that both checklists and their items have ids. This is required for identifying each element, so we update the correct one whenever we make changes.

You may also notice there is no node for the total and remaining items. We don’t need to store them as both can be calculated based on the items array.

Displaying the stored data

To test things out, temporarily add the above example data to to get function of Storage.

return json != null ? JSON.parse(json) : [{
  // Put your test data here
}];
Storage.js
Copied to clipboard!

Create a new file called List.js under components and import it to your App component, passing the checklists down to it.

import React from 'react';

import Header from './components/Header';
import List from './components/List';

import Storage from  './Storage';

const checklists = Storage.get();

export default function App() {
    return (
        <React.Fragment>
            <Header />
            <List list={checklists} />
        </React.Fragment>
    );
};
App.js
Copied to clipboard!

Inside your List.js component, export the following:

import React from 'react';
import { Text, View } from 'react-native';
import { Button, List } from 'react-native-paper';

const Checklist = ({ list }) => {
    const AddButton = () => <Button mode="contained">Add More Lists</Button>;
    const Date = ({ date }) => (
        <View>
            <Text>{date.year}</Text>
            <Text>{date.monthAndDay}</Text>
        </View>
    );

    if (!list.length) {
        return (
            <View>
                <AddButton />
            </View>
        );
    }

    const checklists = list.map((checklist, index) => (
        <List.Item
            key={index}
            title={checklist.name}
            description={`${checklist.items.filter(item => !item.checked).length} item(s) remaining`}
            left={props => <List.Icon icon={require('../assets/checklist-icon.png')} />}
            right={props => <Date date={checklist.date} />}
        />
    ));

    return (
        <View>
            {checklists}
            <AddButton />
        </View>
    );
};

export default Checklist;
List.js
Copied to clipboard!

This is the component where we want to export the whole list and the add button. Both the Button and List components are used from react-native-paper.

You’ll also notice that I’ve used View and Text for the Date. This is because, in React Native, you need to wrap texts into Text components to display them. Also, you can’t use wrappers like div. Instead, you need to use View.

I’ve also added an if statement, to check if there’s any checklist to be displayed. If there’s none, we will just display the AddButton.

As you can see, inside the description prop of the List.Item we can filter for the unchecked entries to calculate the number of remaining items. I’ve also added a custom icon to the assets folder, which I’ve imported here to be used on the left-hand side. On the right-hand side, the creation date will be displayed.

The only problem with this solution is that the list prop, which contains the data, returns a Promise from the AsyncStorage. To battle this, we need to use a hook to wait for the arrival of the data, before using it. But instead of doing this inside the List component, let’s lift the logic up to the App component, as we will reuse this data elsewhere throughout the application. Rewrite your App.js file accordingly:

import React, { useState, useEffect } from 'react';
import Header from './components/Header';
import List from './components/List';

import Storage from  './Storage';

export default function App() {
    const [checklists, updateChecklists] = useState({});

    useEffect(() => {
        const getChecklist = async () => {
            const checklistResponse = await Storage.get();

            updateChecklists(checklistResponse);
        };
       
        getChecklist();
    }, []);

    return (
        <React.Fragment>
            <Header />
            <List list={checklists} />
        </React.Fragment>
    );
};
App.js
Copied to clipboard!

Import useState and useEffect from React, then create a new state on line:8. Add a new useEffect block and fetch the results from the Storage. Since useEffects can’t be an async function directly, you’ll need to define a function inside it.

With this component in place, you should have a similar view:

Display data inside React Native

Adding Styles to the App

As you can see, it’s not looking quite similar to the design of the app, so let’s add some styles. First, change the background color to black. Global styles like this, can be configured inside your app.json. Add the following line:

"backgroundColor": "#111111"
app.json
Copied to clipboard!

Now we can configure the styles for the list items. However, you can’t use regular CSS in React Native. Instead, you will need to define the styles in JavaScript objects.

Open your List.js file and create a new stylesheet with the following code:

// Also import StyleSheet from 'react-native'
import { Text, View, StyleSheet } from 'react-native';

const styles = StyleSheet.create({
    bold: {
        fontWeight: 'bold'
    },
    list: {
        margin: 15
    },
    listItem: {
        backgroundColor: '#FFF',
        marginBottom: 15
    },
    listItemIcon: {
        marginLeft: 0
    },
    date: {
      display: 'flex',
      flexDirection: 'column',
      justifyContent: 'center',
      alignItems: 'flex-end',
      marginRight: 10
    },
    button: {
        borderRadius: 0
    }
});

// Then use the styles inside the component where it's needed, like so
const Date = ({ date }) => (
  <View style={styles.date}>
    <Text style={styles.bold}>{date.year}</Text>
    <Text>{date.monthAndDay}</Text>
  </View>
);
List.js
Copied to clipboard!

After applying the styles to every element, you should have a similar look to the final result.

After applying styles to the app
After applying styles to the app

Using custom fonts

However, there’s still one thing missing: the fonts. Adding them is a little bit tricky, but nothing impossible. In React Native, style inheritance is limited. This means you can’t set a global font-family, like you do on the web. Instead, you need to provide the styles individually.

First, run expo install expo-font to add the ability to import fonts, then inside your App.js rewrite the component in the following way:

import React, { useState, useEffect } from 'react';
import { AppLoading } from 'expo';
import { useFonts } from 'expo-font';

import Header from './components/Header';
import List from './components/List';

import Storage from  './Storage';

export default function App() {
    const [checklists, updateChecklists] = useState({});
    const [fontsLoaded] = useFonts({
        'Lora-Regular': require('./assets/fonts/Lora-Regular.ttf'),
        'Lora-Bold':    require('./assets/fonts/Lora-Bold.ttf')
    });

    useEffect(() => {
        const getChecklist = async () => {
            const checklistResponse = await Storage.get();

            updateChecklists(checklistResponse);
        };
       
        getChecklist();
    }, []);

    if (!fontsLoaded) {
        return <AppLoading />;
    } else {
        return (
            <React.Fragment>
                <Header />
                <List list={checklists} />
            </React.Fragment>
        );
    }
};
App.js
Copied to clipboard!

Import AppLoading and the useFonts hooks and import the fonts, using the hook, starting from line:11. You can get the fonts from Google Fonts. I will also include it among the assets in the GitHub repository.

It’s a common practice to wait for the fonts to be loaded before displaying anything. With an if check at the beginning, you can tell Expo to show the loading screen as long as the fonts are not loaded. And now it’s time to use them.

Inside your Header component, add a titleStyle property to Appbar.Content:

import * as React from 'react';
import { Appbar } from 'react-native-paper';

const Header = () => {
    return (
        <Appbar.Header>
            <Appbar.Content title="Checklists" titleStyle={{ fontFamily: 'Lora-Bold' }} />
            <Appbar.Action icon="plus-circle" />
        </Appbar.Header>
    );
};

export default Header;
Header.js
Copied to clipboard!

Repeat the same steps for List. For the styles, change fontWeight to fontFamily instead, and don’t forget to add a rule for the regular version.

const styles = StyleSheet.create({
    regular: {
        fontFamily: 'Lora-Regular'
    },
    bold: {
        fontFamily: 'Lora-Bold'
    },
    ...
});
List.js
Copied to clipboard!

To change the style of the title and the description on each list item, you can use the titleStyle and descriptionStyle properties respectively:

<List.Item
    key={index}
    style={styles.listItem}
    title={checklist.name}
    titleStyle={styles.bold}
    description={`${checklist.items.filter(item => !item.checked).length} item(s) remaining`}
    descriptionStyle={styles.regular}
    left={props => <List.Icon style={styles.listItemIcon} icon={require('../assets/checklist-icon.png')} />}
    right={props => <Date date={checklist.date} />}
/>
List.js
Copied to clipboard!

And with the fonts in place, everything should start to come together.

Using the new fonts in React Native

Adding Interactions

Let’s move on with adding some interactions to the elements. First, we want to implement the logic for adding new checklists. Remove the temporary mock data from Strorage and open your Header component.

Add a new prop and an onPress property to your Appbar.Action.

import * as React from 'react';
import { Appbar } from 'react-native-paper';

// Destructure `addChecklist` from the props
const Header = ({ addChecklist }) => {
    return (
        <Appbar.Header>
            <Appbar.Content title="Checklists" titleStyle={{ fontFamily: 'Lora-Bold' }} />
            <Appbar.Action icon="plus-circle" onPress={addChecklist} />
        </Appbar.Header>
    );
};

export default Header;
Header.js
Copied to clipboard!

We will define this function inside App.js. Open the file and add a new function, which you also pass down to Header. Do the same to the List component and attach an onPress property to the Buttons inside it, the same way we did to Header.

export default function App() {
    ...
    
    const addChecklist = async () => {
        await Storage.add();
        updateChecklists(await Storage.get());
    };

    ...

    if (!fontsLoaded) {
        return <AppLoading />;
    } else {
        return (
            <React.Fragment>
                {/* Pass down the function to the components */}
                <Header addChecklist={addChecklist} />
                <List list={checklists} addChecklist={addChecklist} />
            </React.Fragment>
        );
    }
};
App.js
Copied to clipboard!

This function will create a new entry and update checklists which will make the List component to rerender.

Of course, we didn’t define any add method for Storage yet, so let’s do that now. Extend the Storage object with the following function:

async add() {
    try {
        const state = await this.get();
        const date = new Date();
        const month = monthNames[date.getMonth()];
        const day = ('0' + date.getDate()).slice(-2);
            
        state.unshift({
            id: ((state[0] || {}).id + 1) || 0,
            name: 'New List',
            date: {
                year: date.getFullYear(),
                monthAndDay: `${month} ${day}`
            },
            items: []
        });

        await AsyncStorage.setItem('@data', JSON.stringify(state));
    } catch (e) {
        // Saving error
    }
}
Storage.js
Copied to clipboard!

This will get the current state of the app and pushes a new entry into it. Unshifting the array will make sure to place the most recent checklist to the top. To make sure ids are unique, I’ve grabbed the first checklist’s id in the list and added +1 to it. If there are no items yet, the id will fall back to 0. To format the date, I’ve also created an array for the short month names, outside the Storage object.

const monthNames = [
    'Jan',
    'Feb',
    'Mar',
    'Apr',
    'May',
    'Jun',
    'Jul',
    'Aug',
    'Sep',
    'Oct',
    'Nov',
    'Dec'
];
Storage.js
Copied to clipboard!

With everything in place, you should be able to create new entries by tapping on the buttons.

Adding items inside the app

There’s only one problem. You can’t scroll down when there are too many items. To fix it, head over to your List component and change the return statement from View to use ScrollView:

import { Text, View, StyleSheet, ScrollView } from 'react-native';

...

return (
    <ScrollView style={styles.list}>
        {checklists}
        <AddButton />
    </ScrollView>
);
List.js
Copied to clipboard!

To also keep the button visible at all times, you can outsource it outside of the ScrollView:

import { Text, View, StyleSheet, ScrollView } from 'react-native';

...

return (
    <React.Fragment>
        <ScrollView style={styles.list}>
            {checklists}
        </ScrollView>
        {/* Make sure you wrap the button into a View to give it the same margins */}
        <View style={styles.list}>
            <AddButton />
        </View>
    </React.Fragment>
);
List.js
Copied to clipboard!
Scrolling through the checklists

How to Add Routing

Now it’s time to implement the details view for each checklist. To do so, however, we need to put some simple routing in place, to change between different views. First, you will have to install a bunch of packages. Copy and paste the following into your console:

npm install @react-navigation/native @react-navigation/stack @react-native-community/masked-view react-native-screens react-native-safe-area-context react-native-gesture-handler react-native-reanimated

The next step is to add some wrapper elements for the routes. Inside the App component, import NavigationContainer and createSackNavigator and create a new stack, by calling the imported function.

import { NavigationContainer } from '@react-navigation/native';
import { createStackNavigator } from '@react-navigation/stack';

const Stack = createStackNavigator();
App.js
Copied to clipboard!

Inside the else path, change the return statement to the following:

const WrappedList = props => <List list={checklists} addChecklist={addChecklist} {...props} />;

return (
    <React.Fragment>
        <Header addChecklist={addChecklist} />
        <NavigationContainer>
            <Stack.Navigator>
                <Stack.Screen name="List" component={WrappedList} />
            </Stack.Navigator>
        </NavigationContainer>
    </React.Fragment>
);
App.js
Copied to clipboard!

The Header can stay outside of the NavigationContainer as that won’t be affected directly by the navigation. The WrappedList is needed for passing additional props created by us. Alternatively, you can also write:

return (
    <React.Fragment>
        <Header addChecklist={addChecklist} />
        <NavigationContainer>
            <Stack.Navigator>
                <Stack.Screen name="List">
                    {props => <List {...props} list={checklists} addChecklist={addChecklist} />}
                </Stack.Screen>
            </Stack.Navigator>
        </NavigationContainer>
    </React.Fragment>
);
App.js
Copied to clipboard!

And this way, you don’t need the extra function. This setup will make it easy to navigate between different components. However, it does two things:

React Navigation broke styles

To fix both of them, add the following prop to Stack.Navigator:

// Import constants at the top of your component
// Contants.manifest will hold the values specified in app.json
import Constants from 'expo-constants';

<Stack.Navigator screenOptions={{ headerShown: false, cardStyle: { backgroundColor: Constants.manifest.backgroundColor } }}>
  ...
</Stack.Navigator>
App.js
Copied to clipboard!

Making the Details View

To change between the views, we need another view. Create a new file under components called Details.js, and add something to display; a button for navigating back:

import React from 'react';
import { Button } from 'react-native-paper';

const Details = ({ navigation }) => {
    return (
        <Button mode="contained" onPress={() => navigation.navigate('List')}>Go back</Button>
    );
};

export default Details;
Details.js
Copied to clipboard!

As you can see, we can navigate between different components, by calling navigation.navigate, passing the name of the route. Inside your App component, import Details and add it as a new route to the NavigationContainer.

import Details from './components/Details';

...

return (
    <React.Fragment>
        <Header addChecklist={addChecklist} />
        <NavigationContainer>
            <Stack.Navigator screenOptions={{ headerShown: false, cardStyle: { backgroundColor: Constants.manifest.backgroundColor } }}>
                <Stack.Screen name="List" component={WrappedList} />
                <Stack.Screen name="Details" component={Details} />
            </Stack.Navigator>
        </NavigationContainer>
    </React.Fragment>
);
App.js
Copied to clipboard!

The name you pass to Stack.Screen can be referenced in navigation.navigate.

To be able to go to the details view, add an onPress prop to your List.Items inside your List component.

// Add `navigation` as a prop
const Checklist = ({ list, addChecklist, navigation }) => {
    ...

    const checklists = list.map((checklist, index) => (
        <List.Item
            key={index}
            style={styles.listItem}
            title={checklist.name}
            titleStyle={styles.bold}
            description={`${checklist.items.filter(item => !item.checked).length} item(s) remaining`}
            descriptionStyle={styles.regular}
            onPress={() => navigation.navigate('Details', checklist)}
            left={() => <List.Icon style={styles.listItemIcon} icon={require('../assets/checklist-icon.png')} />}
            right={() => <Date date={checklist.date} />}
        />
    ));

    ...
};
List.js
Copied to clipboard!

Also, pass in the checklist as the second parameter. This will make the checklist available inside the Details component. So far, we only have a button. Let’s add the remaining elements.

Changing routes in React Native

Adding the remaining elements

Modify your Details component in the following way, to add the missing elements:

import React from 'react';
import { Text, View, StyleSheet, ScrollView } from 'react-native';
import { Button, Checkbox, List, IconButton } from 'react-native-paper';

const styles = StyleSheet.create({
    regular: {
        fontFamily: 'Lora-Regular'
    },
    bold: {
        fontFamily: 'Lora-Bold'
    },
    remaining: {
        color: '#888888'
    },
    year: {
        fontSize: 16
    },
    strikeThrough: {
        textDecorationLine: 'line-through',
        color: '#888'
    },
    statusBar: {
        backgroundColor: '#FFF',
        display: 'flex',
        flexDirection: 'row',
        paddingHorizontal: 20,
        paddingVertical: 10,
        justifyContent: 'space-between'
    }
});

const Details = ({ route }) => {
    const checklist = route.params;
    const StatusBar = ({ total, remaining, date }) => (
        <View style={styles.statusBar}>
            <View>
                <Text style={styles.regular}>{total} items</Text>
                <Text style={[styles.regular, styles.remaining]}>{remaining} remaining</Text>
            </View>
            <View style={{ alignItems: 'flex-end' }}>
                <Text style={[styles.bold, styles.year]}>{date.year}</Text>
                <Text style={styles.regular}>{date.monthAndDay}</Text>
            </View>
        </View>
    );

    return (
        <React.Fragment>
            <StatusBar date={checklist.date} total={checklist.items.length} remaining={checklist.items.filter(item => !item.checked).length} />
            <ScrollView>
                {checklist.items.map((item, index) => (
                    <List.Item
                        key={index}
                        title={item.name}
                        titleStyle={[styles.regular, item.checked ? styles.strikeThrough : { color: '#FFF' }]}
                        onPress={() => console.log('edit')}
                        left={() => (
                            <View style={{ justifyContent: 'center' }}>
                                <Checkbox status={item.checked ? 'checked' : 'unchecked'} onPress={() => console.log('checked')} uncheckedColor="#FFF" color="#FFF" />
                            </View>
                        )}
                        right={() => <IconButton icon="close-circle-outline" color="#FFF" onPress={() => console.log('delete')} /> }
                    />
                ))}
            </ScrollView>
            <View style={{ margin: 15 }}>
                <Button mode="contained" style={{ borderRadius: 0 }} labelStyle={styles.bold} onPress={() => navigation.navigate('List')}>Add todo</Button>
            </View>
        </React.Fragment>
    );
};

export default Details;
Details.js
Copied to clipboard!

On line:33, you can access the passed params with route.params. I’ve outsourced the status bar into a separate component to make the whole file more readable, applied the necessary styles to each element, and finally, added an onPress prop to all interactive elements. I’ve also created some mock data, just so we can verify the styles.

If you run the app inside the browser, you can also see the log messages appearing inside the console. (while on mobile, the messages will appear inside the terminal.)

Adding interactivity to the view

So far, the buttons doesn’t do anything. Let’s change that and add the ability to create, edit and delete todo items.

// Import `useState` at the top of your file
import React, { useState } from 'react';
// Also import storage, so we can sync data
import Storage from  '../Storage';

const Details = ({ route }) => {
    // Move the checklist variable behind a `useState`
    const [checklist, updateChecklist] = useState(route.params);
   
   ...   
 
    return (
        <React.Fragment>
            <StatusBar date={checklist.date} total={checklist.items.length} remaining={checklist.items.filter(item => !item.checked).length} />
            <ScrollView>
                {checklist.items.map((item, index) => (
                    <List.Item
                        key={index}
                        title={item.name}
                        titleStyle={[styles.regular, item.checked ? styles.strikeThrough : { color: '#FFF' }]}
                        onPress={() => showModal(item)}
                        left={() => (
                            <View style={{ justifyContent: 'center' }}>
                                <Checkbox status={item.checked ? 'checked' : 'unchecked'} onPress={() => markTodo(item.id)} uncheckedColor="#FFF" color="#FFF" />
                            </View>
                        )}
                        right={() => <IconButton icon="close-circle-outline" color="#FFF" onPress={() => deleteTodo(item.id)} /> }
                    />
                ))}
            </ScrollView>
            <View style={{ margin: 15 }}>
                <Button mode="contained" style={{ borderRadius: 0 }} labelStyle={styles.bold} onPress={addTodo}>Add todo</Button>
            </View>
        </React.Fragment>
    );
};
Details.js
Copied to clipboard!

Import useState at the top and the Storage we’ve created, then change the console.log callbacks to the one shown above in the code example. Then define these function calls above the return statement:

const updateData = () => {
    updateChecklist({...checklist});
    Storage.set(checklist);
} 

const addTodo = () => {
    checklist.items.unshift({
        id: ((checklist.items[0] || {}).id + 1) || 0,
        name: `Todo #${checklist.items.length + 1}`,
        checked: false
    });

    updateData();
};

const markTodo = id => {
    checklist.items = checklist.items.map(item => {
        if (item.id === id) {
            item.checked = !item.checked;
        }

        return item;
    });

    updateData();
};

const deleteTodo = id => {
    checklist.items = checklist.items.filter(item => item.id !== id);

    updateData();
};
Details.js
Copied to clipboard!

I’ve outsource the data updating into a separate function as that part is reused in multiple function. It first updates the checklist variable to reflect the current state, then also stores the changes. Notice that you’ll also have to provide a new object each time, otherwise React will not update the view. This is why you need object destructuring.

You might also notice we don’t have a function for editing a todo item. This is because, — you may have seen in the previous code example — we have a showModal function call instead, since we want to edit the items inside a modal.

Making changes to todo items
We can now add items, mark and remove them

Displaying information inside a modal

As a next step, it’s time to display the todo’s name inside a modal, so we can actually change it. To be able to work with modals, also import Modal, Portal, Provider and TextInput for the input element, from react-native-paper, and add a couple of new state.

import { Button, Checkbox, List, IconButton, Modal, Portal, Provider, TextInput } from 'react-native-paper';

const Details = ({ route }) => {
    const [checklist, updateChecklist] = useState(route.params);
    const [visible, setVisible] = useState(false);
    const [text, setText] = useState('');
    const [itemID, setItemID] = useState(0);
    
    const hideModal = () => setVisible(false);
    const showModal = item => {
        setItemID(item.id)
        setText(item.name);
        setVisible(true);
    };

    return (
        <Provider>
            <StatusBar ... />
            <ScrollView>...</ScrollView>
            <View style={{ margin: 15 }}>
                <Button mode="contained" style={{ borderRadius: 0 }} labelStyle={styles.bold} onPress={addTodo}>Add todo</Button>
            </View>

            <Portal>
                <Modal visible={visible} onDismiss={hideModal} contentContainerStyle={{ backgroundColor: '#FFF', margin: 15 }}>
                    <TextInput label="Todo's name" value={text} onChangeText={value => editTodo(value)} />
                </Modal>
            </Portal>
        </Provider>
    );
}
Details.js
Copied to clipboard!

Below the “Add todo” button, add a Modal and wrap it inside a Portal. Make sure you also replace React.Fragment to Provider as shown above. The modal will be visible, whenever we click on an item.

It will call the showModal function which sets an ID (the item we are editing), set the value of the input, and also set the modal to visible.

As you can see, here we have an editTodo function which is called, everytime a change is made to the input. Define this function above the return statement as well, next to the other functions.

const editTodo = value => {
    setText(value);

    checklist.items = checklist.items.map(item => {
        if (item.id === itemID) {
            item.name = value;
        }

        return item;
    });

    updateData();
};
Details.js
Copied to clipboard!

With this in place, you should now be able to change the name of each todo:

changing names of todos

If you go back to the home page, however, the changes are not reflected.


Reflect Changes on Different Routes

To fix this, we need to reset checklist inside the App component to trigger a rerender. Add a new function and pass it down to the Details component:

export default function App() {
    ...
    const refreshChecklist = async () => {
        updateChecklists(await Storage.get());
    };
    
    ...

    if (!fontsLoaded) {
        return <AppLoading />;
    } else {
        const WrappedList = props => <List list={checklists} addChecklist={addChecklist} {...props} />;
        const WrappedDetails = props => <Details refreshChecklist={refreshChecklist} {...props} />

        return (
            <React.Fragment>
                <Header addChecklist={addChecklist} />
                <NavigationContainer>
                    <Stack.Navigator screenOptions={{ headerShown: false, cardStyle: { backgroundColor: Constants.manifest.backgroundColor } }}>
                        <Stack.Screen name="List" component={WrappedList} />
                        <Stack.Screen name="Details" component={WrappedDetails} />
                    </Stack.Navigator>
                </NavigationContainer>
            </React.Fragment>
        );
    }
}
App.js
Copied to clipboard!

Then inside your Details component, get the prop, and add a useEffect hook after importing it:

const Details = ({ route, navigation, refreshChecklist }) => {
    ...

    useEffect(() => {
        return async () => {
            await refreshChecklist();
        }
    }, []);
};
Details.js
Copied to clipboard!

By specifying a return statement, the update will only run once the component unmounts. This way, the changes will be reflected in both views. However, the header still stays the same, so let’s take care of it.


Adding Extra Functionality to Headers

Whenever the user lands inside a checklist, we want to show the name of the checklist below the header’s title. Here we also want to provide buttons to edit and delete the checklist itself. Let’s start with the subtitle.

To show different titles based on different checklists, we will have to rerender the App component, and with it, the Header. We also need to pass the currently selected checklist to the Header, in order to display the correct name. This means, we will need to introduce additional state, that will hold the name based on the active checklist.

However, instead of adding another useState inside the App component, it’s better to extend the existing state. That way, we don’t have to call setState twice, so the app won’t be rerendered twice.

Rewrite the states in your App component, in the following way:

export default function App() {
-   const [checklists, updateChecklists] = useState({});
+   const [data, updateData] = useState({
+       allChecklists: [],
+       selectedChecklist: {}
+   });
    const [fontsLoaded] = useFonts({
        'Lora-Regular': require('./assets/fonts/Lora-Regular.ttf'),
        'Lora-Bold':    require('./assets/fonts/Lora-Bold.ttf')
    });

    const refreshChecklist = async () => {
-       updateChecklists(await Storage.get());
+       updateData({
+           allChecklists: await Storage.get(),
+           selectedChecklist: {}
+       });
    };
    
    const addChecklist = async () => {
        await Storage.add();
-       updateChecklists(await Storage.get());

+       updateData({
+           allChecklists: await Storage.get(),
+           selectedChecklist: {}
+       });
    };
    
    useEffect(() => {
        const getChecklist = async () => {
            const checklistResponse = await Storage.get();
            
-           updateChecklists(checklistResponse);
+           updateData({
+               allChecklists: checklistResponse,
+               selectedChecklist: {}
+           });
        };
       
        getChecklist();
    }, []);
   
    ...
};
App.diff
Copied to clipboard!

Don’t forget to also pass data.allChecklists instead of checklists down to your List component.

This so far, makes no visible changes, as we haven’t defined a function for changing the selectedChecklist‘s value. Add a new function inside your App component:

const refreshHeader = checklist => {
    updateData({
        allChecklists: data.allChecklists,
        selectedChecklist: checklist
    });
};

...

// Later down in the component, add it as a prop to `List`
const WrappedList = props => <List list={data.allChecklists} addChecklist={addChecklist} refreshHeader={refreshHeader} {...props} />;
App.js
Copied to clipboard!

The function will set selectedChecklist to the object it is passed. Inside the List component, we can pass this object. Add refreshHeader to the list of props and change the onPress property of List.Item in the following way:

const checklists = list.map((checklist, index) => (
    <List.Item
        key={index}
        style={styles.listItem}
        title={checklist.name}
        titleStyle={styles.bold}
        description={`${checklist.items.filter(item => !item.checked).length} item(s) remaining`}
        descriptionStyle={styles.regular}
        onPress={() => {
            refreshHeader(checklist);
            navigation.navigate('Details', checklist);
        }}
        left={() => <List.Icon style={styles.listItemIcon} icon={require('../assets/checklist-icon.png')} />}
        right={() => <Date date={checklist.date} />}
    />
));
List.js
Copied to clipboard!

This means, every time we click on a checklist, it will rerender the app for us, with the correct values for the header. Go back to your App component, and inside the return statement, pass the selectedChecklist down to Header:

return (
    <React.Fragment>
        <Header addChecklist={addChecklist} selectedChecklist={data.selectedChecklist} />
        <NavigationContainer>
            ...
        </NavigationContainer>
    </React.Fragment>
);
App.js
Copied to clipboard!

Now, open up your Header component and add a subtitle prop to Appbar.Content.

import * as React from 'react';
import { Appbar } from 'react-native-paper';

const Header = ({ addChecklist, selectedChecklist }) => {
    return (
        <Appbar.Header>
            <Appbar.Content
                title="Checklists"
                titleStyle={{ fontFamily: 'Lora-Bold' }}
                subtitle={selectedChecklist.name ? selectedChecklist.name : null}
                subtitleStyle={{ fontFamily: 'Lora-Regular', color: '#B180F6' }}
            />
            <Appbar.Action icon="plus-circle" onPress={addChecklist} />
        </Appbar.Header>
    );
};

export default Header;
Header.js
Copied to clipboard!

By checking first, if there’s a name for selectedChecklist, we can conditionally display a subtitle.

Displaying a subtitle inside a header
The subtitle is now visible inside the header

Adding action buttons inside the header

We also want to change the plus sign, into an edit and delete button. Open the Header component and set the Appbar.Action to the following if-else:

{selectedChecklist.name ? (
    <React.Fragment>
        <Appbar.Action icon="pencil" onPress={toggleInput} color="#FFF" />
        <Appbar.Action icon="delete-forever" onPress={deleteChecklist} color="#FFF" />
    </React.Fragment>
) :
    <Appbar.Action icon="plus-circle" onPress={addChecklist} />
}
Header.js
Copied to clipboard!

This will create the edit and delete buttons. Let’s first add the functionality for editing names. Open your Storage and add a new function:

async rename(id, checklistName) {
    try {
        const currentState = await this.get();
        const newState = currentState.map(state => {
            if (state.id === id) {
                state.name = checklistName;
            }

            return state;
        });

        await AsyncStorage.setItem('@data', JSON.stringify(newState));
    } catch (e) {
        // Saving error
    }
}
Storage.js
Copied to clipboard!

This will except the id of the checklist, as well as a new name. Inside the Header, we will need to introduce some new states, so import useState and useEffect, then add the following functions:

const Header = ({ addChecklist, selectedChecklist }) => {
    const [checklist, setChecklist] = useState(selectedChecklist);
    const [inputVisible, setInputVisible] = useState(false);
    const [text, setText] = useState('');
    const toggleInput = () => {
        setText(selectedChecklist.name);
        setInputVisible(!inputVisible);
    };

    const editChecklist = async value => {
        checklist.name = value;

        setText(value);
        setChecklist(checklist);

        await Storage.rename(selectedChecklist.id, value);
    };

    useEffect(() => {
        setChecklist(selectedChecklist)

        return () => {
            setInputVisible(false);
        }
    }, [selectedChecklist]);

    return (
        <Appbar.Header>
            ...
        </Appbar.Header>
    );
};
Header.js
Copied to clipboard!

This will do a couple of things:

To display the input, change Appbar.Content to the following:

{inputVisible ?  
    <TextInput value={text} mode="outlined" style={{ height: 30, flexGrow: 1 }} onBlur={toggleInput} onChangeText={value => editChecklist(value)} /> :
    <Appbar.Content
        title="Checklists"
        titleStyle={{ fontFamily: 'Lora-Bold' }}
        subtitle={checklist.name}
        subtitleStyle={{ fontFamily: 'Lora-Regular', color: '#B180F6' }}
        style={{ paddingLeft: 0 }}
    />
}
Header.js
Copied to clipboard!

Don’t forget to also change the value of subtitle to reflect the change instantly.

Changing the name of a checklist

Removing checklists

Go to your Header component, and define a new function:

const deleteChecklist = async () => {
    await Storage.remove(selectedChecklist.id);

    navigator.current.navigate('List');
};
Header.js
Copied to clipboard!

This function is already referenced on the delete button. After the remove, we also want to navigate back to the list, but we don’t have access to the navigator object inside the Header as it sits outside of the navigation stack. To get around this, create a new ref inside your App component, and assign it to the NavigationContainer.

export default function App() {
    const navigationRef = React.createRef();
    ...

    return (
        <React.Fragment>
            <Header addChecklist={addChecklist} selectedChecklist={data.selectedChecklist} navigator={navigationRef} />
            <NavigationContainer ref={navigationRef}>
                <Stack.Navigator>
                    ...
                </Stack.Navigator>
            </NavigationContainer>
        </React.Fragment>
    );
};
App.js
Copied to clipboard!

Also make sure that you pass this new ref as a prop, down to Header. And with that, your very first app is ready to be shipped!

Removing checklists inside the app

How to Build the App

To start off, you’ll need to configure your app.json. Add a new node called android with a package and versionCode:

"android": {
    "package": "com.webtips.estalista",
    "versionCode": 1
}
app.json
Copied to clipboard!

To start the build process, run expo build:android. You’ll also need to have an Expo account. You can create one through the CLI from your terminal, or from Expo.io.

During the build, you will be prompted to choose between:

You can also run expo build:android -t apk or expo build:android -t app-bundle to skip this step.

It is recommended to go with app-bundle. The first time you build it, you will also be asked to provide a keystore. This is used for signing your app on Google Play Store. If you do updates to your app, you need to sign it with the same keystore it was created with. In case you don’t have a keystore, you can let Expo to generate one for you.

If you choose this option, make sure to run expo fetch:android:keystore afterward, and save your credentials to a safe place.

the turtle queue dashboard
Turtle Queue dashboard

Once everything set up, your app will be queued for build. The status of your app will be reflected in your terminal, but you can also follow the queue on Turtle Queues. For my build, it took around 15 minutes to be queued and built with around ~15 builds waiting in the queue.

Follow the progress of your app on Expo's dashboard
You can follow the progress through the terminal or from dashboard

Once ready, you can download the build file either from your Expo dashboard or from the link that is provided in your terminal.

Publishing the app

The last step is to publish your app. If this is your first time, you need to publish the app manually through Google Play Console. Note that the registration fee for publishing applications in the app store is $25. Head over to Google Play Console and click on “Create Application”.

In order to be able to publish the app, you will need to fulfill all the checks on the left-hand side menu. To get more information on what is needed to be complete, hover over the checkmarks.

The sidebar of Google Play Console
Make sure, every checkmark is green in Google Play Console

Here you can manage different releases or your app, and even make it only available for internal testing. For the first submission, I also recommend reading through Expo’s step by step tutorial.

Once you published your app, it will get reviewed by Google which can take some time before you actually able to download it.

If you are planning to release for iOS, a full tutorial is available on the official documentation of Expo as always.


What are the Drawbacks of Expo

Expo provides a great and easy way to quickly create mobile application with React Native, without much configuration upfront. Of course this comes at a price.

First off, there are some native APIs that are not supported yet. As Expo handling the compiling and building step, there is a limit on what you can use. For example, if you are planning to use Bluetooth or WebRTC at the writing of this article, you need another solution.

If app size is a priority for you, you may also not be happy with the end result. For a simple todo app, your app size can be as large as 25MB. This is because Expo bundles APIs you not even use. The good side is that above the initial large file size, bundle sizes don’t increase dramatically with additional features.

You also have a limitation on both Android and iOS versions. If you need to support Android below version 5 or iOS below version 10, you need to look elsewhere.

Lastly, as you could see at the end, builds can take more time than usual, because there’s often a queue. If time is a top priority for you, you need to use another solution where you don’t depend on a third party service.

For a more exhaustive list of limitations, I recommend going through the docs of Expo.

What are the alternatives?

So what are the alternatives? Well, you have React Native without Expo. While it requires you to set up additional tools such as Android Studio and additional SDKs, and configure your machine more throughout with environment variables, it does give you more flexibility over building, bundling, and developing process in general.

If you would also like to look into other tools, two common choices are Ionic and NativeScript. Both of them allow you to build mobile application not just with React, but also Angular or Vue.


Summary

Expo is a great way to get your hands dirty in mobile development. It’s easy to use and relatively simple to set up. You can start writing your app in matter of minutes.

Their documentation is in-depth, and if you happen to have a problem, there’s a good chance the community already found a solution for it on GitHub or StackOverflow.

With this simple todo app, you’ve got an introduction to the most common implementation techniques in React Native. As always, there are room for improvements, especially when it comes to performance.

As promised, you can get the full project from my GitHub repository. If you have any questions, don’t hesitate to ask in the comment!

If you would like to try out the app, you can download it from Google Play.

Do you have experience with React Native, Expo, or mobile development in general? Let us know your thoughts in the comments below! Thank you for reading through, happy coding!

How to Set Up Protected Routes in Your React Application

📚 Get access to exclusive content

Want to get access to exclusive content? Support webtips to get access to tips, checklists, cheatsheets, and much more. ☕

Get access Support us
Read more on
🎉 Thank you for subscribing to our newsletter. x