Navigation

React Native Tutorial

Overview

MongoDB Realm provides a Node SDK that allows you to create a Mobile Application with React Native. This tutorial illustrates the creation of a “Task Tracker” React Native application that allows users to:

  • Sign in to their account with their email and password and sign out later.
  • View, create, modify, and delete tasks.

This tutorial should take around 30 minutes.

Check Out the Quick Start

If you prefer to explore on your own rather than follow a guided tutorial, check out the React Native Quick Start. It includes copyable code examples and the essential information that you need to set up a MongoDB Realm application.

Download the Complete Source Code

We host this tutorial application’s complete and ready-to-compile source code on GitHub. Just follow the instructions in README.md to get started. Don’t forget to update the getRealmApp.js file with your App ID, which you can find in the Realm UI.

Prerequisites

Ensure that you have the following:

  • nodejs version 10.x or later (including Node version 14)
  • If you wish to run on iOS: Xcode version 11.0 or higher, which requires macOS 10.14.4 or higher.
  • If you wish to run on Android: Android Studio.
  • Set up the backend.

Procedure

1

Create a New React Native Project

You can use npx to create your React Native app. Open your terminal and run the following commands:

npx react-native init tasktracker
cd tasktracker
2

Install Dependencies

Within the directory that react-native init generated for you, run the following command to install the realm module:

npm install realm@v10.0.0-beta.6

In this tutorial, we’ll also use the React Native Elements library for our UI. Install with the following command:

npm i react-native-elements react-native-vector-icons

If you are planning to build for iOS, edit the ios/Podfile file and change the first line to set the minimum target iOS version:

platform :ios, '12.0'

In the ios/ directory, complete the dependency installation with CocoaPods:

cd ios
pod install
cd ..
3

Run the App

Now that you have installed the dependencies, it’s a good idea to run the app to make sure everything is working. Run the following command to build and launch the app:

npx react-native run-ios

Note

We’re using iOS here, but you can use run-android to build for Android and run on an Android simulator, if you’d prefer.

After a few minutes of building, a simulator window should appear showing the default React Native app screen. Now we’re ready to start adding React components.

4

Initialize the Realm App Client

In client code, you interact with your Realm app through an app object. We need to access this object from anywhere in the project code, so let’s make it globally accessible.

Create a file in your project directory called getRealmApp.js and paste the following code. Be sure to change <enter your Realm app ID here> to your Realm app ID, which you can find in the Realm UI.

Note

To learn how to find your MongoDB Realm appId, read the Find Your App Id doc.

import Realm from 'realm';

let app;

// Returns the shared instance of the Realm app.
export function getRealmApp() {
  if (app === undefined) {
    const appId = '<enter your Realm app ID here>'; // Set Realm app ID here.
    const appConfig = {
      id: appId,
      timeout: 10000,
      app: {
        name: 'default',
        version: '0',
      },
    };
    app = new Realm.App(appConfig);
  }
  return app;
}

First, we import the Realm module that we installed earlier. Then, we expose a function that initializes the app the first time it is called, making it easy to access the app instance from anywhere in the code.

5

Define the AuthProvider

In this tutorial, we’ll use React Context and hooks to pass realm data and authentication to the view components of the app.

Note

If you aren’t familiar with these patterns, don’t worry. We’ll try to explain as we walk through the example. For more information, please take a moment to refer to the official React documentation on Context and hooks.

Create a new file in your project directory called AuthProvider.js. Here we define a React component that will act as the Provider of a shared context containing all of the actions and data that has to do with authentication. Any child components under the Provider will have access to that context and will re-render whenever that Provider’s state changes.

In AuthProvider.js, begin by importing the relevant modules and the getRealmApp function we just defined:

import React, {useContext, useState} from 'react';
import Realm from 'realm';
import {getRealmApp} from './getRealmApp';

Next, declare some of the module-scoped variables we’ll need to define our provider:

// Access the Realm App.
const app = getRealmApp();

