Building Offline First React Native Apps

Author

Spencer Carli

Developer, cat dad, and devout pizza lover. Teaching at React Native School and building apps with Handlebar Labs.

0

When building a mobile app it’s reasonable to expect that a user will, at some point, use your app without an internet connection. How does your react native app handle that situation? Does it sit on a loading spinner? Show them an error message? Give them a useful experience? Or a fully functional one?

I strive for the “useful experience”. Things might not work 100% the same way but it should still do something. It also means that if someone is using my app and temporarily loses their internet connection they can continue doing what they were doing without any (or at least minimal) negative impact.

Strategy

So how can we accomplish this? Let’s say we’ve got a contact list app. You can enter, edit, remove contacts but the app will also, on the backend, fetch additional information about that contact you just added. This means that your contact can have new information the next time you use the app.

But now let’s think about the offline experience. What if someone is about to go to a meeting and they want to pull up some info about the person they’re about to meet with (such as how long they’ve worked at their current company, thanks LinkedIn) but they don’t have an internet connection?

To alleviate this issue we could have saved the contact list to disk the last time our user used the app. This means that the user will have some data, though it may not be the most up to date, about the person they’re about to meet with. Useful but not necessarily complete experience.

Something else we’ll want to consider is certain actions a user may have gotten used to (adding a contact, searching contacts) should work while offline. Implementing these is beneficial on two fronts: first) the user can take common actions while offline, second) even when they are online you can make their experience “instant” since you’ve got optimistic updates in place.

redux-persist

Note: Using redux-persist assumes you use redux in your app. You could accomplish similar things by using AsyncStorage directly, though it would be much more manual.

I’ve found that once you’re to this point of building your app you’ve probably implemented redux into your app, at least to some extent. Once you’ve done that you can use the excellent package redux-persist to save data to AsyncStorage (or localStorage in a web browser). Not only can you save the data but it also makes it easy to rehydrate your store with that data. Again, this brings two wins.

  1. Offline experience. You can fill the store with the last set of data — it may not be perfect but it’s functional.
  2. Instant loading. You can load the cached data into your store and fetch new data behind the scenes — allowing your user to start using the app instantly rather than waiting a few seconds for new data to load.

So how do we actually implement it?

First of all we need to configure our redux store.

store.js

import { createStore, applyMiddleware } from 'redux';
import reducers from './reducers';

const middleWare = [];

const createStoreWithMiddleware = applyMiddleware(...middleWare)(createStore);

export default createStoreWithMiddleware(reducers);

Now we need to implement redux-persist. We’ll set up both persisting to disk and auto rehydration now.

store.js

import { AsyncStorage } from 'react-native'; // we need to import AsyncStorage to use as a storage enging
import { createStore, applyMiddleware } from 'redux';
import { persistStore, autoRehydrate } from 'redux-persist'; // add new import
import reducers from './reducers';

const middleWare = [];

const createStoreWithMiddleware = applyMiddleware(...middleWare)(createStore);

export default (configureStore = onComplete => {
  const store = autoRehydrate()(createStoreWithMiddleware)(reducers);
  persistStore(store, { storage: AsyncStorage }, onComplete);

  return store;
});

So what’s going on in that code? The interesting pieces start at line 10 where we export a function rather than the store itself. That’s because rehydrating the store is an asynchronous operation — it may take some time to do so we can call this function from our component (will show below) and once it’s been properly rehydrated then we can render our app. On line 11 we’re rehydrating the store with data, if it exists. This will do nothing if nothing if no previous state is found. Finally, on line 12 we’re setting up the logic to persist our store to disk. Of note is that we’re telling the app about AsyncStorage as our storage engine (since you can use redux-persist in different environments).

Now, how do we use this code?

App.js

import React, { Component } from 'react';
import { View, Text } from 'react-native';
import configureStore from './store';

class App extends Component {
  constructor(props) {
    super(props);
    this.state = {
      isLoading: true,
      store: configureStore(() => this.setState({ isLoading: false })),
    };
  }

  render() {
    if (this.state.isLoading) return null;

    return (
      <View>
        <Text>Your app store is loaded!</Text>
      </View>
    );
  }
}

export default App;

After looking over the code above you can see that we’ll initialize and hydrate the store as soon as the app launches. Until the store is successfully hydrated we won’t show anything — don’t worry, I’ve found that hydrating relatively large stores takes minimal time, I’ve never noticed it.

That’s really all you need to do to persist the store to disk, ensuring a user will have at least some useful experience while offline.

Considerations

Persisting your redux store can be really beneficial for both your user and you, the developer. But you do have to take a few things into consideration.

  1. Think about the shape of your data. Once you’re persisting the store to disk changes in how your redux state tree is shaped can cause unexpected issues. You can migrate your store but it’s easier to just take the time and plan ahead so you don’t have to modify it.
  2. Consider how often you’re writing your store to disk. In an app I’ve been working on we get a lot of data back, from different endpoints, in a small amount of time. We found that if writing to disk after each response the user would, in this case, have issues scrolling. The solution? Use the debounce option to only write data every 500 milliseconds. This small change made a big impact on our user’s experience.

Thought Exercise

To wrap this up I wanted to leave you with a question. How would you handle actions taken while offline and sync them with the server the next time user uses the app and is online? Here’s my answer.