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 synchronisation concept for offline apps.
Data synchronisation is a continuous process to ensure the data is consistent across all devices. We will look into the concept in detail in this article.
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:
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 authorisation handling is not a hassle.
Normally, auth token & refresh token are used for authorisation. 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 authorised, you can hit the refresh_token
API to get authorisation again (in case refresh_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 authorisation, requests can be dispatched to the server for data syncing.
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:
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:
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.
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:
sync_queue
and at the same time it would be persisted. We will also whitelist the reviews list in redux-persist.sync_queue
one by one. For a detailed implementation of queue that supports dispatching jobs in background too, check this resource.sync_queue
will be removed from the queue.Say the user wants to see upcoming movies in the app. Let's now look at all the steps in the flow:
{
"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.
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 recognises 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.
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.