// Create a new Context object that will be provided to descendants of the AuthProvider.
const AuthContext = React.createContext(null);

The next code block defines the AuthProvider itself. Define the AuthProvider as a component that accepts child components in its props. Use the state hook to hold the user object. When this user state variable changes due to login or logout, the child components re-render, as if “notified” of the change.

Define the actions around authentication that child components can use to affect the authentication state. In this case, we define a logIn() function that uses the email/password authentication provider to log in and a logOut() function to log out. Both functions update the user state variable, which triggers a re-render of the child components. We also define a registerUser() method which creates a user, but does not log that user in and thus does not trigger a rerender.

Finally, render the Provider of the AuthContext defined above. By setting its value prop to an object containing all of our state and actions, we make the user object and the logIn(), logOut() and registerUser() functions available to child components.

// The AuthProvider is responsible for user management and provides the
// AuthContext value to its descendants. Components under an AuthProvider can
// use the useAuth() hook to access the auth value.
const AuthProvider = ({children}) => {
  const [user, setUser] = useState(null);

  // The log in function takes an email and password and uses the Email/Password
  // authentication provider to log in.
  const logIn = async (email, password) => {
    console.log(`Logging in as ${email}...`);
    const creds = Realm.Credentials.emailPassword(email, password);
    const newUser = await app.logIn(creds);
    setUser(newUser);
    console.log(`Logged in as ${newUser.identity}`);
  };

  // Log out the current user.
  const logOut = () => {
    if (user == null) {
      console.warn("Not logged in -- can't log out!");
      return;
    }
    console.log('Logging out...');
    user.logOut();
    setUser(null);
  };

  // The register function takes an email and password and uses the emailPassword
  // authentication provider to register the user.
  const registerUser = async (email, password) => {
    console.log(`Registering as ${email}...`);
    await app.auth.emailPassword.registerEmail(email, password);
  };

  return (
    <AuthContext.Provider
      value={{
        logIn,
        logOut,
        registerUser,
        user,
      }}>
      {children}
    </AuthContext.Provider>
  );
};

One way to expose the above context value – that is, our user and authentication actions – to the descendant components is with a hook. Any descendant component can use this hook to access the enclosing AuthProvider’s value.

Note

Because we provided null as the initial value when we created the AuthContext, if the component is not a descendant of an AuthProvider, the context value is null.

We define the useAuth hook as follows, then export the AuthProvider and useAuth hook so we can use them elsewhere in the app:

// The useAuth hook can be used by components under an AuthProvider to access
// the auth context value.
const useAuth = () => {
  const auth = useContext(AuthContext);
  if (auth == null) {
    throw new Error('useAuth() called outside of a AuthProvider?');
  }
  return auth;
};

export {AuthProvider, useAuth};
6

Define the LogInView

We can use the actions of the AuthProvider in the login screen. Create the file LogInView.js and paste the following code that declares a UI with email and password inputs, an error message text box, a blue button that dynamically switches between “Login” or “Register,” depending on the auth mode, and a white button that allows users to toggle between the two modes.

When the user presses the “Haven’t created an account yet? Register” button, the auth mode switches to register. When the user presses the “Register” button in blue, we call registerUser() from the Auth Provider with the email and password text input values and we set the auth mode to login.

If a user has already registered, they can click the “Have an account already? Login” button toggling to the login auth mode. When the user presses the login button in blue, we pass the email and password text input values to the logIn() function from the AuthProvider.

Notice how we use the useAuth hook to access the actions to be provided by the AuthProvider. For instance, when the user presses the login button, we pass the email and password text input values to the logIn function from the AuthProvider.

import React, {useState} from 'react';
import {Button, Text, Input} from 'react-native-elements';
import {useAuth} from './AuthProvider';

