DEV Community

Cover image for Build an Offline-First React Native Mobile App with Expo and Realm
Diego Freniche
Diego Freniche

Posted on • Updated on • Originally published at mongodb.com

Build an Offline-First React Native Mobile App with Expo and Realm

Introduction

Building Mobile Apps that work offline and sync between different devices is not an easy task. You have to write code to detect when you’re offline, save data locally, detect when you’re back online, compare your local copy of data with that in the server, send and receive data, parse JSON, etc.

It’s a time consuming process that’s needed, but that appears over and over in every single mobile app. You end up solving the same problem for each new project you write. And it’s worse if you want to run your app in iOS and Android. This means redoing everything twice, with two completely different code bases, different threading libraries, frameworks, databases, etc.

To help with offline data management and syncing between different devices, running different OSes, we can use MongoDB’s Realm. To create a single code base that works well in both platforms we can use React Native. And the simplest way to create React Native Apps is using Expo.

React Native Apps

The React Native Project, allows you to create iOS and Android apps using React “a best-in-class JavaScript library for building user interfaces”. So if you’re an experienced Web developer who already knows React, using React Native will be the natural next step to create native Mobile Apps.

But even if you’re a native mobile developer with some experience using SwiftUI in iOS or Compose in Android, you’ll find lots of similarities here.

Expo and React Native

Expo is a set of tools built around React Native. Using Expo you can create React Native Apps quickly and easily. For that, we need to install Expo using Node.js package manager npm:

npm install --global expo-cli
Enter fullscreen mode Exit fullscreen mode

This will install expo-cli globally so we can call it from anywhere in our system. In case we need to update Expo we’ll use that very same command. For this tutorial we’ll need the latest version of Expo, that’s been updated to support Realm. You can find all the new features and changes in the Expo SDK 44 announcement blog post.

To ensure you have the latest Expo version run:

expo --version
Enter fullscreen mode Exit fullscreen mode

Should return at least 5.0.1. If not, run again npm install --global expo-cli

Realm & Expo Logos

Prerequisites

Now that we have the latest Expo installed, let’s check out that we have everything we need to develop our application:

  • Xcode 13, including Command Line Tools, if we want to develop an iOS version. We’ll also need a macOS computer running at least macOS 11/Big Sur in order to run Xcode.
  • Android Studio, to develop for Android and at least one Android Emulator ready to test our apps.
  • Any code editor. I’ll be using Visual Studio Code as it has plugins to help with React Native Development, but you can use any other editor.
  • Check that you have the latest version of yarn running npm install -g yarn
  • Make sure you are NOT on the latest version of node, however, or you will see errors about unsupported digital envelope routines. You need the LTS version instead. Get the latest LTS version number from https://nodejs.org/ and then run:
nvm install 16.13.1 # swap for latest LTS version
Enter fullscreen mode Exit fullscreen mode

If you don’t have Xcode or Android Studio, and need to build without installing anything locally you can also try Expo Application Services, a cloud-based building service that allows you to build your Expo Apps remotely.

MongoDB Atlas and Realm App

Our App will store data in a cloud-backed MongoDB Atlas cluster. So we need to create a free MongoDB account and set up a cluster. For this tutorial, a Free-forever, M0 cluster will be enough.

Once we have our cluster created we can go ahead and create a Realm App. The Realm App will sync our data from mobile into a MongoDB Atlas database, although it has many other uses: manages authentication, can run serverless functions, host static sites, etc. Just follow this quick tutorial (select the React Native template) but don’t download any code, as we’re going to use Expo to create our app from scratch. That will configure our Realm App correctly to use Sync and set it into Development Mode.

Read It Later - Maybe

Now we can go ahead and create our app, a small “read it later” kind of app to store web links we save for later reading. As sometimes we never get back to those links I’ll call it Read It Later - Maybe.

You can always clone the repo and follow along.

Login Adding a Link
Login/Signup screen with email and password fields Adding a Link, with both Name and URL filled up, waiting to tap on “Add Link!” button
All Links Deleting a Link
The App showing a list of two links. Swiping Right to Left we can show a button to delete a Link

Install Expo and create the App

We’ll use Expo to create our app using expo init read-later-maybe. This will ask us which template we want to use for our app. Using up and down cursors we can select the desired template, in this case, from the Managed Workflows we will choose the blank one, that uses JavaScript. This will create a read-later-maybe directory for us containing all the files we need to get started.

Terminal window showing how after launching expo init we choose a template and the messages Expo show until our project is ready.

