For a long time now, SQLite has been the go-to database library for mobile and cross-platform developers. It’s lightweight, serverless, and offers an easy learning curve for anyone familiar with SQL. SQLite is written in C and supports more than 30 other programming languages and platforms, including Android and React Native.
In recent years, a new competitor, Realm, has emerged. Realm is a mobile-first database that is object-oriented and intended for use in cross-platform and mobile applications.
In this post, we’ll cover the following topics:
Introduced in 2014, Realm is an open source database platform with real-time, two-way synchronization and Node.js SDK support. The now-legacy product has been downloaded over 2 billion times.
In 2019, Realm was acquired by MongoDB, who maintains it now. MongoDB Realm (or simply Realm, as it is now called) combines the old Realm database and MongoDB Stitch, the serverless platform for MongoDB, into a unified solution to enable easier development on mobile devices.
According to Realm.io, “Thousands of apps are built on Realm — from hyper-growth startups to Fortune 500 companies.”
As of today, Realm is supported by various programming languages, including:
Realm offers cross-platform support and is easy to work with. It’s also faster than SQLite for normal operations and requires less code to perform regular operations than SQLite or Core Data.
Some of the features of Realm include:
Developers can also use Realm to persist data on local disk storage or store it temporarily in memory based on the specific requirements of your application.
Unlike other databases, a realm does not represent a single, application-wide database. It is not even a relational table; it can contain any number of object types essential to a given domain. The data persisted in the application by the Realm database will be stored in a default Realm file called default.realm
.
There are also additional files that may be used by the database, such as files that are suffixed with .lock
, i.e., default.realm.lock
. The purpose of the lock file is to monitor versions of data in a realm actively being used.
The structure of a realm object is defined via a schema. An object cannot contain a property that is not described by its schema. Since we will be using TypeScript for examples, the schema will be defined in a .ts
file.
Each realm makes use of a versioned schema. When that schema changes, say, if you added a new column (or “property”, in Realm jargon) or modified a column name, then you must define a migration through a new version to move object data between schema versions.
Realm included React Native support through a package that developers install as a dependency. Then, around a year ago, the @realm/react
library was published, which has made using Realm in React Native much easier and better.
The rationale for publishing this can be summed up from the npm website:
Setting up Realm in a React Native application has historically been complex. Re-rendering of components when objects in the database change requires manually adding and removing listeners, which produce a lot of boilerplate code and is error-prone (if listeners properly removed on unmount). This library alleviates that by providing React hooks which return Realm data that is state aware. As a consequence, any change to the Realm data will cause components using the hook to re-render.
This library requires React Native version ≥ 0.59 and Realm v ≥ 11 to be installed on your system.
Let’s explore using Realm in a React Native application. There are several approaches you can choose from:
We’ll use the third approach. The Realm team provides a React Native template with Realm and TypeScript on their GitHub.
To begin with, you can type:
npx react-native init AwesomeRealmProject --template @realm/react-native-template-ts
You may also give a custom name to your project. Let’s use reactrealmdemo
as the name.
Once the project is created, you can test it by running Metro or any other dev client from within the project folder and then, run the project:
cd reactrealmdemo npm start (or react-native start) npm run android
The templated Realm-React Native project is designed to add and delete tasks in a to-do list. It uses Realm for persistence and Realm React Hooks for React integration. It also supports syncing, which allows users to login and synchronize their to-do lists across multiple devices.
If you open the project to examine its files, especially package.json
, you will see the packages that have been automatically installed for you.
{ "name": "reactrealmdemo", "version": "0.0.1", "private": true, "scripts": { "android": "react-native run-android", "ios": "react-native run-ios", "start": "react-native start", "test": "jest", "lint": "eslint . --ext .js,.jsx,.ts,.tsx" }, "dependencies": { "@realm/react": "^0.4.0", "react": "^18.1.0", "react-native": "^0.70.3", "react-native-get-random-values": "^1.8.0", "realm": "^11.0.0" }, "devDependencies": { "@babel/core": "^7.12.9", "@babel/plugin-proposal-decorators": "^7.19.0", "@babel/runtime": "^7.12.5", "@react-native-community/eslint-config": "^2.0.0", "@realm/babel-plugin": "^0.1.0", "@types/jest": "^26.0.23", "@types/react-native": "^0.67.3", "@types/react-test-renderer": "^17.0.1", "@typescript-eslint/eslint-plugin": "^5.17.0", "@typescript-eslint/parser": "^5.17.0", "babel-jest": "^26.6.3", "eslint": "^7.32.0", "jest": "^26.6.3", "metro-react-native-babel-preset": "^0.72.3", "react-test-renderer": "^18.1.0", "typescript": "^4.4.4" }, "resolutions": { "@types/react": "^17" }, "jest": { "preset": "react-native", "moduleFileExtensions": [ "ts", "tsx", "js", "jsx", "json", "node" ] } }
The template application contains several crucial files and also has two versions: one for syncing data, and one which does not sync. The crucial files include:
Task.ts
under models
— defines the schema through an object model with a class named Task
AddTaskForm.tsx
— contains the code to create and add new tasks via a formTaskItem.tsx
TaskList.tsx
— displays a list of tasksTaskManager.tsx
under components
AppSync.tsx
AppWrapperSync.tsx
under app
Note that the type/class name given in the model has to be unique among object types in a Realm.
I built upon this template example to create an application that allowed me to perform CRUD operations to add, view, update, and delete movies from a list. I also wanted to incorporate navigation between screens using React Native Navigation.
Let’s initialize the project:
npx react-native init reactrealmdemo --template @realm/react-native-template-ts
Next, we’ll define a Movie
object model in Movie.ts
:
import {Realm} from '@realm/react'; export class Movie extends Realm.Object<Movie> { description!: string; title!:string; releaseyear!: number; userId!: string; constructor(realm: Realm, description: string, title:string, releaseyear: number, userId?: string) { super(realm, {description, title, releaseyear, userId: userId || '_SYNC_DISABLED_'}); } }
Our index.ts
under models
is defined thusly:
import {createRealmContext} from '@realm/react'; import {Movie} from './Movie'; export const MovieRealmContext = createRealmContext({ schema: [Movie] });
The createRealmContext()
method is used to create a React Context
object for a realm with a given Realm.Configuration
. The Context
object comprises inbuilt Hooks and a wrapper component that provides its child components with access to the Hooks to work with the realm and a context provider.
Next, if you observe the parent component of the template application, AppWrapperNonSync.tsx
, you will see that it is wrapped with the RealmProvider
component included in the Context
object, which was returned from createRealmContext()
.
Here is what our AppWrapperNonSync.tsx
file looks like:
... <SafeAreaView style={styles.screen}> <RealmProvider> <AppNonSync /> </RealmProvider> </SafeAreaView>
The RealmProvider
provides your main component and its children access to the configured Realm through the inbuilt Hooks useRealm()
, useObject()
, and useQuery()
.
In the template app, the code for actually writing to the realm and retrieving/deleting from the realm is enclosed in TaskManager
.ts
, which is called in the AppNonSync
.ts
file.
return <TaskManager tasks={tasks} />;
However, I followed a different approach after playing around with the code. After creating the Movie
object model, I installed various dependencies for navigation and gesture handling.
npm install react-navigation react-native-gesture-handler react-native-safe-area-context @react-native-community/masked-view react-native-screens react-native-reanimated react-navigation-stack react-navigation/native-stack
Since I want to use React Native Navigation with Realm in my project, I created a RootStackParams.ts
file to hold the navigation screen names.
Here’s what my RootStackParams.ts
file looks like:
export type RootStackParamList = { MainScreen: undefined; HomeScreen: undefined; NewAddMovieScreen: undefined; TestUpdateScreen: undefined; UpdateMovieScreen: undefined; DeleteMovieScreen:undefined; ViewMovieScreen:undefined};
I called these in my App.tsx
, the entry point of my project, and I have wrapped my main component between <RealmProvider>
tags so that all child components will get access to Realm’s inbuilt Hooks. Observe the lines in our App.tsx
file that are in bold below:
import React from 'react'; import {NavigationContainer} from '@react-navigation/native'; import {createStackNavigator} from '@react-navigation/stack'; import 'react-native-gesture-handler'; import {RootStackParamList} from './RootStackParams'; import HomeScreen from './screens/HomeScreen'; import NewAddMovieScreen from './screens/NewAddMovie'; import ViewMovieScreen from './screens/ViewMovieScreen'; import { MovieRealmContext } from './models'; import DeleteMovieScreen from './screens/DeleteMovieScreen'; const Stack = createStackNavigator<RootStackParamList>(); export default function App() { const {RealmProvider} = MovieRealmContext; return ( <NavigationContainer> <RealmProvider> <Stack.Navigator> <Stack.Screen name="HomeScreen" component={HomeScreen} /> <Stack.Screen name="NewAddMovieScreen" component={NewAddMovieScreen}/> <Stack.Screen name="ViewMovieScreen" component={ViewMovieScreen} /> <Stack.Screen name="DeleteMovieScreen" component={DeleteMovieScreen} /> </Stack.Navigator> </RealmProvider> </NavigationContainer> ); }
add
operations in RealmWithin HomeScreen
, I configured the UI so it would take us to different screens as we perform CRUD operations.
In AddMovieScreen.tsx
below, I reused some of the code present throughout various files in the template application, mostly AddTaskForm.tsx
and TaskManager.tsx
, to create a single screen that would perform the add
operation.
import { View, StyleSheet, ScrollView, KeyboardAvoidingView, TextInput, Text, Pressable, Platform } from 'react-native'; import { StackNavigationProp } from '@react-navigation/stack'; import { RootStackParamList } from '../RootStackParams'; import { Movie } from '../models/Movie'; import colors from '../styles/colors'; import { shadows } from '../styles/shadows'; import { buttonStyles } from '../styles/button'; import { useNavigation } from '@react-navigation/native'; import { MovieRealmContext } from '../models'; import React, { useState, useCallback } from 'react'; type addScreenProp = StackNavigationProp<RootStackParamList, 'NewAddMovieScreen'>; const { useRealm } = MovieRealmContext; const AddMovieScreen = () => { const realm = useRealm(); const handleAddMovie = useCallback( (description: string, title: string, releaseyear: number, userId: string): void => { if (!description) { return; } realm.write(() => { return new Movie(realm, description, title, releaseyear, userId); }); }, [realm], ); return ( <View style={styles.content}> {<AddMovieForm onSubmit={handleAddMovie} />} </View> ); }; type AddMovieFormProps = { onSubmit: (description: string, title: string, releaseyear: number, userId: string) => void; }; export const AddMovieForm: React.FC<AddMovieFormProps> = ({ onSubmit }) => { const [description, setDescription] = useState(''); const [title, setTitle] = useState(''); const [releaseyear, setYear] = useState(''); const [userId] = useState(''); const navigation = useNavigation<addScreenProp>(); const convertdate = (textyear: string) => { var dt = new Date(textyear); return dt.getFullYear(); } const handleSubmit = () => { onSubmit(description, title, convertdate(releaseyear), userId); setDescription(''); setTitle(''); setYear(''); }; return ( <View style={styles.listContainer}> <ScrollView keyboardShouldPersistTaps="handled" > <KeyboardAvoidingView behavior="padding" style={{ flex: 1, justifyContent: 'space- between' }}> <TextInput value={title} placeholder="Enter new movie title" onChangeText={setTitle} autoCorrect={false} autoCapitalize="none" style={styles.textInput} /> <TextInput value={description} placeholder="Enter new movie description" onChangeText={setDescription} autoCorrect={false} autoCapitalize="none" style={styles.textInput} /> <TextInput value={releaseyear} placeholder="Enter new movie year" onChangeText={setYear} maxLength={4} keyboardType="numeric" style={styles.textInput} /> <View > <Pressable onPress={handleSubmit} style={styles.submit}> <Text>Submit</Text> </Pressable> </View> <Pressable style={styles.button} onPress={() => navigation.navigate('HomeScreen')}> <Text style={styles.buttontext}>Go Home</Text> </Pressable> </KeyboardAvoidingView> </ScrollView> </View> ); }; export default AddMovieScreen;
In this file, I included code to display a form that accepts movie details via user input. We use the inbuilt useRealm(
) Hook to retrieve the currently open Realm instance. The data from the form fields is then passed to the method handleAddMovie()
, which in turn is wrapped around a callback.
The data from the form is written to the realm using realm.write()
. A write
operation in Realm is a callback function passed to a Realm instance function, and enables you to create, modify, and delete Realm objects. This means that even to delete data in a realm, you will be calling realm.write()
.
Upon testing my project on a real device (since it was faster than an Android emulator), I opened the app to the HomeScreen.
Clicking Add Movie brings me to this screen:
Likewise, in DeleteMovieScreen.tsx
, I added code to perform delete
operations in the realm.
import React from 'react'; import {View, StyleSheet, Pressable, Text, FlatList, SafeAreaView} from 'react-native'; import {Movie} from '../models/Movie'; import {MovieRealmContext} from '../models'; import {StackNavigationProp} from '@react-navigation/stack'; import {RootStackParamList} from '../RootStackParams'; import { useNavigation } from '@react-navigation/native'; import colors from '../styles/colors'; const {useRealm, useQuery} = MovieRealmContext; type delScreenProp = StackNavigationProp<RootStackParamList, 'DeleteMovieScreen'>; function DeleteMovieScreen() { const navigation = useNavigation<delScreenProp>(); const realm = useRealm(); const movies = useQuery(Movie); return ( <SafeAreaView style={styles.listContainer}> <View style={{ flexDirection: 'row', justifyContent: 'center', margin: 10 }}> </View> <FlatList data={movies.sorted("releaseyear")} renderItem={({ item }) => { return ( <View style={{ flexDirection: 'row', justifyContent: 'center', margin: 10 }}> <Text style={styles.text} >{item.title}</Text> <Pressable style={styles.button} onPress={() => { realm.write(() => { realm.delete(item) }) }} ><Text style={styles.buttontext}>{"Delete 🗑️"}</Text></Pressable> </View> ); }} ></FlatList> <Pressable style={styles.button} onPress={() => navigation.navigate('HomeScreen')}> <Text style={styles.buttontext}>Go Home</Text> </Pressable> </SafeAreaView > ); } export default DeleteMovieScreen;
Observe the lines in bold above. The statement realm.delete()
is included within a realm.write()
. The outcome of this screen will be a sorted list of existing movies with a Delete button and trash can icon next to each movie title.
Clicking the Delete button will delete the specific record for the selected movie title from the realm.
In ViewMovieScreen.tsx
, I wrote code to display a list of movies from the realm. The inbuilt useQuery()
Hook will return a collection of Realm objects of a given type. It can be either invoked with the Object Schema class or the model name given as a string. The useQuery()
Hook monitors updates to any objects in the collection and re-renders the component on any change to the query results.
import React from 'react'; import { View, StyleSheet, Pressable, Text, FlatList } from 'react-native'; import { Movie } from '../models/Movie'; import { MovieRealmContext } from '../models'; import { StackNavigationProp } from '@react-navigation/stack'; import { RootStackParamList } from '../RootStackParams'; import { useNavigation } from '@react-navigation/native'; /* import { ViewMovieItem } from '../components/ViewMovieItem'; */ import { shadows } from '../styles/shadows'; import colors from '../styles/colors'; type updateScreenProp = StackNavigationProp<RootStackParamList, 'ViewMovieScreen'>; function ViewMovieScreen() { const navigation = useNavigation<updateScreenProp>(); const { useQuery } = MovieRealmContext; const result = useQuery(Movie); const movies = result.sorted("releaseyear"); return ( <View style={styles.listContainer}> <FlatList data={movies} renderItem={({ item }) => ( <ViewMovieItem movie={item} /> )} /> <Pressable style={styles.button} onPress={() => navigation.navigate('HomeScreen')}> <Text style={styles.buttontext}>Go Home</Text> </Pressable> </View> ); } type ViewMovieItemProps = { movie: Movie & Realm.Object; }; export const ViewMovieItem = React.memo<ViewMovieItemProps>( ({ movie }) => { return ( <View style={styles.movie}> <View style={styles.movieContainer}> <Text numberOfLines={1} style={styles.text}> {movie.title} </Text> <Text numberOfLines={1} style={styles.text}> {movie.description} </Text> <Text numberOfLines={1} > {movie.releaseyear} </Text> </View> </View> ); }, ); export default ViewMovieScreen;
The output is a list of movie details sorted by release year.
If you change the schema after the app has been executed and data already exists in the database, you must migrate the schema or increase the schema version. You can do this in the index.ts
file, as shown here:
import {createRealmContext} from '@realm/react'; import {Movie} from './Movie'; export const MovieRealmContext = createRealmContext({ schema: [Movie], schemaVersion:2 });
A sample use case for schema change would be changing the name or type of a property. In such a case, increasing the schema version is the easiest way to continue with executing the app. If you don’t do this, the application fails to run and raises an error about migration.
update
functionality in Realm with Realm StudioFor the Update
task of CRUD, I decided to go offbeat. Instead of creating a screen similar to the View
, Delete
, and Add
screens we built to manipulate realm data, I used MongoDB’s Realm Studio v 13.0 to more easily manage my Realm database. Opening existing realms and adding and modifying data in them becomes a lot simpler with a GUI-based tool.
To achieve the Update
task using Realm Studio, let’s first locate our Realm file. You can determine the local path of the Realm file on your device or simulator using the below code:
Realm.open({}).then(realm => { console.log("Realm is located at: " + realm.path); });
For example, in the current project, the path would be:
/data/data/com.reactrealmdemo/files/default.realm
We can then open this file through Android Studio’s Device File Explorer tool, but only when then app is running. Ensure that you have configured Android Studio as a framework for the project regardless of which IDE you used to edit your project files. For instance, I used VS Code to edit my project files, so when I opened the project for the first time in Android Studio, I was prompted to configure the framework.
Once you’re past that step and have opened the Device File Explorer, scroll down until you find your realm file.
Right-click default.realm
and click Save As to save the realm file to your disk. Once this is done, you can use Realm Studio to open, view, and edit the file.
Launch Realm Studio and click Open Realm file on its main screen. Select the location where you just saved the file.
Here is the default.realm
file I had saved to C:\Source Codes
. Our movie data is now visible in the GUI tool. I can click any property (column) and make edits.
I can even add new properties, delete existing records, and so on.
Note that this data is now present on the local disk and not on the emulator or the device where the project was running, so the original realm file remains unaffected by any changes we make here, unless we replace that file with an updated one.
It is also possible to set the realm location and name to a different one than the default one before it is created for the first time using Realm.Configuration
.
Realm Studio also allows you to import and export realm data. You can import data from any CSV or export existing realm data to a CSV.
You can also save your model definitions to specific formats and reuse them in other applications.
You can save data to JSON format or another local realm file.
Thus, using Realm with React Native is quite easy, more so with the @realm/react library. A developer can make good use of this library, its ready-made templates, and Realm Studio to handle data storage and manipulation tasks quickly and efficiently in mobile applications.
Some final do’s and don’ts when working with Realm for React Native:
LogRocket is a React Native monitoring solution that helps you reproduce issues instantly, prioritize bugs, and understand performance in your React Native apps.
LogRocket also helps you increase conversion rates and product usage by showing you exactly how users are interacting with your app. LogRocket's product analytics features surface the reasons why users don't complete a particular flow or don't adopt a new feature.
Start proactively monitoring your React Native apps — try LogRocket for free.
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowExplore use cases for using npm vs. npx such as long-term dependency management or temporary tasks and running packages on the fly.
Validating and auditing AI-generated code reduces code errors and ensures that code is compliant.
Build a real-time image background remover in Vue using Transformers.js and WebGPU for client-side processing with privacy and efficiency.
Optimize search parameter handling in React and Next.js with nuqs for SEO-friendly, shareable URLs and a better user experience.