// This view has an input for email and password and logs in the user when the
// login button is pressed.
export function LogInView() {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [error, setError] = useState();
  const {logIn, registerUser} = useAuth();
  const [authMode, setAuthMode] = useState('Login');

  return (
    <>
      <Text h3>{authMode}</Text>
      <Input
        autoCapitalize="none"
        placeholder="email"
        onChangeText={setEmail}
      />
      <Input
        secureTextEntry={true}
        placeholder="password"
        onChangeText={setPassword}
      />
      <Button
        onPress={async () => {
          console.log(`${authMode} button pressed with email ${email}`);
          setError(null);
          try {
            if (authMode === 'Login') {
              await logIn(email, password);
            } else {
              await registerUser(email, password);
              setAuthMode('Login');
            }
          } catch (e) {
            setError(`Operation failed: ${e.message}`);
          }
        }}
        title={authMode}
      />
      <Text>{error}</Text>
      <ToggleAuthModeComponent setAuthMode={setAuthMode} authMode={authMode} />
    </>
  );
}

const ToggleAuthModeComponent = ({authMode, setAuthMode}) => {
  if (authMode === 'Login') {
    return (
      <Button
        title="Haven't created an account yet? Register"
        type="outline"
        onPress={async () => {
          setAuthMode('Register');
        }}
      />
    );
  } else {
    return (
      <Button
        title="Have an account already? Login"
        type="outline"
        onPress={async () => {
          setAuthMode('Login');
        }}
      />
    );
  }
};
7

Update the App UI

Now that we have defined the AuthProvider and a view that uses it, let’s tie the two together. Open the App.js file, which react-native init generated for you, and replace its entire contents with the following code block. Note how we can use the useAuth hook within the AppBody component to access the user and actions of its parent AuthProvider.

import React from 'react';
import {SafeAreaView, View, StatusBar} from 'react-native';
import {Button} from 'react-native-elements';
import {useAuth} from './AuthProvider';
import {LogInView} from './LogInView';
import {AuthProvider} from './AuthProvider';

const App = () => {
  return (
    <AuthProvider>
      <AppBody />
    </AuthProvider>
  );
};

// The AppBody is the main view within the App. If a user is not logged in, it
// renders the login view. Otherwise, it renders the tasks view. It must be
// within an AuthProvider.
function AppBody() {
  const {user, logOut} = useAuth();
  return (
    <>
      <StatusBar barStyle="dark-content" />
      <SafeAreaView>
        <View>
          {user == null ? (
            <LogInView />
          ) : (
            <Button onPress={logOut} title="Log Out" />
          )}
        </View>
      </SafeAreaView>
    </>
  );
}

export default App;

It would be a good idea to run the app now and ensure you can register, log in and log out.

If everything is working correctly, your app should look something like this:

8

Define the Object Model Schemas

Now that we can log in with a user account, let’s start working with data. First, we must define the data model. Create a file called schemas.json and paste the following data model code in:

import {ObjectId} from 'bson';

class Task {
  constructor({
    name,
    partition,
    status = Task.STATUS_OPEN,
    id = new ObjectId(),
  }) {
    this._partition = partition;
    this._id = id;
    this.name = name;
    this.status = status;
  }

  static STATUS_OPEN = 'Open';
  static STATUS_IN_PROGRESS = 'InProgress';
  static STATUS_COMPLETE = 'Complete';

  static schema = {
    name: 'Task',
    properties: {
      _id: 'objectId',
      _partition: 'string',
      name: 'string',
      status: 'string',
    },
    primaryKey: '_id',
  };
}

export {Task};

You don’t need to use a class when working with Realm in JavaScript, but we find it convenient. The Task class has a constructor to take care of setting defaults, such as the initial Task status and the _id ObjectId property. It exposes some constants for possible status values and the schema that we will provide to the realm.

We’ll open the realm and use this model in another provider component we’ll define next, the TasksProvider.

9

Implement the TasksProvider

In order to provide the data and actions related to fetching, creating, updating, and deleting Tasks to descendant components, we’ll use the same Context, Provider and Hook pattern we used for the AuthProvider.

