Tools and Services for Storing Data in React Native
React Native as a mobile app development platform is fairly new. Having only been released in 2015, it still hasn’t reached version one yet. But even so, companies are already using it to build their apps. Most apps usually need a database to store their user data, be it locally or in a server. Due to React Native’s popularity, there are already lots of database solutions to choose from—which makes the task of choosing the right database a bit challenging.
In this guide, we’ll take a look at some of the most popular choices for storing and managing data in React Native. It aims to provide an overview of the different database solutions currently available to make the decision-making process a little easier. Note that some of the solutions we’ll be exploring don’t have official React Native support, so a bit of work may be required.
AsyncStorage
AsyncStorage is the React Native counterpart of the localStorage API that we use in the web environment. It’s a simple key-value store with no encryption and can only persist data locally.
If your app doesn’t need to store a whole lot of data, and the data only needs to live locally, then you can use AsyncStorage. It’s best used for apps that only have a simple data structure, and no querying and filtering needs. Yes, you can store stringified JSON objects and then filter them using JavaScript once you’ve fetched them from the local store. But take note that AsyncStorage has a slow runtime and it has no indexing capability, so it can’t handle a whole bunch of data.
AsyncStorage used to be a part of the core React Native APIs, but now it lives under the React Native Community umbrella due to the lean core efforts —with the aim of reducing the number of APIs that are part of React Native core as well as making the development efforts for these individual APIs separate from the core.
Here’s an example of how you would use AsyncStorage. As you can see, it only allows you to store string values:
import AsyncStorage from "@react-native-community/async-storage"; // import the module
// saving data:const saveAttendee = async (name) => { try { await AsyncStorage.setItem("attendee", name); } catch (err) { console.log("error storing the data: ", err); }};saveAttendee('John Wick');
// retrieving dataconst retrieveAttendee = async () => { try { const attendee = await AsyncStorage.getItem("attendee"); return attendee; } catch (err) { console.log("error retrieving data: ", err); }};
const attendee_name = await retrieveAttendee();
Check out the following links to learn more:
Realm
Realm is a mobile database with a back-end component. It’s offline-first, so the data is saved locally first and then syncs to the back-end component if the user is online. It has an SDK for the most popular mobile app development platforms including Java, Swift, Objective-C, JavaScript, and Xamarin. This makes it a good choice for an app which you may want to transition away from React Native in the future.
Realm is an object-based database. The database objects directly reflect the values in your database. So there’s no additional overhead for converting back and forth when reading or writing data.
Realm performs 10x better than SQLite implementations (which we’ll cover later), even though both storage engines are written in C. It can also handle concurrent operations, because it has algorithms in place for handling conflicts. This makes it possible to use Realm for large data and high-performance applications.
The only disadvantage of Realm is that it needs a bit of setup if you’re opting for the self-hosted solution—though you also have the option of letting Realm host your data.
Realm includes the following features:
- Realm sync —saves the data locally first. It only syncs the data back to the server if the user is online.
- Offline capability —provided by Redux offline if you’re using Redux for state management.
- Realm Studio —for managing your database locally. This tool allows you to open and edit local and synced Realms, and administer any Realm Object Server instance.
- Queries —uses an SQL-like query for sorting and filtering data.
- Migrations —you can write code for changing the structure of your database. Realm also allows you to define how conflicts will be handled when updating existing data that still uses the old structure.
- Notifications —Realm instances send out notifications to other instances every time a write transaction is committed. You can also add a listener to react to these changes.
- Encryption —if you need to secure your data. You can also have it encrypted, although the performance will suffer a bit (about 10% slower).
Here’s an example of how to use Realm. As you can see, it requires a bit more setup than AsyncStorage. When using Realm, you need to set up a model that represents the object you’re trying to save. You also need to create the schema that describes each of the properties of the object you’re trying to save. Only after this can you save the actual data. You can also expect the same thing on all the other libraries that we’ll be exploring:
import Realm from "realm";import uniqid from "uniqid";
class PersonModel { constructor(name) { this.id = uniqid(); this.name = name; }}
// create a schemalet attendee = new Realm({ schema: [ { name: "Attendee", // name of the schema or table primaryKey: "id", // ID uniquely identifies each Attendee properties: { id: { type: "string", indexed: true }, // index the ID for faster searching name: "string" } } ]});
// save the data:attendee.write(() => { attendee.create("Attendee", new PersonModel("John Wick"));});
// retrieve data:attendee.objects("Attendee").filtered("name = 'John Wick'");
Check out the following links to learn more:
Firebase
If you need a database with real-time capabilities, Firebase is the first choice. Firebase’s official JavaScript SDK works on React Native as well. This allows you to use its real-time database using the same API that you get when working on a web app’s client side. The only downside is that Firebase doesn’t have an official React Native SDK. So if you’re looking into using some of Google’s other services (Cloud Messaging, Google Analytics) then you’re out of luck. The only alternative solution is React Native Firebase.
Firebase is also cross-platform, so you can use the same API on your Node.js back end, web app client-side, and React Native app.
Do note that Firebase doesn’t have data encryption capabilities. So if your app needs to handle sensitive data, you’re out of luck. But if you have time to spare, you can actually encrypt the data on the client side before sending it to Firebase.
Just like Realm, Firebase also has Offline capabilities. Firebase provides ways for you to write to the database when a client is disconnected from the server. More information can be found about it in this page: Enabling Offline Capabilities in JavaScript.
Here’s an example of how to use Firebase:
import Firebase from "firebase";let config = { apiKey: "XXX", authDomain: "XXX-XXXX.firebaseapp.com", databaseURL: "XXX-XXXX.firebaseapp.com", projectId: "XXX-XXXX", storageBucket: "XXX-XXXX.appspot.com", messagingSenderId: "XXXXXXX"};let app = Firebase.initializeApp(config);const db = app.database();
// save dataconst saveAttendee = name => { db.ref("/attendees").push({ name });};
saveAttendee("John Wick");
// retrieve data:db.ref("/attendees").on("value", snapshot => { let data = snapshot.val(); let attendees = Object.values(data); console.log("attendees: ", attendees);});
As you can see from the code above, Firebase isn’t really a traditional database into which you save data and then retrieve it later on. It’s a real-time database, so the whole premise is to subscribe to the data and sync it across all instances. This makes the retrieval of very specific records almost impossible to accomplish.
Check out the following links to learn more:
SQLite
If your app only needs local storage with only a little data to work with, then SQLite is for you. It’s an ACID-compliant database, and it uses SQL for performing CRUD operations. You can use the React Native SQLite 2 library to use SQLite in React Native.
If your app needs data encryption, you have to purchase a license in order to use it.
Here’s an example of how to use SQLite. For this to work, you need to have an SQLite database already created. There’s a tool called DB Browser for SQLite which you can use to quickly create and manage an SQLite database:
import SQLite from "react-native-sqlite-2";import uniqid from "uniqid";
const db = SQLite.openDatabase("attendees.db", "1.0", "", 1);
// saving data:db.transaction(tx => { tx.executeSql("INSERT INTO attendees (id, name) VALUES (:id, :name)", [ uniqid(), "John Wick" ]);});
// retrieve data:db.transaction(tx => { tx.executeSql("SELECT * FROM attendees;", [], (tx, results) => { const rows = results.rows;
for (let i = 0; i < rows.length; i++) { console.log("attendee: ", rows.item(i)); } });});
As you can see from the code above, you’ll feel right at home if you’ve used SQL before. When using SQLite, you’re basically executing SQL queries to the database and extracting the data that they return.
Check out the following link to learn more:
PouchDB
PouchDB is the mobile counterpart of CouchDB, a NoSQL database that’s built for syncing. It supports syncing multiple replicas of the same database all over the world. It also has a replication feature that allows local storage to be disconnected from the server while both copies are concurrently updated.
PouchDB has first-class support for connecting to a CouchDB database. This gives you the ability to perform CRUD operations using a simple API—and use the map-reduce pattern to query data. The data is stored in JSON format, so it’s very easy to reason about, especially if you’re coming from a web background.
The disadvantage if you want to use PouchDB is that there’s no official React Native SDK for working with it. There’s the pouch-db-react-native library, but it’s no longer maintained as much. So there might be some issues in using it with React Native. Your best best is to follow this tutorial to get PouchDB to work with React Native: Hacking PouchDB to Use It on React Native.
As for the back-end database for syncing, aside from CouchDB, you can also use the following:
- IBM Cloudant —a distributed database that’s optimized for handling heavy workloads
- PouchDB Server —a drop-in replacement for CouchDB built on PouchDB and Node.js.
Here’s an example of how to use PouchDB:
import PouchDB from "@craftzdog/pouchdb-core-react-native";import HttpPouch from "pouchdb-adapter-http";import replication from "@craftzdog/pouchdb-replication-react-native";import mapreduce from "pouchdb-mapreduce";
import SQLite from "react-native-sqlite-2";import SQLiteAdapterFactory from "pouchdb-adapter-react-native-sqlite";
const SQLiteAdapter = SQLiteAdapterFactory(SQLite);
import uniqid from "uniqid";
const pouch = PouchDB.plugin(HttpPouch) .plugin(replication) .plugin(mapreduce) .plugin(SQLiteAdapter);
const db = new pouch("attendees.db", { adapter: "react-native-sqlite" });
// save data:db.post({ id: uniqid(), name: "John Wick"}) .then(result => console.log("saved attendee: ", result)) .catch(error => console.warn("error", error, error.message, error.stack));
// retrieve data:db.allDocs({ include_docs: true, limit: null}) .then(result => { const attendees = result.rows.map(row => row.doc); console.log("attendees: ", attendees); }) .catch(error => console.warn("Could not load Documents", error, error.message) );
// watch for changes:localDB.changes({ since: "now", live: true }).on("change", change => { console.log("do something with the changes..", change);});
From the code above, you can see that we’re actually using SQLite as the local database. This is because PouchDB doesn’t have its own. You need to bring in your local database in order to work with it.
Check out the following links to learn more:
WatermelonDB
If your app needs to work with tens of thousands of records and still remain fast, WatermelonDB is for you. It implements lazy loading so that it only loads specific data when it’s requested. Built on top of SQLite, it runs on a separate native thread, which makes it fast because it doesn’t need to go through the “ bridge ”. Data in WatermelonDB is stored locally, but you have to bring your own back end if you want to implement the sync feature.
WatermelonDB has first-class support for React because it’s made for React. This means that you can easily plug in data that comes from WatermelonDB into your components. It’s also a reactive database, so when data is updated, the UI that uses that particular data is also automatically re-rendered.
Do note that even though WatermelonDB also has synchronization capabilities, it’s only able to sync with a remote database. This allows for replication of data on multiple databases around the world. But as mentioned earlier, you have to implement your own back end. It stores information on which records were created, updated, or deleted locally since the last sync, but you’ll have to be the one to write the code for handling it on your back end. You can read more about the sync feature here.
Just like Realm, WatermelonDB has the following features as well:
- Query API —the queries are similar to SQL, but different methods are exposed instead of writing the query directly.
- Relation API —allows you to specify the relationship between two tables so you can get the child data from a parent table. For example, getting the comments added by a specific user in a forum. In this case,
users
andcomments
are two separate tables. So in order to get the comments by a specific user you have to add a relation between the two. - Migrations —provides a way for you to add new tables and columns to your existing database without breaking old data.
Here’s an example of how to use WatermelonDB:
import { Database } from "@nozbe/watermelondb";import SQLiteAdapter from "@nozbe/watermelondb/adapters/sqlite";
import { appSchema, tableSchema, Model } from "@nozbe/watermelondb";import { field } from "@nozbe/watermelondb/decorators";
import uniqid from "uniqid";
// set up the database schemaconst attendeeSchema = appSchema({ version: 2, tables: [ tableSchema({ name: "attendees", columns: [ { name: "id", type: "number", isIndexed: true }, { name: "name", type: "string" } ] }) ]});
// set up the modelclass Attendee extends Model { static table = "attendees";
@field("id") id; @field("name") name;}
// set the local databaseconst adapter = new SQLiteAdapter({ dbName: "attendees", schema: attendeeSchema});
// connect to the databaseconst db = new Database({ adapter, modelClasses: [Attendee]});
// get the collectionconst attendees_collection = db.collections.get("attendees");
// save data:attendees_collection.create(attendee => { attendee.id = uniqid(); attendee.name = 'John Wick';});
// retrieve data:const best_attendee = await attendees_collection.query(Q.where('name', 'John Wick')).fetch();
As you can see from the code above, WatermelonDB requires some amount of setup before you can use it to manipulate your data. And just like PouchDB, it also requires SQLite as the local database.
Check out the following links to learn more:
Amazon DynamoDB
Amazon DynamoDB is a fully managed NoSQL database service that provides fast and predictable performance with seamless scalability. Since it’s all managed by Amazon, you don’t need to worry about scaling, replication, and closely managing it. All you have to do is scale it up via their admin interface when the need arises.
When it comes to security, Amazon DynamoDB encrypts user data at rest. The encryption keys are stored in your AWS Key management Service.
You can use AWS Amplify to connect to your Amazon DynamoDB database. AWS Amplify is a JavaScript library for front-end and mobile developers building cloud-enabled applications. Aside from DynamoDB, it also allows you to add authentication, analytics, push notifications, and other services that are needed for mobile apps.
Here’s an example of how to use Amazon DynamoDB:
import Amplify, { API } from "aws-amplify";import uniqid from "uniqid";
// save data:const saveAttendee = async name => { let attendee = { body: { id: uniqid(), name: name } }; const path = "/attendees"; try { await API.put("attendees", path, attendee); } catch (e) { console.log(e); }};
// retrieve data:const getAttendee = async id => { const path = "/attendees/object/" + id; try { const res = await API.get("attendees", path); return res; } catch (e) { console.log(e); }};
saveAttendee("John Wick");console.log("attendee: ", getAttendee("1"));
From the code above, you can see that Amazon DynamoDB leans more towards simplicity. All it provides is put
and get
methods, which makes the API comparable to what’s provided by AsyncStorage.
Different Data Types and Complex Queries
If you want to be able to store data with fields of different data types and perform complex queries, you’ll have to use an entirely different service called AppSync. Using AppSync gives you the additional functionality for subscribing to specific events that happens in the database. This gives your app realtime sync features just like PouchDB and Realm.
Check out the following links to learn more:
Comparing the Solutions
Before we wrap up this guide, let’s compare the solutions side by side. Each database solution is rated from 1 to 5 (1 being the lowest and 5 being the highest) based on the following metrics:
- Ease of use —how easy it is to get up and running with it.
- Advanced usage —whether it allows for complex queries.
- Security —the rating of the security tools that it provides.
- Offline-first —whether it supports local storage.
- Performance —the rating of how it performs when you’re working with a lot of data.
- Realtime sync —whether it supports realtime synchronization of data across multiple devices.
- Pricing —how much the monthly cost of the service is. Note that all of the paid services (Realm, Firebase, Amazon DynamoDB) start free. You can visit the pages that I just linked to earlier for more information about the pricing. This is especially needed for Amazon DynamoDB, because it doesn’t have a fixed monthly pricing—though when I tried its pricing calculator, it was right around the ballpark of Realm’s monthly cost (maybe even cheaper depending on your needs).
A Note about the Results
The scores for the ease of use and advanced usage metrics are highly dependent on my own personal experience as I tried to use each of them to do two basic operations: insert and retrieve. The performance and security metric only depends on what came up on my research. No actual benchmarking and security testing was done.
Ease of use | Advanced usage | Security | Pricing | Offline-first | Performance | Realtime sync | |
---|---|---|---|---|---|---|---|
AsyncStorage | 5 | 1 | 1 | Free | Yes | 3 | No |
Realm | 4 | 5 | 5 | $30 upwards | Yes | 5 | Yes |
Firebase | 5 | 2 | 1 | $25 upwards | No | 5 | Yes |
SQLite | 4 | 5 | 3 | Free | Yes | 5 | No |
PouchDB | 1 | 5 | 3 | Free | Yes | 5 | Yes |
WatermelonDB | 2 | 5 | 3 | Free | Yes | 5 | Yes |
Amazon DynamoDB | 5 | 5 | 4 | Depends | Yes | 5 | Yes |
Based on the table above, you would generally choose either AsyncStorage or SQLite for simple apps. For more complex apps, the best choice would be Realm. Bt if you have time to set up your own back-end solution, or to deal with issues that come with having no official support, you can either choose WatermelonDB or PouchDB. Lastly, if you have realtime syncing needs, consider either Firebase, Realm, PouchDB, or WatermelonDB.
Conclusion
In this guide, we covered some of the most popular choices for storing and managing the data for your React Native app—AsyncStorage, Realm, SQLite, Firebase, PouchDB, WatermelonDB, and Amazon DynamoDB.
We haven’t really covered popular back-end–only database solutions such as MySQL, PostgreSQL, and MongoDB here, but they can certainly be used to power your React Native app’s back-end database needs as well—though you’ll have to create your own API for interacting with your data.
When it comes to choosing what’s best, it’s important to know what the requirements of the app are. You should consider if the solution you’re thinking of using now will continue to meet your needs into the future.
This guide has provided you with an overview of the different solutions available. I encourage you to check out the documentation for these various tools and try using them, because that’s the only way to know which is right for your specific needs.