Building an offline-first app with React and RxDB

Offline functionality is becoming an increasingly important part of an app’s user experience. This is not just important for apps that function offline-first, but for web or mobile JavaScript apps that work with intermittent internet connectivity.

Making an application work without internet connection involves two things. First, the application files need to be stored locally. This can be done with Progressive Web Apps and Service Workers.

Second, a form of local storage must be used as the main data source of the application. This storage must be continuously synchronized with a remote data source. Luckily, there are databases like PouchDB, which store data locally and can be synced automatically with a CouchDB database in a server.

In this tutorial, we’re going to build an anonymous chat app with offline capabilities. It will look like this:

We’re going to use Create React App to automatically generate a progressive web app with React and RxDB, a reactive, client-side, and offline-first database that will sync with a server-side CouchDB database.

You will need:

  • Node version 6 or higher. You can use nvm to switch between Node versions.
  • A modern browser that supports IndexedDB.

For reference, you can find the entire source code of on this GitHub repository:

Setting up the React app

Install (or upgrade to the latest version) create-react-app:

npm install -g create-react-app

Then, create a new app and cd into it:

create-react-app offline-anonymous-chat
cd offline-anonymous-chat

Now let’s install all the dependencies for our project by executing:

npm install --save concurrently moment pouchdb-adapter-http pouchdb-adapter-idb pouchdb-server react-toastify rxdb rxjs serve

Here’s the description of each dependency:

  • concurrently. We’ll need it to run two npm scripts at the same time.
  • moment. We’ll need it to format the creation date of the message.
  • pouchdb-adapter-http. PouchDB adapter for communicating with an external CouchDB (or CouchDB-like) database.
  • pouchdb-adapter-idb. PouchDB adapter to use IndexedDB in a browser. You can use PouchDB (and RxDB) in different environments by just switching the adapter.
  • pouchdb-server. This will work as our server-side database.
  • react-toastify. We’ll need it to show notifications in the app for the database events.
  • rxdb. This will work as our client-side database.
  • rxjs. RxDB handles data in a reactive way, so it depends on rxjs.
  • serve. When testing the offline functionality, we’ll need an HTTP server to serve our app (more on this later).

Next, edit the start script in the package.json file. Instead of:

"start": "react-scripts start",

It should be:

"start": "concurrently \"npm run server\" \"react-scripts start\"",

Also, we should add the server script:

"server": "pouchdb-server -d ./db",

This will execute the PouchDB server at the same time the application is running. The database file will be stored in the db directory. You can find all the configuration options for the database server here.

When you execute npm start, you can go to http://localhost:3000 (if it’s not opened automatically) to view the app generated by create-react-app:

For the database, you can view its web interface (Fauxton) by opening http://localhost:5984/_utils:

As you can see, there are just two databases created that are used internally by PouchDB.

In the next section, we’ll create the schema for our app database.

Creating the database schema

RxDB relies on PouchDB (in turn, inspired by CouchDB), which makes it a NoSQL document database, similar to MongoDB.

We can use schemas to define how the data will look and validate every inserted document. The data is organized into collections and every collection has its own schema.

For our app, we are going to use a collection named messages which will store the date the message was created and the message itself.

Schemas are defined using the JSON Schema standard. For our purposes, create the file src/Schema.js with the following content:

In the above schema:

  • The version number is zero. If the version is greater than zero, you have to provide a data migration strategy.
  • It has two properties of type string, id, and message. The first one is the primary key, and it represents the date the message was inserted as the number of milliseconds since the Unix epoch.
  • The property message is required.

You can learn more about schemas here.

Now let’s configure the local database.

Configuring the local database

Add the following imports to App.js:

In addition to importing the main RxDB module and the schema created in the previous section, we’re importing the module QueryChangeDetector.

As RxDB is a reactive database, you can subscribe to queries to receive new results in real-time, but executing a query every time this happens can impact performance, so the option QueryChangeDetector optimizes observed queries by getting new results from database events instead.

This option is currently in beta and disabled by default, but we can enable it with the following code:

We can also enable debugging so every time an optimization is made, a message in the console is shown.

Next, configure the adapters to use IndexDB as the storage engine, and enable syncing to a remote database over HTTP:

And declare as constants the URL of the remote database and the name of the local database:

Inside the class, let’s declare a method to create the database:

The database is created in an asynchronous way, but instead of using callbacks or promises, we’re going to use async/await for more concise and clean code. Here’s a good article comparing the three approaches of writing asynchronous code.

This way, we create the database passing a name, and adapter, and a password (which must have at least 8 characters):