In the AuthProvider component, we’ll open a realm, fetch all Tasks in it, and watch that collection of Tasks for changes. We’ll also define the following actions:

  • Create a Task
  • Update a Task’s status
  • Delete a Task

Create a new file called TasksProvider.js. To begin, import everything we will need – including the useAuth hook and Task class we defined earlier – and create a Context object our TasksProvider can use:

import React, {useContext, useState, useEffect, useRef} from 'react';
import Realm from 'realm';
import {useAuth} from './AuthProvider';
import {Task} from './schemas';

// Create the context that will be provided to descendants of TasksProvider via
// the useTasks hook.
const TasksContext = React.createContext(null);

Now, begin defining the TasksProvider component. The component accepts two props: its children, of course, and a projectId to specify which subset of Tasks to work with.

Use the useAuth hook to get the logged-in user and declare the state variable that will contain the list of Tasks. We also use the useRef hook to store a reference to our opened realm. We use useRef rather than useState for this because we don’t care to re-render when the realm itself changes.

const TasksProvider = ({children, projectId}) => {
  // Get the user from the AuthProvider context.
  const {user} = useAuth();

  // The tasks list will contain the tasks in the realm when opened.
  const [tasks, setTasks] = useState([]);

  // This realm does not need to be a state variable, because we don't re-render
  // on changing the realm.
  const realmRef = useRef(null);

Next, still within the definition of TasksProvider, we use the useEffect hook to implement our logic for opening the realm and reading the Tasks. This effect callback runs whenever the user or projectId prop changes, including when the component first mounts.

Within the callback, we define the configuration we’ll use to open the realm. Notice we pass the user object from the authentication provider and use the projectId as the partition value in the sync field of the configuration. When opening a synced realm for a given partition value, MongoDB Realm checks whether the user has permissions to read and/or write for that partition value. If the user is allowed to read with that partition value, the realm contains all Tasks with that partition value. Otherwise, the request to open fails.

Check the Logs Tab in the Realm UI

When working with Realm Sync, remember to check the Logs page in the Realm UI. That page usually contains additional information that can help you troubleshoot any issues you may encounter.

Upon successfully opening the realm, we read all Tasks from the realm and attach a change listener to the collection of Tasks. When the change listener fires – due to a change to the Tasks – we react by updating the tasks list state variable with the latest list of Tasks. This causes a re-render of all the child components, so they are essentially notified of the change to the Tasks.

Note

Replacing the entire list of Tasks whenever there is any change is great for an introduction to MongoDB Realm or when you only have a small number of objects. But you may want to be more selective and nuanced with your updates when scaling your app to deal with larger sets of objects. That’s out of scope for this tutorial.

If the call to Realm.open() fails, we pass any errors to console.warn.

To finish off the effect callback, we return a cleanup function to be called the next time the effect is run or when the TasksComponent unmounts. This simply closes the realm if it is open.

Everything should work at this point, but there’s a slight issue. If this cleanup function runs before the request to asynchronously open the realm completes, it creates a race condition where the realm open success handler might try to update invalid state variables. Worse, since the cleanup function already ran, that newly-opened realm won’t get cleaned up, which is a resource leak!

We fix this with a simple canceled flag shared between the open success and cleanup callbacks. We set canceled to true in the cleanup callback. In the open success callback, we check that same flag. If it was set, we close the newly-opened realm and leave the function.

  // The effect hook replaces lifecycle methods such as componentDidMount. In
  // this effect hook, we open the realm that contains the tasks and fetch a
  // collection of tasks.
  useEffect(() => {
    // Check that the user is logged in. You must authenticate to open a synced
    // realm.
    if (user == null) {
      console.warn('TasksView must be authenticated!');
      return;
    }

    // Define the configuration for the realm to use the Task schema. Base the
    // sync configuration on the user settings and use the project ID as the
    // partition value. This will open a realm that contains all objects where
    // object._partition == projectId.
    const config = {
      schema: [Task.schema],
      sync: {
        user,
        partitionValue: projectId,
      },
    };

    console.log(
      `Attempting to open Realm ${projectId} for user ${
        user.identity
      } with config: ${JSON.stringify(config)}...`,
    );

    // Set this flag to true if the cleanup handler runs before the realm open
    // success handler, e.g. because the component unmounted.
    let canceled = false;

    // Now open the realm asynchronously with the given configuration.
    Realm.open(config)
      .then((openedRealm) => {
        // If this request has been canceled, we should close the realm.
        if (canceled) {
          openedRealm.close();
          return;
        }

        // Update the realmRef so we can use this opened realm for writing.
        realmRef.current = openedRealm;

        // Read the collection of all Tasks in the realm. Again, thanks to our
        // configuration above, the realm only contains tasks where
        // task._partition == projectId.
        const syncTasks = openedRealm.objects('Task');

        // Watch for changes to the tasks collection.
        openedRealm.addListener('change', () => {
          // On change, update the tasks state variable and re-render.
          setTasks([...syncTasks]);
        });

        // Set the tasks state variable and re-render.
        setTasks([...syncTasks]);
      })
      .catch((error) => console.warn('Failed to open realm:', error));

    // Return the cleanup function to be called when the component is unmounted.
    return () => {
      // Update the canceled flag shared between both this callback and the
      // realm open success callback above in case this runs first.
      canceled = true;

      // If there is an open realm, we must close it.
      const realm = realmRef.current;
      if (realm != null) {
        realm.removeAllListeners();
        realm.close();
        realmRef.current = null;
      }
    };
  }, [user, projectId]); // Declare dependencies list in the second parameter to useEffect().

Continuing in the TasksProvider function definition, let’s define the create, update, and delete actions. Each function opens a write transaction on the realm in order to modify its data. Again, we use the Task class we defined earlier.

  // Define our create, update, and delete functions that users of the
  // useTasks() hook can call.
  const createTask = (newTaskName) => {
    const realm = realmRef.current;

    // Open a write transaction.
    realm.write(() => {
      // Create a new task in the same partition -- that is, in the same project.
      realm.create(
        'Task',
        new Task({name: newTaskName || 'New Task', partition: projectId}),
      );
    });
  };

  // Define the function for updating a task's status.
  const setTaskStatus = (task, status) => {
    // One advantage of centralizing the realm functionality in this provider is
    // that we can check to make sure a valid status was passed in here.
    if (
      ![
        Task.STATUS_OPEN,
        Task.STATUS_IN_PROGRESS,
        Task.STATUS_COMPLETE,
      ].includes(status)
    ) {
      throw new Error(`Invalid Status ${status}`);
    }
    const realm = realmRef.current;

    realm.write(() => {
      task.status = status;
    });
  };

  // Define the function for deleting a task.
  const deleteTask = (task) => {
    const realm = realmRef.current;
    realm.write(() => {
      realm.delete(task);
    });
  };

At the end of the definition for TasksProvider, we provide the actions we just defined and the list of Tasks as the value of the TasksContext, similarly to how we provided the data and actions in the AuthProvider:

  // Render the children within the TaskContext's provider. The value contains
  // everything that should be made available to descendants that use the
  // useTasks hook.
  return (
    <TasksContext.Provider
      value={{
        createTask,
        deleteTask,
        setTaskStatus,
        tasks,
        projectId,
      }}>
      {children}
    </TasksContext.Provider>
  );
};

