Creating an offline-first React Native app

We fixed this problem (though we don’t advise using in space). Original image https://unsplash.com/photos/_GJrk7Gj8lE

At OnCare we’ve gone to great pains to make a service that our customers love using. Part of our journey has been to ensure the app customers use through the day, every day works offline or on terrible internet connections. Here’s some of our learnings in realising this offline experience.

Assumed starting point

There’s a lot to building an app, most of that has been well covered elsewhere so I’m going to assume you’re familiar with the following concepts:

  • React Native
  • Redux
  • Making backend read and write requests

Objective

There’s many aspects to a fully offline first app but for the purposes of this article I want to focus on the most impactful ones:

  • Treating the network as an unreliable, second class citizen
  • Retaining state upon crash or restart
  • Not duplicating server resources when making backend retries

Pulling it together step by step

Networking

It’s logical to think that you should base your application flow logic on requests to your backend but this perspective needs to change to cater to the real world of offline usage or just flaky connections.

Start with empty states

Empty goal https://unsplash.com/photos/YtLAiN5YJ-8

With the conventional way of thinking, you’d have a view’s initial state being a loading screen whilst it awaits the backend response. When there’s no connection, this is a promise we can’t ever fulfil for the user so we need to think differently.

Assume that there’s no connection, you should show an empty state for whatever your scene is. Thankfully this is not a new concept and there’s plenty of inspiration on how this can be pulled off at http://emptystat.es/.

The ideal user experience here is that you show something of value that unblocks the user from doing whatever they are trying to achieve. This could be as simple as bundling some useful content for the first-time use.

Pessimistically call the backend

Now you have an offline state, it’s time to start attempting to fetch your backend resources in the background. You want to wait for an internet connection, fetch your resources and sensibly retry the fetch if the request fails. We want some way of eventually giving up and declaring that the request failed so we can cancel loading indicators etc. This is quite a lot of grunt work to orchestrate this yourself but thankfully there’s an elegant library to help — React Offline created by Jani Eväkallio.

Redux Offline

Glossing over the setup, the crux of Redux offline is the use of a Redux action that covers 4 use-cases:

  • Initiate the request; useful for showing loading indicators
  • Be responsible for calling your backend with an exponential backing off strategy (the effect). This backing off strategy is useful to avoid making unnecessary calls which will hit performance of your app and backend. The high level summary is that subsequent to the first request, another request will be made a second thereafter, 2 seconds thereafter, 4 seconds thereafter…
  • Call an action on success with the request response (commit)
  • Call an action on failure with the context of the request (rollback). The best practice is to undo any UI that was optimistic that the request would succeed and to cancel any loading spinners

This is the example anatomy of the action:

Out of the box React Offline uses the NetInfo API to determine if there’s an internet connection but I found this unreliable so added my own middleware. The unreliability is owing to a shortcoming in the NetInfo API that works off the assumption that if the WiFi or cellular network has an allocated public IP, it’s connected. This assumption falls down when you’re connected to a captive WiFi connection (coffee shop) that’s behind a paywall. The phone reports a WiFi connection but there’s no outbound connection to the internet. Another issue that’s caught us off was when a user has a cellular connection but they’ve exhausted their data plan.

To get around this I added a simple ping to our backend server with a /ping endpoint that’s called every few seconds. It’s useful to create the extra endpoint to ensure the backend response is fast by cutting out the common application stack flow overhead.

Redux Offline exposes an override for network connection status. Here’s a rudimentary implementation:

You get a nice side effect that if your backend was down (obviously never going to happen for you) app clients would just assume it’s offline. If you rely on a mix of third party services, it’s wise just to ping a public service like Google.

State persistence

Original image https://unsplash.com/photos/YjN1l87BUOk

Now you’re gracefully handling your backend requests, your users are seeing the data they expect when they have a stable connection. If their app is closed (crashes or when it’s reset by the OS when it’s not in use) all that data will be reset as it was held in memory. This is frustrating and slow for your users when they re-open the app, especially if they no longer have a connection.

One popular option is to use a persistence database like Realm, which we started off doing but this became a glaring problem. The issue we had was manually synchronising our Redux store with our Realm DB which was non trivial with our deep data structures and the fact Realm is a schema backed DB and Redux is unstructured JSON.

My aspiration was to automatically persist our Redux store update and restore on app boot. While it’s possible to do this yourself, I discovered the battle tested Redux Persist.

The useful parts of Redux Persist are configuring the reducers you wish to persist and what storage backend you use. Here’s a contrived example:

Once you’ve configured this, you just need to wrap your app entry-point with a persist gate that ensures the state is retrieved from storage on app boot before showing any other UI. E.g:

You’ll be able to verify this hotness by fetching all your remote state whilst online, go offline (airplane mode), close the app and re-open to see all the data intact.