Next, we enable the leader-election algorithm which makes sure that always exactly one tab is managing the remote data access (in case there are multiple tabs of the application at the same time). When the leader is elected, a crown will be shown next to the page’s title:

Then, we create the messages collection passing the schema:

Finally, let’s set up the replication feature and return the database object:

Now let’s integrate this method into our React app.

Building the app

Let’s start by adding the imports for the component to show notifications and the moment library to handle dates:

Next, add the following constructor to the class:

We have two properties as the state of our component, one for new messages, and an array to store all the messages.

We also defined an array to store the handlers of the subscriptions we’re going to use so we can unsubscribe when necessary, along with the binding of the functions to add a new message and handle the text box’s changes for new messages.

In componentDidMount, let’s call the method createDatabase, and then subscribe to a query that returns all the messages sorted by id:

The method $() will return a RxJS observable which streams every change to the data returned by the query. If any messages are returned, a notification is shown and the state is changed.

Notice that we also add a reference to the subscription to the subs array. This way, when the component is about to be unmounted, we’ll use those references to unsubscribe from the observables:

Next, modify the render method so it looks like this:

As you can see, the rendering logic is simple.

The method renderMessages converts each message object into a div element showing the relative insertion date of the message with the help of the moment library (remember that the id represents the date in milliseconds) and the message:

This is the definition of the method handleMessageChange:

And to add a message, we create an object and insert it into the database in the following way, setting the state to an empty string for the next message:

Finally, probably is a good idea to show the replication events to understand what is going on. The method RxCollection.sync() returns a RxReplicationState object that can be used to observe events and to cancel the replication.

Modify the last part of the method createDatabase so it looks like this:

Some of these events emit an object, that’s why in some cases they are printed in the console in addition or instead of being shown as notifications. Here’s a description of each event:

  • change$. Triggered every time a document is replicated. It can emit a boolean or an object describing the change.
  • docs$. Emits each replicated document.
  • active$. Emits true or false depending on whether the replication is running or not.
  • complete$. Emits true or false depending on whether the replication is completed or not (actually, only one-time-replications will complete).
  • error$. Triggered when an error occurs during the replication process.

Also, we added these subscriptions to the subs array so we can unsubscribe when necessary.

And that’s it. Let’s test the app.

Testing the app

Execute npm start and go to http://localhost:3000. You should see something like this:

Start adding some messages. Notice the crown in the title, it indicates this tab is the leader. If you open another tab, and close the first one, now the other should have the crown:

Thanks to the reactive nature of RxDB (and the fact that all the tabs in a browser share the same local storage), the database state is broadcasted in real-time, which means that the changes in one tab will be reflected automatically in another tab:

Go to http://localhost:5984/_utils and now you should see the app’s database:

Click on the database name and then on the pencil icon of one of the documents to edit it:

The change made on the database web interface should be applied to the app almost instantly:

So it seems to be working fine, but does it work in offline mode?

Going offline

The production build of the app generated by create-react-app is a fully functional, offline-first progressive web app.

There are some considerations we must take into account, but basically, we just have to build the application and serve it. Execute:

npm run build

This command creates a build directory with a production build of your app.

Now add the following scripts to the package.json file:

"http": "serve -p 3000 -s build",
"offline": "concurrently \"npm run server\" \"npm run http\""

And execute:

npm run offline

Since the app now will install a service worker and a manifest, probably you may want to test it using your browser’s incognito-mode, to avoid conflicts if you want to go back to the development version later.

Once again, go to http://localhost:3000, the application will install the service worker and the browser will cache the files. If you’re using Chrome, you can check this by going to Developer Tools — Application and choose the sections Manifest and/or Service Workers:

Now go offline by either choosing the Offline option from the Service Worker panel, the Offline option of Developer ToolsNetwork tab, or just disconnecting from the network.

When you reload the app, you should still be able to insert messages:

After inserting some messages, go back online. After a while, you should see some notifications about replication:

If you go to the database web interface, the database should contain the new documents:


RxDB has a lot of more features that were not covered here, like encryption, middleware hooks, or atomic update operations.

You can extend this application by implementing editing and deleting operations, or by synchronizing the data to a CouchDB server or an IBM Cloudant database.

Remember that you can find the entire source code of the app on this GitHub repository.

Plug: LogRocket, a DVR for web apps

LogRocket is a frontend logging tool that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.

In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single page apps.

It’s tough to keep up-to-date on front-end dev. Join our weekly mailing list to learn about new tools, libraries and best practices that will help you build better apps: