Creating an offline-first React Native app
There are many reasons to consider building an offline-first React Native app. You’ll need to consider doing so even before building your app because if you wait until your app gets critical reviews because it is slow or unresponsive, it could be too late.
But offline-first is not a one-size-fits-all solution. There are several ways to implement offline-first and, depending on the structure of your app, some methods will work without having to change the app’s architecture, while others won’t. In this article, we’ll cover five ways to implement an offline-first app in React Native.
What is offline-first?
Offline-first means that you build your React Native app so that it can work with or without an Internet connection. You can do this in various ways, but let’s look at why we would want to make use of an offline-first app.
The most obvious reason is that your app may be accessed in locations that have intermittent or no cellular service, which is why I started building an offline-first app in the first place. My app was receiving negative reviews because of a lost connection. Implementing offline-first would fix this issue, but there is another reason to use it: saving data locally is simply faster.
No matter what type of mobile connection you have, there will be latency that will affect the user experience. By saving data locally first, then syncing that data to services in the background, loading indicators spend less time in the UI and users can get to their next task faster.
Using offline-first in your mobile app means users get the same responsive UI whether they have a 3G, 4G, 5G, or no connection at all.
Implementing offline-first methods
Implementing offline-first in an existing app isn’t easy, but until I started digging deeper into it, I didn’t realize that there were so many options, even more than I list here because everyone creates a React Native app slightly differently.
This article started as research for implementing offline-first in an existing React Native app. If you are starting an app from scratch, any of these options may work for you. If you’re using an existing app, you may have to make some changes.
Using react-native-offline and Redux for local-first functionality
The first two methods we’ll cover both use Redux to handle local-first functionality. This makes sense if all the parts of your app use Redux and it connects to remote services to save data. You won’t have to write synchronization methods or ensure that your local database is the same type as your remote database.
The react-native-offline package is a Swiss Army knife of tools specifically designed for React Native apps. We want an app that will do everything offline that it can do online, as well as sync changes when it finds a connection.
To start with react-native-offline, add the network reducer that react-native-offline provides to your root reducer:
import { createStore, combineReducers } from 'redux';
import { reducer as network } from 'react-native-offline';
const rootReducer = combineReducers({
// ... your other reducers here ...
network,
});
const store = createStore(rootReducer);
export default store;
Now you have two options. You can use the ReduxNetworkProvider
as a descendant of your Redux provider so that it can access the store, like so:
import store from './reduxStore';
import React from 'react';
import { Provider } from 'react-redux';
import { ReduxNetworkProvider } from 'react-native-offline';
const Root = () => (
<Provider store={store}>
<ReduxNetworkProvider>
<App />
</ReduxNetworkProvider>
</Provider>
);
The other option is to fork the networkSaga
from your root saga
if your app uses Redux sagas. This method works without having to wrap your components with extra functionality.
import { all } from 'redux-saga/effects';
import saga1 from './saga1';
import { networkSaga } from 'react-native-offline';
export default function* rootSaga(): Generator<*, *, *> {
yield all([
fork(saga1),
fork(networkSaga, { pingInterval: 30000 }),
]);
}
Then, through the use of the Redux middleware that react-native-offline provides, you can determine if the app is online before you make an API call. If it is online, everything functions as you would expect.
If the app is offline, the action that was dispatched gets stored in a queue where it can be re-dispatched once the app is back online, which will be enough if your phone only goes offline when the app is running.
To have offline-first functionality when the app is launched and there is no connection, you will need to add redux-persist to your app. This enables it to store a snapshot of the state of your app to the device’s memory and rehydrate the state when the app is launched.
Enabling offline-first with redux-offline
The npm redux-offline package is similar to react-native-offline because it uses Redux to handle the online versus offline functionality, but does it slightly differently. First, you add the redux-offline store enhancer to your root reducer.
import { applyMiddleware, createStore, compose } from 'redux';
import { offline } from '@redux-offline/redux-offline';
import offlineConfig from '@redux-offline/redux-offline/lib/defaults';
const store = createStore(
reducer,
compose(
applyMiddleware(middleware),
offline(offlineConfig)
)
);
Next, decorate your Redux actions with offline metadata:
const saveData = data => ({
type: 'SAVE_DATA',
payload: { data },
meta: {
offline: {
// the network action to execute:
effect: { url: '/api/save-data', method: 'POST', json: { data } },
// action to dispatch when effect succeeds:
commit: { type: 'SAVE_DATA_COMMIT', meta: { data } },
// action to dispatch if network action fails permanently:
rollback: { type: 'SAVE_DATA_ROLLBACK', meta: { data } }
}
}
});
If an API call completes, the commit action will be called. If the app is not online, redux-offline will wait until the connection is restored and retry saving any data that has not been committed.
If the action fails permanently, then the rollback action will be called which will rollback the application state to what is was before the action was called. Here you can specify an action to call when this occurs. For example, notifying the user that their changes have not been saved and they will have to try again once they have network connection.
Redux-offline uses redux-persist by default, so you don’t have to worry about writing your implementation of it like you do with react-native-offline. But redux-offline also uses the unreliable NetInfo API to check the connection. The NetInfo API assumes that if you have a public IP address, you are connected to the Internet, which is not always the case because the app could lose connection after receiving an IP address. You may want to replace the detectNetwork
method in reduxOfflineConfig
with a method to ping your own backend to check the connection.
Using WatermelonDB for complex, offline-first React Native apps
If your React Native app is simple and you use remote services to store your data in a database and Redux throughout the app, then either of the two npm packages above will work for you.
But for apps that are data-intensive, those methods could slow down your app. Using Redux where it’s unnecessary is enough to create slower loading times, and both of those packages require Redux for handling any data both online and offline.
For more complex apps that are backed by a SQL database, WatermelonDB is a good option. With WatermelonDB, all data is saved and accessed locally in an SQLite database using a separate native thread. Watermelon is also lazy. It only loads data when it is needed, so queries are resolved quickly.
WatermelonDB is just a local database, but it also provides sync primitives and sync adaptors you can use to sync local data to your remote database. To use WatermelonDB to sync your data, you need to create two API endpoints on your backend — one for pushing changes and one for pulling changes. You will also have to create your own logic to determine when to sync this data. For more information on how this works, check out how to use WatermelonDB for offline data sync.
Using MongoDB Realm for data-intensive apps
If your data-intensive app uses non-relational data, then WatermelonDB may not be the best solution. MongoDB Realm might be a better solution. Realm database is a local NoSQL database you can use in your React Native app. It can be integrated with MongoDB Atlas.
If you choose to use Realm, then creating a React Native app is relatively simple using the MongoDB Realm React Native SDK. Realm has built-in user management that allows user creation and authentication across devices using a variety of authentication providers, including email/password, JWT, Facebook, Google, and Apple. If you choose to sync your data to a MongoDB that is hosted in the cloud, that feature is also built into the SDK.
SQLite and cloud storage
This is a good option for side projects and hobby apps. It is also a simple way to prototype a React Native app. It is a very basic concept: simply store data locally using SQLite, then use a cloud service like Dropbox to sync the database to the cloud.
For the SQLite piece of the puzzle, you will want to use the react-native-sqlite-storage npm package. For more details on this, check out this article on using SQLite with React Native.
The next step is to add DropBox or another cloud storage provider. You will have to go to the Dropbox developers page to create your app. Then you will need to create both a method to authorize DropBox and a method to sync the database file.
Conclusion
Offline-first can change the way users react to your React Native app, especially if it is used in the field where network connections can be spotty. But there is no simple one-size-fits-all solution.
I didn’t even cover all potential solutions in this article. If you are starting from scratch, you can use any of these options. With an existing app, however, it is likely you will have to make some architectural changes unless you already use Redux to handle all of your state. Happy coding!
Originally published on LogRocket: Creating An Offline-First React Native App