Occasionally data can get into a bad state so you’d defensively want to present the user with a way of resetting state in an advanced settings scene, else the user would have to delete & re-install their app.

Resource creation & mutation

Now that you have a request backing off strategy through Redux Offline, it’s going to be probable that resource creation requests (POST) will be called multiple times. This leads to problems with duplicate entries being created on your backend service. Just to be verbose; imagine your backend firing off these requests in close succession:


POST http://yourservice.com/inventory { “name”: “Sink” }
POST http://yourservice.com/inventory { “name”: “Sink” }

Your database will now have 2 sink inventory items.

In this example, you could have an resource check against the name property but in real world applications, it’s more complex than this.

You need to have an idempotent safe way of creating resources, which I tackled by generating a unique ID on the app client (GUID) and using that on the backend to check if the resource has already been created. This means that requests now look like:

POST http://yourservice.com/inventory { “name”: “Sink”, “idempotent_id”: “30dd879c-ee2f-11db-8314–0800200c9a66” }
POST http://yourservice.com/inventory { “name”: “Sink”, “idempotent_id”: “30dd879c-ee2f-11db-8314–0800200c9a66” }

Your backend code will then follow this pattern (Django for terseness):

Advanced topics

Encryption at rest

OnCare is working with sensitive data so we can’t persist in plaintext. Given Redux Persist persists to disk in plaintext, we needed to change this behaviour to encrypt on every update and decrypt on app startup. I achieved this by using a Redux Persist encryption transformer plugin and stored the private key in the device’s private keychain using react-native-sensitive-info. This private key is derived from a salted hash of authentication credentials provided at first login.

Multi-user state persistence

Our app has been designed to allow users to log in as user A, log out, log in as user B and log back in to user A and expect to see all their transient state restored. In our app you can generate a report for a care visit (akin to an email draft) which is stored locally until you publish.

React Persist didn’t support hot-swapping the persistor based on the user (at least at the time I implemented this) so I had to create my own non-trivial solution that catered to the encrypted nature of the store.

The high level of how I’ve implemented this is to make use of store.replaceReducer(persistReducer(persistConfig, newRootReducer)). The app starts with an ephemeral storage backend (in memory) whilst no user is logged in. When a user logs in, a new persistReducer config is created which is keyed against that user. I can elaborate on this if it was useful, just add a comment to express interest.

Gotchas

Authentication

Whilst we have an objective to be offline-first for our app, we still have reliance on a connection to authenticate you. We haven’t committed a great effort into tackling this as we theorise that it happens very infrequently and when it does, it’s likely shortly after downloading the app which you would have required an internet connection for. Assuming the user has authenticated, periodically throughout their usage of the app they will be required to provide a passcode (in future it will be biometric) to ensure it’s not in the wrong hands. Upon success of each prompt, we’ll attempt to extend their session (JWT token) from the backend.

Handling error states

Now your app works offline, your users can create resources offline and connect to your backend in the future which will hit a backend validation error. How do you handle this user experience given they feel they’ve already created the resource, especially if they’re in the middle of doing something else in your app? The best case scenario is that you can’t get into a state where the backend will reject a request as your client business logic is watertight. The reality is you need to cater for it so you need to have some UI that illustrates that a given resource has errors that need to be addressed. This could be as simple as an icon alongside a ListView row. You’d then go through your edit flow to fix the errors.

Illustrate what’s offline

OnCare reports created offline

When your user generated data is valuable, it’s important to let your users know what is waiting to be synced with your backend. We’ve tackled this by adding some UI around the reports users have created as seen above

Asynchronous flows

As you’re making backend calls in the background that could have an effect during any part of the user’s journey, you need to be reactive to changes in your apps state. Redux Thunks are one way to manage this but it doesn’t natively handle interval calls or cancelling operations. We used Redux Sagas as they allow watching Redux actions, dispatching actions on an interval and much much more.

We started by over zealously using Redux Sagas for everything which made it hard to reason with the application flow. We reigned the use of it back to the bare essentials and instead relied more heavily on pure Redux.

If this subject sounds interesting, we have plenty more interesting challenges we are solving and are looking for engineers like you to help us. Our open positions are available at https://angel.co/weareoncare/jobs but feel free to reach out to me directly at james @ weareoncare.com.

--

--

--

CTO @ OnCare

Love podcasts or audiobooks? Learn on the go with our new app.

Recommended from Medium

Functions in JavaScript

React.lazy

Writing JS libraries less than 1TB size

Simple Vue.js Form Validation with Vuelidate

How to move focus in otp box

Features JavaScript (JS)

Easily call a method after a time delay in Godot

Thinking JavaScript

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
James Broad

James Broad

CTO @ OnCare

More from Medium

Best Folder Structure for React Native Project

Reactjs vs React Native: Which one should you consider for your next app?

ReactJS Development Course | AchieversIT

Everything you need to automate React Native apps with CI/CD using App Center.