Tutorial: Atlas Device Sync for React Native
On this page
- Prerequisites
- Start with the Template App
- Set Up the Template App
- Install Dependencies
- Build the App
- Test the App
- Get Familiar With the Template App
- Atlas App Services App
- React Native App
- Add a Priority Level Field
- Define the Priority Levels
- Update the
Item
Data Model - Add a Priority Picker
- Run and Test the App
- Update the Sync Subscription
- Add a Mode Toggle to the UI
- Update the Sync Subscription
- Test the App
- What's Next?
Estimated time to complete: 30 minutes, depending on your experience with React Native
You can use the Realm React Native SDK and @realm/react to build a mobile application with React Native. This tutorial walks you through how to build your own app that uses Flexible Sync.
For this tutorial, we'll start with a pre-built TypeScript template application to see how everything fits together.
The app is a pre-built template that includes a working React Native application (frontend) and its corresponding App Services App configuration files (backend).
The template app is a basic to-do list application that lets users do various things to manage their tasks:
Create email/password accounts and log in and out of the app.
Create, read, update, and delete their own tasks.
View all tasks, even if the user is not the owner.
After you've got the template app running, you will add a new priority
field to the existing Item
model and update the Flexible Sync
subscription to only show items within a range of priorities. This example
illustrates how you might adapt the template app for your own needs.
You would not necessarily make this change given the current structure of
the template app.
The template app provides a toggle that simulates the device being in an offline mode. This toggle lets you quickly test Device Sync functionality, emulating the user having no internet connection. However, you would likely remove this toggle in a production application.
Note
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 React Native app with Atlas Device Sync.
Prerequisites
You must set up your local environment for React Native development before you can begin this tutorial. For detailed instructions, see Setting up the development environment in the React Native docs.
This tutorial starts with a Template App. You need an Atlas Account, an API key, and App Services CLI to create a Template App.
You can learn more about creating an Atlas account in the Atlas Getting Started documentation. For this tutorial, you need an Atlas account with a free-tier cluster.
You also need an Atlas API key for the MongoDB Cloud account you wish to log in with. You must be a Project Owner to create a Template App using App Services CLI.
To learn more about installing App Services CLI, see Install App Services CLI. After installing, run the login command using the API key for your Atlas project.
Start with the Template App
This tutorial is based on the React Native SDK Flexible Sync Template App named
react-native.todo.flex
. We start with the default app and build new features
on it.
To learn more about the Template Apps, see Template Apps.
If you don't already have an Atlas account, sign-up to deploy a Template App.
Follow the procedure described in the Create an App Services App guide, and select Create App from Template. Select the Real-time Sync template. This creates an App Services App pre-configured to use with one of the Device Sync template app clients.
After you create a template app, the UI displays a modal labeled
Get the Front-end Code for your Template. This modal
provides instructions for downloading the template app client code
as a .zip
file or using App Services CLI to get the client.
After selecting the .zip
or App Services CLI method, follow the on-screen
instructions to get the client code. For this tutorial, select the
JavaScript (React Native) client code.
Note
The default Windows ZIP utility may show the .zip file as empty. If you encounter this, use one of the third-party zip programs that are available.
The appservices apps create command sets up the backend and creates a React Native template app for you to use as a base for this tutorial.
Run the following command in a terminal window to create an app
named "MyTutorialApp" that is deployed in the US-VA
region
with its environment set to "development" (instead of production
or QA).
appservices app create \ --name MyTutorialApp \ --template react-native.todo.flex \ --deployment-model global \ --environment development
The command creates a new directory in your current path with the
same name as the value of the --name
flag.
You can fork and clone a GitHub repository that contains the Device Sync client code. The React Native client code is available at https://github.com/mongodb/template-app-react-native-todo.
If you use this process to get the client code, you must create a template app to use with the client. Follow the instructions at Create a Template App to use the Atlas App Services UI, App Services CLI, or Admin API to create a Device Sync template app.
Set Up the Template App
Use the following steps to get the template app up and running on your computer:
Install Dependencies
In your terminal, go to the directory that contains the client code. If you
created the app with the App Services CLI, go to
MyTutorialApp/react-native.todo.flex
.
Otherwise, go to the root of your downloaded or cloned project. Then
run following commands to navigate to install the app's dependencies:
npm install
To build and run the app on an iOS device or simulator, install the additional iOS dependencies with CocoaPods.
cd ios npx pod-install
Build the App
At this point, you should have a fully functional React Native app that can run on iOS, Android, or both.
If you encounter an error or otherwise have issues, make sure that your React Native environment is set up correctly. Refer to the official React Native development environment setup guide. Follow all the steps for your Development OS and Target OS.
To make sure that everything works on iOS, build the app and run it in an iOS simulator:
npm run ios
To make sure that everything works on Android:
Start an Android emulator. See Run apps on the Android Emulator for details on how to do this.
Build the app on the emulator:
npm run android One common error is
Error: spawn ./gradlew EACCES
. This means the project file permissions aren't what they need to be. On MacOS, you can fix this by enteringchmod 755 android/gradlew
in your terminal.
Test the App
When the build completes, you should have a functional app running on your simulator. In the app, register a new account and test the features:
Add a few to-do items to the list.
Press the checkbox on one or two items to mark them as complete.
Press the X on an item to delete it from the list.
Toggle internet connectivity in the app to simulate offline mode.
If you connect to your Atlas Cluster and query the
todo.Item
collection you can see your App's data. As long as the React Native
app is not in offline mode, new data and changes in the app automatically
sync to the todo.Item
collection.
Tip
To learn how to connect to your Atlas Cluster, see Connect to a Cluster.
Similarly, any changes in the collection automatically sync down to the React Native app. Try changing an item's completion status in your cluster - the React Native app will automatically update with the new value whenever a network connection is available.
Tip
To learn more about updating data in your cluster, see Update Documents.
Get Familiar With the Template App
Now that you have the template app running let's dive into the code to see what we're working with.
Atlas App Services App
The template app includes a fully configured App Services App in the
backend
directory. It has a unique appId
value in
atlasConfig.json
that client applications use to connect.
It also includes the following pre-defined configurations:
A data source linked to your Atlas Cluster.
A data model for the
todo.Item
collection that matches theItem
class in the React Native app.An authentication provider that lets users register for and log in to your app with an email and password.
A flexible sync configuration with a single session role that allows users to read and write their own items and view other users' items.
React Native App
The React Native app is a fully-configured mobile client that can run on iOS and Android devices.
The app uses the @realm/react library. The library includes React hooks and components that streamline working with your Atlas backend and Realm database.
The app contains some configuration files and directories, but you can
ignore those unless you want to customize the app. For this tutorial,
you should be familiar with the React components in the source/
directory:
File Name | Description |
---|---|
ItemSchema.tsx | The Item class, including its object data model. We import this
class in AppWrapper.tsx to include it in the app's overall Realm
schema. |
AppWrapper.tsx | This is the root component for the app. It functions as a wrapper
component and contains all of the @realm/react providers. This is
where you configure your realm and your connection to your Atlas
backend. |
App.tsx | Most of the app's functionality is contained in this component and its
children. Because the @realm/react providers are wrapped around this
component, it can access an instance of your Atlas backend, user objects,
and interact with the Realm database. |
WelcomeView.tsx | The user registration and login form that users see when they
first open the app. |
ItemListView.tsx | The main to-do list app that users interact with after they log
in. It queries for Item Realm objects and displays them
in a list. It also includes the code to create new Item
objects and store them in Realm. |
CreateToDoPrompt.tsx | A UI form that lets us enter data for new Item objects. The
code that actually creates new objects is in ItemListView.tsx . |
LogoutButton.tsx | A reusable button that logs out an authenticated user. |
OfflineModeButton.tsx | A reusable button that simulates an offline mode by pausing and resuming
the current Realm syncSession . |
Add a Priority Level Field
Now that you're more familiar with what's already provided in the template app, let's write some code to implement a new feature.
For this tutorial, we'll add a new priority
property to the
Item
objects. This will let us organize to-dos by how important they
are and allow us to focus only on the most important ones.
Define the Priority Levels
We want to allow a small number of named priority levels, and we
want to easily be able sort the levels. To do this, we'll use a
helper function to define an enum
object that maps a set of
ordered level names to and from an integer that represents their
priority.
Add the following code directly under the import statements in
source/ItemSchema.tsx
:
function createEnum(arr) { arr.forEach((p, i) => arr[p] = i); return arr; } // Priority.High === 1 // Priority[Priority.High] === "High" export const Priority = createEnum([ "Severe", "High", "Medium", "Low", ])
The priority levels in the enum
are ordered from most important
to least. The corresponding index value for each level increases
from the most important, Priority[0]
, to the least important,
Priority[3]
. This means that a higher priority level (meaning more important)
has a lower index value.
Update the Item
Data Model
Now we have an enum
that defines the possible values of the
priority
field. However, we still have to define the
priority
field in the Item
class.
Add the following lines to your code in source/ItemSchema.tsx
to
add priority
to the Item
data model:
export class Item extends Realm.Object<Item> { _id!: BSON.ObjectId; isComplete!: boolean; summary!: string; owner_id!: string; priority!: string; static schema: Realm.ObjectSchema = { name: 'Item', primaryKey: '_id', properties: { // This allows us to automatically generate a unique _id for each Item _id: {type: 'objectId', default: () => new BSON.ObjectId()}, // All todo items will default to incomplete isComplete: {type: 'bool', default: false}, summary: 'string', owner_id: 'string', priority: { // Store the index value of the Priority enum rather than the name type: 'int', default: Priority.High }, }, }; }
Note
Why Didn't This Break Sync
At this point, your React Native Item
model and its
corresponding schema in your App Services App no longer agree. That's
okay!
Adding a property to a Realm object is not a breaking change and therefore does not require a client reset. The template app has Development Mode enabled, so changes to the client Realm object are reflected in the server-side schema. For more information, see Development Mode and Update Your Data Model.
Add a Priority Picker
Your app's data model now includes a priority
for each Item
object. Let's update the app UI so that you can choose a priority
value when you add a new to-do to the list.
First, we'll install an external library to implement the priority picker component. Run the following in your terminal inside of your project root:
npm install @react-native-picker/picker
If you're building for iOS, make sure to link the associated Cocoapods after you've installed the package:
npx pod-install
Tip
You may need to rebuild your app after installing. To do so, stop the bundler for your project and then run the build command:
npm run ios
npm run android
Now that the package is fully installed, let's update the new to-do creation prompt component to use the picker.
Add the following imports to the top of source/CreateToDoPrompt.tsx
:
import {Picker} from '@react-native-picker/picker'; import {Priority} from './ItemSchema';
Then, modify the CreateToDoPrompt
component:
Add
priority
to theonSubmit()
props definitionKeep track of
priority
in a state hookConnect the state to the
Picker
component that you importedPass
priority
to theonSubmit()
handler
type Props = { onSubmit(args: {summary: string; priority: string;}): void; }; export function CreateToDoPrompt(props: Props): React.ReactElement<Props> { const {onSubmit} = props; const [summary, setSummary] = useState(''); const [priority, setPriority] = useState(Priority.High); return ( <View style={styles.modalWrapper}> <Text h4 style={styles.addItemTitle}> Add To-Do Item </Text> <Input placeholder="What do you want to do?" onChangeText={(text: string) => setSummary(text)} autoCompleteType={undefined} /> <Picker style={{width: '80%'}} selectedValue={priority} onValueChange={value => setPriority(value)}> {Priority.map(priority => ( <Picker.Item key={priority} label={priority} value={Priority[priority]} /> ))} </Picker> <Button title="Save" buttonStyle={styles.saveButton} onPress={() => onSubmit({summary, priority})} /> </View> ); }
In source/ItemListView.tsx
, modify the createItem()
function
to accept and use priority
:
const createItem = useCallback( ({summary, priority}: {summary: string, priority: string}) => { realm.write(() => { return new Item(realm, { summary, owner_id: user?.id, priority }); }); }, [realm, user], );
Then, modify the create to-do submission handler to accept the
priority
level and pass it to createItem()
:
<CreateToDoPrompt onSubmit={({summary, priority}) => { setShowNewItemOverlay(false); createItem({summary, priority}); }} />
Finally, modify the list item template to render the to-do's priority
before the summary
:
<ListItem key={`${item._id}`} bottomDivider topDivider hasTVPreferredFocus={undefined} tvParallaxProperties={undefined}> <Text>{item.priority}</Text> <ListItem.Title style={styles.itemTitle}> {item.summary} </ListItem.Title> <ListItem.Subtitle style={styles.itemSubtitle}> {item.owner_id === user?.id ? '(mine)' : ''} </ListItem.Subtitle> <ListItem.CheckBox checked={item.isComplete} checkedColor={COLORS.primary} iconType="material" checkedIcon="check-box" uncheckedIcon="check-box-outline-blank" onPress={() => toggleItemIsComplete(item._id)} /> <Button type="clear" onPress={() => deleteItem(item._id)} icon={ <Icon type="material" name="clear" size={12} color="#979797" tvParallaxProperties={undefined} /> } /> </ListItem>
Update the Sync Subscription
The Device Sync protocol uses a flexible model where each sync client uses a standard RQL query to choose a subset of application data and then subscribes to the subset. This automatically pulls the latest version of all data in the subset to the device and syncs changes to the data between devices.
For example, the template app you're using has the following built-in subscription to items that the current user owns:
realm.subscriptions.update(mutableSubs => { mutableSubs.removeByName(itemSubscriptionName); mutableSubs.add( realm.objects(Item).filtered(`owner_id == "${user?.id}"`), {name: ownItemsSubscriptionName}, ); });
You can customize the subscription during runtime to sync only the data that your app needs. Let's add a feature to demonstrate how.
For this tutorial, we'll add a button that lets us toggle between two
modes: one where the app syncs all to-do items and another where it
only syncs important ones with a priority
of High or Severe.
Add a Mode Toggle to the UI
First, add a useState()
hook to the ItemListView
component
to keep track of the current mode:
const [showImportantOnly, setShowImportantOnly] = useState(false);
Then, add a new button that toggles the mode to the bottom of the
to-do list, after <ListItem>
:
<Button title={showImportantOnly ? 'Show All' : 'Show Important Only'} buttonStyle={{ ...styles.addToDoButton, backgroundColor: showImportantOnly ? '#00A35C' : '#FFC010', }} onPress={() => setShowImportantOnly(showImportantOnly => !showImportantOnly)} />
Update the Sync Subscription
At this point, the app can switch modes in the UI, but we haven't done anything else so the modes are functionally identical. Let's update the sync subscription to only sync data relevant to the current mode.
In the first useEffect
of the ItemListView
component,
add code that checks the current mode and appends an additional
priority
filter to the query if the showImportantOnly
mode is
active:
useEffect(() => { if (showAllItems) { realm.subscriptions.update(mutableSubs => { mutableSubs.removeByName(ownItemsSubscriptionName); mutableSubs.add(realm.objects(Item), {name: itemSubscriptionName}); }); } else if (showImportantOnly) { realm.subscriptions.update(mutableSubs => { mutableSubs.removeByName(itemSubscriptionName); mutableSubs.add( realm.objects(Item).filtered(`owner_id == "${user?.id}" && priority <= 1`), {name: ownItemsSubscriptionName}, ); }); } else { realm.subscriptions.update(mutableSubs => { mutableSubs.removeByName(itemSubscriptionName); mutableSubs.add( realm.objects(Item).filtered(`owner_id == "${user?.id}"`), {name: ownItemsSubscriptionName}, ); }); } }, [realm, user, showAllItems, showImportantOnly]);
Important
Don't forget to add showImportantOnly
to the list of
dependencies in the second argument of useEffect
.
Test the App
Your app is now set up to modify its sync subscription based on the current mode.
Rebuild and run the app to make sure everything works. You should be able to create, complete, and delete to-do items as well as toggle between viewing all items and only important items.
Tip
Changing Subscriptions with Developer Mode Enabled
In this tutorial, when you change the subscription and query on the priority field for the first time, the field is automatically added to the Device Sync Collection Queryable Fields. This occurs because the template app has Development Mode enabled by default. If Development Mode was not enabled, you would have to manually add the field as a queryable field to use it in a client-side Sync query.
For more information, refer to Queryable Fields.
What's Next?
Read our React Native SDK documentation.
Find developer-oriented blog posts and integration tutorials on the MongoDB Developer Hub.
Join the MongoDB Community forum to learn from other MongoDB developers and technical experts.
Add a feature to the template app by using Triggers and Atlas Search.
Explore engineering and expert-provided example projects.
Note
Share Feedback
How did it go? Use the Rate this page widget at the bottom right of the page to rate its effectiveness. Or file an issue on the GitHub repository if you had any issues.