As app developers, we always seek to eliminate as many inconveniences on the user's side as we can. One such inconvenience can be losing Internet connection in the midst of using an app. To avoid that, we must design for offline mode - a win-win for both product and user.

In this article, we will go through strategies that serve as a base for designing offline applications in React Native. We will also examine some libraries, their complexities, and edge-cases to achieve offline mode.

Most mobile apps consume textual/JSON data because it’s really easy to store, having multiple support technologies i.e. Realm, SQLite, Redux, etc. We can cache the plain data as well, but invalidation of cached data becomes an issue. In some cases, we can overwrite the already-cached data.

Mobile applications can be divided into two big categories based on data being consumed or generated by users.

If we take Weather app for instance, it shows almost all data from the server and displays it in the mobile app. In case there is not internet cached, data can be displayed. Also, once the app gets updated data from the server, the app will delete previously-cached data and save new data from the server.

Let’s take another case: Skype. Here, the user generates data continuously, and it keeps taking messages despite the Internet not working. As soon as it connects to the internet, the app will upload all data to the server - this is essentially the data synchronization concept for offline apps.  

Data synchronization is a continuous process to ensure the data is consistent across all devices. We will look into the concept in detail in this article.

Internet not working?

Let’s discuss how to develop a mobile app if the internet is not working.

1) First, the UI/UX should reflect the fact that we are in offline mode so we must accommodate a banner/message to indicate the internet is not working. If some features are not available in offline mode, the screen should display some visual cues to show this feature is not available in offline mode.

2) Then, at the data level, the features that can be handled offline have to be synced with the server. When internet is not working, the app should either do the server's job on a particular request or queue it until the app is back online.

To get this right, a queue should be maintained for all API requests, for executing requests when the internet connection will work.

The queue can have objects with basic information of API data like URL, params, body. Whenever the internet is connected, API requests from the queue will be dispatched one by one and data will be synced between the server and the mobile app.

The array of jobs can be whitelisted in redux-persist to store on local storage. You can maintain a queue manager class that would be responsible for adding, removing & dispatching jobs.

These are possible actions taken on saved jobs:

  1. Queue Manger will read all store jobs on app launch.
  2. It will dispatch a job in the background if internet is working.
  3. If the job is completed successfully, Queue Manager will remove that stored job.
  4. If the job fails, Queue Manager will not remove it from stored jobs and will try to dispatch it again.
  5. This loop keeps going until all jobs are dispatched successfully.

For a detailed implementation of Queue Manager, I suggest you check this link.

As we are queueing API requests when the internet is not working, we need to make sure authorization handling is not a hassle.

Normally, auth token & refresh token are used for authorization. When the token is not refreshed at a specific time, it expires. After that, if the app connects back to the internet and the queue starts dispatching requests, the server will display a 'not authorized' error.

When the app is not authorized, you can hit the refresh_token API to get authorization again (in case referesh_token is expired too). The app flow can be moved to the login screen or it should show the message 'login back to sync data'. After authorization, requests can be dispatched to the server for data syncing.

Data Sync

Syncing is a concept we use to make sure that both the local app and the server’s data are the same at all times. Both have the same data version whenever both are connected to the internet.

It's always useful to observe some edge cases that help us implement a smoother user experience.

Let’s take a movie ticketing app. The app will show all upcoming movies and their screening date in the cinema. Normally, once a movie schedule is planned, it remains intact, with no change in it.

In syncing we will have to handle these scenarios:

  • New movies added on the server in the upcoming list
  • Deleting from the device those movies that are removed from the server
  • If some movie information is updated, syncing the info locally i.e. name changed, etc.
  • Some extra information (data nodes) are added with the movie i.e. added cast info etc.

There must be sync criteria decided between local app data and server data.

One of the key strategies to sync data is to save a timestamp on local app data when the most recent server data is synced. This timestamp will be sent to the server and if the server has updated data after the last synced time, it will be returned.