Finally, define the useTasks hook that provides descendants of a TasksProvider access to those Tasks and related actions, and export both the TasksProvider and the useTasks hook so we can use them elsewhere in the app:

// The useTasks hook can be used by any descendant of the TasksProvider. It
// provides the tasks of the TasksProvider's project and various functions to
// create, update, and delete the tasks in that project.
const useTasks = () => {
  const value = useContext(TasksContext);
  if (value == null) {
    throw new Error('useTasks() called outside of a TasksProvider?');
  }
  return value;
};

export {TasksProvider, useTasks};
10

Implement the TaskItem

We just need to a UI to display the Tasks and allow the user to use the actions we defined in the TasksProvider.

To represent Tasks in a list, let’s define the TaskItem component. When rendered as a descendant of the TasksProvider we just defined, it can use the useTasks hook to access the Task-related actions. We’ll present buttons to execute these actions in a generic helper component called ActionSheet. Once again, we’ll base some of these views on the React Native Elements toolkit. In the finished app, it will look something like this:

Create a new file called TaskItem.js and paste the following code. Notice everywhere that we use the actions from the TasksProvider:

import React, {useState} from 'react';
import {Text, ListItem, Overlay} from 'react-native-elements';
import {Task} from './schemas';
import {useTasks} from './TasksProvider';