To start our app, just enter that directory and start the React Native Metro Server using yarn start. This will tell Expo to install any dependencies and start the Metro Server.

cd read-later-maybe
yarn start
Enter fullscreen mode Exit fullscreen mode

This will open our default browser, with the Expo Developer Tools at http://localhost:19002/. If your browser doesn't automatically open, press d to open Developer Tools in the browser. From this web page we can:

  • Start our app in the iOS Simulator
  • Start our app in the Android Emulator
  • Run it in a Web browser (if our app is designed to do that)
  • Change the connection method to the Developer Tools Server
  • Get a link to our app. (More on this later when we talk about Expo Go)

We can also do the same using the developer menu that’s opened in the console, so it’s up to you to use the browser and your mouse or your Terminal and the keyboard.

Running in a Terminal we can see all the options Expo Developer Tools are showing us.

Running our iOS App

To start the iOS App in the Simulator, we can either click “Start our app in the iOS Simulator” on Expo Developer Tools or type i in the console, as starting expo leaves us with the same interface we have in the browser, replicated in the console. We can also directly run the iOS app in Simulator by typing yarn ios if we don’t want to open the development server.

Expo Go

The first time we run our app Expo will install Expo Go. This is a native application (both for iOS and Android) that will take our JavaScript and other resources bundled by Metro and run it in our devices (real or simulated/emulated). Once run in Expo Go, we can make changes to our JavaScript code and Expo will take care of updating our app on the fly, no reload needed.

Open Expo Go 1st time Expo Go greeting Debug menu
before running inside the iOS Simulator, we get a confirmation “Open in Expo Go? 1st time we open the app in Expo, we get a welcome message “Hello there, friend” debug Expo Go menu inside our app has many useful options and can be opened later

Expo Go apps have a nice debugging menu that can be opened pressing “m” in the Expo Developer console.

Structure of our App

Now our app is working, but it only shows a simple message: “Open up App.js to start working on your app!”. So we’ll open the app using our code editor. These are the main files and folders we have so far:

.
├── .expo-shared
│   └── assets.json
├── assets
│   ├── adaptive-icon.png
│   ├── favicon.png
│   ├── icon.png
│   └── splash.png
├── .gitignore
├── App.js
├── app.json
├── babel.config.js
├── package.json
└── yarn.lock
Enter fullscreen mode Exit fullscreen mode

The main three files here are:

  • package.json, where we can check / add / delete our app’s dependencies
  • app.json: configuration file for our app
  • App.js: the starting point for our JavaScript code

These changes can be found in tag step-0 of the repo.

Let’s add some navigation

Our App will have a Login / Register Screen and then will show the list of Links for that particular User. We’ll navigate from the Login Screen to the list of Links and when we decide to Log Out our app we’ll navigate back to the Login / Register Screen. So first we need to add the React Native Navigation Libraries, and the gesture handler (for swipe & touch detection, etc). Enter the following commands in the Terminal:

expo install @react-navigation/native
expo install @react-navigation/stack
expo install  react-native-gesture-handler
expo install  react-native-safe-area-context
expo install react-native-elements
Enter fullscreen mode Exit fullscreen mode

These changes can be found in tag step-1 of the repo.

Now, we’ll create a mostly empty LoginView in views/LoginView.js (the views directory does not exist yet, we need to create it first) containing:

import React from "react";
import { View, Text, TextInput, Button, Alert } from "react-native";

export function LoginView({ navigation }) {
  return (
    <View>
      <Text>Sign Up or Sign In:</Text>
      <View>
        <TextInput          
          placeholder="email"
          autoCapitalize="none"
        />
      </View>
      <View>
        <TextInput
          placeholder="password"
          secureTextEntry
        />
      </View>
      <Button title="Sign In" />
      <Button title="Sign Up" />
    </View>
  );
}
Enter fullscreen mode Exit fullscreen mode

This is just the placeholder for our Login screen. We open it from App.js. Change the App function to:

export default function App() {
  return (
    <NavigationContainer>
      <Stack.Navigator>
        <Stack.Screen
            name="Login View"
            component={LoginView}
            options={{ title: "Read it Later - Maybe" }}
          />
      </Stack.Navigator>
    </NavigationContainer>
  );
}
Enter fullscreen mode Exit fullscreen mode

And add required imports to the top of the file, below the existing import lines.

import { NavigationContainer } from "@react-navigation/native";
import { createStackNavigator } from "@react-navigation/stack";
import { LoginView } from './views/LoginView';
const Stack = createStackNavigator();
Enter fullscreen mode Exit fullscreen mode