Find more about syncing in this great talk about about CRDTs (Conflict-free Replicated Data Type) here:

Architecture in React Native

To implement an architecture that checks all the concepts we discussed above, we need some help from libraries like Redux-Persist and NetInfo.

Redux Persist is a JavaScript library for persisting and rehydrating data. In React Native terms, Asyncstorage is a key-value-based, unencrypted, asynchronous storage system that is global and can be used as the local storage with redux-persist for the app.

We will use the redux-persist library for data persistent storage. Now whenever the app starts, it loads all stored data in redux states again. We will focus more on concepts, how all aspects of offline mode can be tackled with redux-persist.

Redux-persist is set up on application-level, similar to the way redux store is set up: it wraps the whole application with redux-store layer. Likewise, the redux-persist configuration is used with redux on the app level. PersistReducer wraps the root reducer, which ensures your redux state is saved to persisted storage whenever it changes.

NetInfo library can be used for detecting internet connection.

Listeners can be implemented with Netinfo, while internet status can be stored in redux, so that it’s accessible on all components and we can do UI rendering based on the current state of our internet connection.

Post Data Request Flow

Let’s say the user wants to post a review of the movie in offline mode. We have to execute the flow step-by-step for clear understanding:

  1. A user wrote a review and hit the post button.
  2. The app would dispatch a redux action to post a review.
  3. The request would come to the reducer layer, where it checks whether the internet is working.
  4. If the internet is not working, the request data will be stored in sync_queue and at the same time it would be persisted. We will also whitelist the reviews list in redux-persist.
  5. Whenever the internet reconnects, the app will dispatch the jobs from sync_queue one by one. For a detailed implementation of queue that supports dispatching jobs in background too, check this resource.
  6. Upon API success, the job from sync_queue will be removed from the queue.
  7. This loop keeps rolling like this.

Fetch Data Request Flow

Say the user wants to see upcoming movies in the app. Let's now look at all the steps in the flow:

  1. If the internet is not working, the movies list will be populated from locally-saved data using redux-persist.
  2. In case the internet is working, a timestamp from the app will be sent in the fetch request.
  3. The server will send new data that is not synced with the mobile app to the server, the timestamp will be a checkpoint between the server and the mobile app for the last data syncing.
  4. New records from the server will be appended in local data, to persist for the future.
  5. It’s up to the developer to decide the structure of API for smooth local operation.
  6. If some data is to be deleted from the movies list stored locally, the server will send IDs of records to delete:
{ "movies" : [ { "id" : 3, "name" : "Avengers" }], "updates": [{ "id" : 1, "name" : "Avengers" }], "toDelete" : [1,2,3]
}

From locally stored movies, these specific IDs will be removed.

7. If records are to be updated in local data, the server will send the IDs of the updated records and updated objects array, that record will be replaced in the local app:

{ "movies" : [ { "id" : 3, "name" : "Avengers" }], "updates": [{ "id" : 1, "name" : "Avengers" }], "toDelete" : [1,2,3]
}

From locally stored movies, these specific records will be replaced.

Migration

Let's say we take a movie list on a mobile app that shows only movie name, genre, and description.

Data will be persisted with the old movie object structure. In case the movie object is changed on the server, the mobile app will crash. We want to avoid this scenario, the version number should be handled with redux-persist, whenever there is a structural change, the version number will be incremented.

We can specify the version in the redux-persist config. If no version is specified, the library recognizes reducer as minus 1 version. The key is to configure your persist configuration in your rootReducer.

For detailed implementation of migration with redux-persist, I recommend checking this resource.

Conclusion

We have discussed all scenarios of handling data locally, as well as Redux and Redux-Persist that help a lot with developing the offline mode of the mobile app.

I hope my article helped you develop an offline-supported mobile app in React Native and hopefully in the future we'll see offline-first apps being the norm.

Learned something new here? Maybe you want to learn more stuff by subscribing to the Around25 newsletter (check below). No spammy content, just solid tech bites and tutorials from our side.