// Action sheet contains a list of actions. Each action should have a `title`
// string and `action` function property. A "Cancel" action is automatically
// added to the end of your list of actions. You must also provide the
// closeOverlay function that this component will call to request that the
// action sheet be closed.
function ActionSheet({actions, visible, closeOverlay}) {
  const cancelAction = {
    title: 'Cancel',
    action: closeOverlay,
  };
  return (
    <Overlay
      overlayStyle={{width: '90%'}}
      isVisible={visible}
      onBackdropPress={closeOverlay}>
      <>
        {[...actions, cancelAction].map(({title, action}) => (
          <ListItem
            key={title}
            title={title}
            onPress={() => {
              closeOverlay();
              action();
            }}
          />
        ))}
      </>
    </Overlay>
  );
}

// The TaskItem represents a Task in a list. When you click an item in the list,
// an action sheet appears. The action sheet contains a list of actions the user
// can perform on the task, namely deleting and changing its status.
export function TaskItem({task}) {
  // Pull the task actions from the TasksProvider.
  const {deleteTask, setTaskStatus} = useTasks();

  // The action sheet appears when the user presses an item in the list.
  const [actionSheetVisible, setActionSheetVisible] = useState(false);

  // Specify the list of available actions in the action list when the user
  // presses an item in the list.
  const actions = [
    {
      title: 'Delete',
      action: () => {
        deleteTask(task);
      },
    },
  ];

  // For each possible status other than the current status, make an action to
  // move the task into that status. Rather than creating a generic method to
  // avoid repetition, we split each status to separate each case in the code
  // below for demonstration purposes.
  if (task.status !== Task.STATUS_OPEN) {
    actions.push({
      title: 'Mark Open',
      action: () => {
        setTaskStatus(task, Task.STATUS_OPEN);
      },
    });
  }
  if (task.status !== Task.STATUS_IN_PROGRESS) {
    actions.push({
      title: 'Mark In Progress',
      action: () => {
        setTaskStatus(task, Task.STATUS_IN_PROGRESS);
      },
    });
  }
  if (task.status !== Task.STATUS_COMPLETE) {
    actions.push({
      title: 'Mark Complete',
      action: () => {
        setTaskStatus(task, Task.STATUS_COMPLETE);
      },
    });
  }

  return (
    <>
      <ActionSheet
        visible={actionSheetVisible}
        closeOverlay={() => setActionSheetVisible(false)}
        actions={actions}
      />
      <ListItem
        key={task.id}
        onPress={() => {
          setActionSheetVisible(true);
        }}
        title={task.name}
        bottomDivider
        checkmark={
          task.status === Task.STATUS_COMPLETE ? (
            <Text>&#10004; {/* checkmark */}</Text>
          ) : task.status === Task.STATUS_IN_PROGRESS ? (
            <Text>In Progress</Text>
          ) : null
        }
      />
    </>
  );
}
11

Implement the AddTaskView

To add a Task, the user can press a button and enter the new Task name into a prompt. Let’s define this functionality in a new component called AddTaskView.