All these changes can be found in tag step-2 of the repo.

Adding the Realm Library

Installing Realm

To add our Realm library to the project we’ll type in the Terminal:

expo install realm
Enter fullscreen mode Exit fullscreen mode

This will add Realm as a dependency in our React Native Project. Now we can also create a file that will hold the Realm initialization code, we’ll call it RealmApp.js and place it in the root of the directory, alongside App.js.

import Realm from "realm";
const app = new Realm.App({id: "your-realm-app-id-here"});
export default app;
Enter fullscreen mode Exit fullscreen mode

We need to add a Realm App ID to our code. Here are instructions on how to do so. In short, a Mobile Realm-powered App will use a local database to save changes and will connect to a MongoDB Atlas Database using a Realm App that we create in the cloud. We have Realm as a library in our Mobile App, doing all the heavy lifting (sync, offline, etc.) for our React Native app, and a Realm App in the cloud that connects to MongoDB Atlas, acting as our backend. This way, if we go offline we’ll be using our local database on device and when online, all changes will propagate in both directions.

All these changes can be found in tag step-3 of the repo.

Update 24 January 2022

A simpler way to create a React Native App that uses Expo & Realm is just to create it using a template.
For JavaScript based apps:
npx expo-cli init ReactRealmJsTemplateApp -t @realm/expo-template-js

For TypeScript based apps:
npx create-react-native-app ReactRealmTsTemplateApp -t with-realm

Auth Provider

All Realm related code to register a new user, log in and log out is inside a Provider. This way we can provide all descendants of this Provider with a context that will hold a logged in user. All this code is in providers/AuthProvider.js. You’ll need to create the providers folder and then add AuthProvider.js to it.

Realm not only stores data offline, syncs across multiple devices and stores all your data in a MongoDB Atlas Database, but can also run Serverless Functions, host static html sites or authenticate using multiple providers. In this case we’ll use the simpler email/password authentication.

We create the context with:

const AuthContext = React.createContext(null);
Enter fullscreen mode Exit fullscreen mode

The SignIn code is asynchronous:

const signIn = async (email, password) => {
    const creds = Realm.Credentials.emailPassword(email, password);
    const newUser = await app.logIn(creds);
    setUser(newUser);
  };
Enter fullscreen mode Exit fullscreen mode

As is the code to register a new user:

  const signUp = async (email, password) => {
    await app.emailPasswordAuth.registerUser({ email, password });
  };
Enter fullscreen mode Exit fullscreen mode

To log out we simply check if we’re already logged in, in that case call logOut

const signOut = () => {
    if (user == null) {
      console.warn("Not logged in, can't log out!");
      return;
    }
    user.logOut();
    setUser(null);
  };
Enter fullscreen mode Exit fullscreen mode

All these changes can be found in tag step-4 of the repo.

Login / Register code

Take a moment to have a look at the styles we have for the app in the stylesheet.js file, then modify the styles to your heart’s content.

Now, for Login and Logout we’ll add a couple states to our LoginView in views/LoginView.js. We’ll use these to read both email and password from our interface.

Place the following code inside export function LoginView({ navigation }) {:

  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");
Enter fullscreen mode Exit fullscreen mode

Then, we’ll add the UI code for Login and Sign up. Here we use signIn and signUp from our AuthProvider.

  const onPressSignIn = async () => {
    console.log("Trying sign in with user: " + email);
    try {
      await signIn(email, password);
    } catch (error) {
      const errorMessage = `Failed to sign in: ${error.message}`;
      console.error(errorMessage);
      Alert.alert(errorMessage);
    }
  };

  const onPressSignUp = async () => {
    console.log("Trying signup with user: " + email);
    try {
      await signUp(email, password);
      signIn(email, password);
    } catch (error) {
      const errorMessage = `Failed to sign up: ${error.message}`;
      console.error(errorMessage);
      Alert.alert(errorMessage);
    }
  };
Enter fullscreen mode Exit fullscreen mode

All changes can be found in step-5.

Prebuilding our Expo App

On save we’ll find this error:

Error: Missing Realm constructor. Did you run "pod install"? Please see https://realm.io/docs/react-native/latest/#missing-realm-constructor for troubleshooting
Enter fullscreen mode Exit fullscreen mode

Right now, Realm is not compatible with Expo Managed Workflows. In a managed Workflow Expo hides all iOS and Android native details from the JavaScript/React developer so they can concentrate on writing React code. Here, we need to prebuild our App, which will mean that we lose the nice Expo Go App that allows us to load our app using a QR code.

The Expo Team is working hard on improving the compatibility with Realm, as is our React Native SDK team, who are currently working on improving the compatibility with Expo, supporting the Hermes JavaScript Engine and expo-dev-client. Watch this space for all these exciting announcements!

So to run our app in iOS we’ll do:

expo run:ios
Enter fullscreen mode Exit fullscreen mode

We need to provide a Bundle Identifier to our iOS app. In this case we’ll use com.realm.read-later-maybe

This will install all needed JavaScript libraries using yarn, then install all native libraries using CocoaPods, and finally will compile and run our app. To run on Android we’ll do:

expo run:android
Enter fullscreen mode Exit fullscreen mode

Navigation completed

Now we can register and login in our App. Our App.js file now looks like:

export default function App() {
  return (
    <AuthProvider>
      <NavigationContainer>
        <Stack.Navigator>
          <Stack.Screen
              name="Welcome View"
              component={LoginView}
              options={{ title: "Read it Later - Maybe" }}
            />
        </Stack.Navigator>
      </NavigationContainer>
    </AuthProvider>
  );
}
Enter fullscreen mode Exit fullscreen mode

We have an AuthProvider that will provide the user logged in to all descendants. Inside is a Navigation Container with one Screen: Login View. But we need to have two Screens: our “Login View” with the UI to log in/register and “Links Screen”, which will show all our links.

So let’s create our LinksView screen:

import React, { useState, useEffect } from "react";
import { Text } from "react-native";

export function LinksView() {
    return (
        <Text>Links go here</Text>
    );
}
Enter fullscreen mode Exit fullscreen mode

Right now only shows a simple message “Links go here”, as you can check in step-6

Log out

We can register and log in, but we also need to log out of our app. To do so, we’ll add a Nav Bar item to our Links Screen, so instead of having “Back” we’ll have a logout button that closes our Realm, calls logout and pops out our Screen from the navigation, so we go back to the Welcome Screen.

In our LinksView Screen in we’ll add:

React.useLayoutEffect(() => {
    navigation.setOptions({
        headerBackTitle: "Log out",
        headerLeft: () => <Logout closeRealm={closeRealm} />
    });
  }, [navigation]); 
Enter fullscreen mode Exit fullscreen mode

Here we use a components/Logout component that has a button. This button will call signOut from our AuthProvider. You’ll need to add the components folder.

   return (
    <Button
      title="Log Out"
      onPress={() => {
        Alert.alert("Log Out", null, [
          {
            text: "Yes, Log Out",
            style: "destructive",
            onPress: () => {
              navigation.popToTop();
              closeRealm();
              signOut();
            },
          },
          { text: "Cancel", style: "cancel" },
        ]);
      }}
    />
  );
Enter fullscreen mode Exit fullscreen mode

Nice! Now we have Login, Logout and Register! You can follow along in step-7.

Links

CRUD

We want to store Links to read later. So we’ll start by defining how our Link class will look like. We’ll store a Name and a URL for each link. Also, we need an id and a partition field to avoid pulling all Links for all users. Instead we’ll just sync Links for the logged in user. These changes are in schemas.js

class Link {
  constructor({
    name,
    url,
    partition,
    id = new ObjectId(),
  }) {

    this._partition = partition;
    this._id = id;
    this.name = name;
    this.url = url;
  }

  static schema = {
    name: 'Link',
    properties: {
      _id: 'objectId',
      _partition: 'string',
      name: 'string',
      url: 'string',
    },

    primaryKey: '_id',
  };
}
Enter fullscreen mode Exit fullscreen mode

You can get these changes in step-8 of the repo.

And now, we need to code all the CRUD methods. For that, we’ll go ahead and create a LinksProvider that will fetch Links and delete them. But first, we need to open a Realm to read the Links for this particular user:

  realm.open(config).then((realm) => {
      realmRef.current = realm;
      const syncLinks = realm.objects("Link");
      let sortedLinks = syncLinks.sorted("name");
      setLinks([...sortedLinks]);

      // we observe changes on the Links, in case Sync informs us of changes
      // started in other devices (or the cloud)
      sortedLinks.addListener(() => {
        console.log("Got new data!");
        setLinks([...sortedLinks]);
      });
    });
Enter fullscreen mode Exit fullscreen mode

To add a new Link we’ll have this function that uses [realm.write](https://docs.mongodb.com/realm-sdks/js/latest/Realm.html#write) to add a new Link. This will also be observed by the above listener, triggering a UI refresh.

const createLink = (newLinkName, newLinkURL) => {

    const realm = realmRef.current;

    realm.write(() => {
      // Create a new link in the same partition -- that is, using the same user id.
      realm.create(
        "Link",
        new Link({
          name: newLinkName || "New Link",
          url: newLinkURL || "http://",
          partition: user.id,
        })
      );
    });
  };
Enter fullscreen mode Exit fullscreen mode

Finally to delete Links we’ll use [realm.delete](https://docs.mongodb.com/realm-sdks/js/latest/Realm.html#delete).

  const deleteLink = (link) => {

    const realm = realmRef.current;

    realm.write(() => {
      realm.delete(link);
      // after deleting, we get the Links again and update them
      setLinks([...realm.objects("Link").sorted("name")]);
    });
  };
Enter fullscreen mode Exit fullscreen mode

Showing Links

Our LinksView will map the contents of the links array of Link objects we get from LinkProvider and show a simple List of Views to show name and URL of each Link. We do that using:

{links.map((link, index) =>
    <ScrollView>
        <ListItem.Content>
            <ListItem.Title>
                {link.name}
            </ListItem.Title>
            <ListItem.Subtitle>
                {link.url}
            </ListItem.Subtitle>
        </ListItem.Content>
        <ListItem.Chevron />
    </ScrollView>
Enter fullscreen mode Exit fullscreen mode

UI for deleting Links

As we want to delete links we’ll use a swipe right-to-left gesture to show a button to delete that Link

<ListItem.Swipeable
    onPress={() => onClickLink(link)}
    bottomDivider
    key={index} 
    rightContent={
        <Button
            title="Delete"
            onPress={() => deleteLink(link)}
        />
    }
>
Enter fullscreen mode Exit fullscreen mode

We get deleteLink from the useLinks hook in LinksProvider:

  const { links, createLink, deleteLink } = useLinks();
Enter fullscreen mode Exit fullscreen mode

UI for adding Links

We’ll have a TextInput for entering name and URL, and a button to add a new Link directly at the top of the List of Links. We’ll use an accordion to show/hide this part of the UI:

<ListItem.Accordion
        content={
          <ListItem.Content>
            <ListItem.Title>Create new Link</ListItem.Title>
          </ListItem.Content>
        }
        isExpanded={expanded}
        onPress={() => {
          setExpanded(!expanded);
        }}
      >
      {
        <>
          <TextInput
            style={styles.input}
            onChangeText={setLinkDescription}
            placeholder="Description"
            value={linkDescription}
          />
          <TextInput
            style={styles.input}
            onChangeText={setlinkURL}
            placeholder="URL"
            value={linkURL}
          />
          <Button
                title='Click!'
                color='red'
                onPress={ () => { createLink(linkDescription, linkURL); }}
                />
        </>
      }
      </ListItem.Accordion>
Enter fullscreen mode Exit fullscreen mode

Adding Links in the main App

Finally, we’ll integrate the new LinksView inside our LinksProvider in App.js

<Stack.Screen name="Links">
    {() => {
        return (
            <LinksProvider>
                <LinksView />
            </LinksProvider>
        );
    }} 
</Stack.Screen>
Enter fullscreen mode Exit fullscreen mode

The final App

Wow! That was a lot, but now we have a React Native App, that works with the same code base in both iOS and Android, storing data in a MongoDB Atlas Database in the cloud thanks to Realm Sync. And what’s more, any changes in one device syncs in all other devices with the same user logged-in. But the best part is that Realm Sync works even when offline!

Syncing iOS and Android Offline Syncing!
Animation showing how adding a Link in an iOS Simulator appears in an Android Emulator. After that, deleting on Android makes data disappear also in iOS. Setting Airplane mode in Android and then adding a new Link adds only in Android. When the Android emulator is back online it syncs with iOS.

Recap

In this tutorial we’ve seen how to build a simple React Native application using Expo that takes advantage of Realm Sync for their offline and syncing capabilities. This App is a prebuilt app as right now Managed Expo Workflows won’t work with Realm (yet, read more below). But you still get all the simplicity of use that Expo gives you, all the Expo libraries and the EAS: build your app in the cloud without having to install Xcode or Android Studio.

The Realm SDK team is working hard to make Realm fully compatible with Hermes. Once we release an update to the Realm React Native SDK compatible with Hermes, we’ll publish a new post updating this app. Also, we’re working to finish an Expo Custom Development Client. This will be our own Realm Expo Development Client that will substitute Expo Go while developing with Realm. Expect also a piece of news when that is approved!

All the code for this tutorial can be found in this repo.

Top comments (0)