Create a new file called AddTaskView.js and paste the following code in. Notice where we use the createTask action from the TasksProvider we defined earlier:

import React, {useState} from 'react';
import {Overlay, Input, Button} from 'react-native-elements';
import {useTasks} from './TasksProvider';

// The AddTaskView is a button for adding tasks. When the button is pressed, an
// overlay shows up to request user input for the new task name. When the
// "Create" button on the overlay is pressed, the overlay closes and the new
// task is created in the realm.
export function AddTaskView() {
  const [overlayVisible, setOverlayVisible] = useState(false);
  const [newTaskName, setNewTaskName] = useState('');
  const {createTask} = useTasks();
  return (
    <>
      <Overlay
        isVisible={overlayVisible}
        overlayStyle={{width: '90%'}}
        onBackdropPress={() => setOverlayVisible(false)}>
        <>
          <Input
            placeholder="New Task Name"
            onChangeText={(text) => setNewTaskName(text)}
            autoFocus={true}
          />
          <Button
            title="Create"
            onPress={() => {
              setOverlayVisible(false);
              createTask(newTaskName);
            }}
          />
        </>
      </Overlay>
      <Button
        type="outline"
        title="Add Task"
        onPress={() => {
          setOverlayVisible(true);
        }}
      />
    </>
  );
}
12

Implement the TasksView

We present the Task items in a list and expose the Add Task functionality a new component called TasksView.

Create the file TasksView.js and paste the following code in. We use the useTasks hook to access the list of tasks and create a TaskItem for each Task in that list. We also place the AddTaskView at the top of the view, next to a logout button.

import React from 'react';
import {View, ScrollView} from 'react-native';
import {Text, Button} from 'react-native-elements';
import {useAuth} from './AuthProvider';
import {useTasks} from './TasksProvider';
import {TaskItem} from './TaskItem';
import {AddTaskView} from './AddTaskView';

// The Tasks View displays the list of tasks of the parent TasksProvider.
// It has a button to log out and a button to add a new task.
export function TasksView() {
  // Get the logOut function from the useAuth hook.
  const {logOut} = useAuth();

  // Get the list of tasks and the projectId from the useTasks hook.
  const {tasks, projectId} = useTasks();

  return (
    <>
      <View style={{flexDirection: 'row', justifyContent: 'space-between'}}>
        <Button type="outline" title="Log Out" onPress={logOut} />
        <AddTaskView />
      </View>
      <Text h2>{projectId}</Text>
      <ScrollView>
        {tasks.map(task => (
          <TaskItem key={`${task._id}`} task={task} />
        ))}
      </ScrollView>
    </>
  );
}
13

Tie It All Together

At last, we can tie the authentication and tasks together. Open App.js again and import the new components by adding the following lines near the top of the file:

import {TasksProvider} from './TasksProvider';
import {TasksView} from './TasksView';

Update the AppBody component to use the TasksProviders and TasksView when a user is logged in. We hardcode the projectId to “My Project” for now. In a future tutorial, we will implement Projects, which will allow us to enable more complex permissions and collaboration features.

function AppBody() {
  const {user} = useAuth();
  return (
    <>
      <StatusBar barStyle="dark-content" />
      <SafeAreaView>
        <View>
          {user == null ? (
            <LogInView />
          ) : (
            <TasksProvider projectId="My Project">
              <TasksView />
            </TasksProvider>
          )}
        </View>
      </SafeAreaView>
    </>
  );
}

Here’s how this should look:

14

Run the App

Run your app and verify that you can log in and view, create, update, and delete Tasks.

Run the app in another simulator and edit Tasks simultaneously to see Realm Sync in action! When you make a change on one simulator, it should automatically update on the other. For example, try adding a Task, changing its status, and deleting a Task on one simulator. If everything is working correctly, the app on the other simulator will automatically reflect the changes you make in real time.

Congratulations! You have completed the first realm React Native tutorial. How did it go? Please leave feedback using the feedback widget on the bottom right of the page.

What’s Next?