🎉 New release for Pusher Chatkit - Webhooks! Extend your in-app chat functionality
Hide
Products
chatkit_full-logo

Extensible API for in-app chat

channels_full-logo

Build scalable realtime features

beams_full-logo

Programmatic push notifications

Developers

Docs

Read the docs to learn how to use our products

Tutorials

Explore our tutorials to build apps with Pusher products

Support

Reach out to our support team for help and advice

Sign in
Sign up

Creating offline-friendly React Native apps - Part 1: General tips

  • Wern Ancheta
January 7th, 2019
You will need Node, Yarn and React Native installed on your machine. This tutorial was developed using Node 11.2 and React Native CLI 2.0.1.

Most of the apps listed in the app store today require an internet connection in order to work properly. But the problem is we don’t really know if the user’s device is connected to the internet 24/7. That’s why we need to consider making our apps as offline-friendly as possible. This is to ensure the best possible experience for the user even if they’re not connected to the internet.

In this tutorial, we’re going to explore some of the techniques to make your React Native apps work offline. That’s not to say that internet connection should be considered optional in all apps. For example, when the user is authenticating for a sensitive action within the app. In this case, the user needs to be online in order to authenticate with the server. That way, you can protect their account from the wrong hands.

Specifically, we’re going to talk about the following:

  • General tips on what to avoid and what to do if the user is offline.
  • How to inform the user that they are offline.
  • How to cache resources from the server.
  • How to treat online connectivity as a second-class citizen.

You can view the source code used in this tutorial on its GitHub repo.

Prerequisites

Basic knowledge of React Native is required. We’ll also be using Redux and Redux Saga, so a bit of familiarity with those is helpful as well.

This tutorial assumes that you already have a working React Native development environment. If you’re using Expo, this tutorial also includes a few tips that are specific to Expo users. But for the most part, it assumes you’re developing React Native apps via the “native” way.

For your reference, the following versions are used:

  • Node 11.2.0
  • Yarn 1.7.0
  • React Native CLI 2.0.1
  • React Native 0.57.6

We will be using third-party React Native packages as well. Refer to the package.json file in the GitHub repo if you want to know the specific versions used.

What to avoid when the user is offline

Sometimes, it’s already annoying enough for the user to not have an internet connection. The user might be in the middle of something. They know they had a connection when they opened the app, but suddenly they don’t. They might be in a subway and the connection is choppy, or their prepaid mobile data ran out. We can never really know, that’s why it’s important for the app to be helpful on these occasions.

Here are a few things you need to avoid once when the user goes offline:

  • Pop-ups and overlays saying they don't have a connection.
  • Not showing anything at all. Or just showing a loading animation that spins indefinitely.
  • Only showing errors that they are offline.
  • Blocking the user from what they are doing. Or not allowing them to do anything at all.
  • Clearing out what they have already inputted.

What to do when the user is offline

Now that you know the things you need to avoid when the user is offline, it’s time to look at what you can do instead:

  • When the user goes offline, use a nice graphic to inform them that they went offline. Better yet, use an animation to entertain them if you know they have a connection but the problem is with your server. React Native comes with the NetInfo library to help with this.
  • Cache data coming from your server, especially the ones that they just accessed. This way they’ll still be able to see the most recent data when they go offline. If you’re using Redux, you can use the redux-persist library. This allows you to persist your Redux store using a storage engine of your choice (for example, AsyncStorage) and use the stored data when the user goes offline. The way it works is that if the network request succeeds, we serve the fresh data and persist it with redux-persist. And if a network request fails, we serve the cached copy (rehydrate) instead.
  • Cache resources from the server. For images, you can use the react-native-cached-image library.
  • Use a passcode as an alternative for authenticating the user if they’re offline. You can use the react-native-sensitive-info library to store passcodes and other sensitive data that needs to be available offline.
  • If the user is trying to navigate to a page that simply isn’t accessible without an internet connection, redirect them to the app’s homepage and inform them that they don’t have a connection. This is so users know which app they're in, and they can still navigate to other pages.

In the coming sections, we’ll look at how you can implement a few of the tips mentioned above.

Inform the user that they are offline

In most cases, simply informing when the user goes offline is very useful. This is especially true if the feature they’re trying to use really requires an internet connection for it to be useful. In this section, we’ll look at how you can implement this in your apps.

React Native comes with a library that determines the online status out of the box, it’s called NetInfo. This library allows you to determine various information about the device’s connection type. It tells you whether the device is connected via wifi or a data plan.

To use NetInfo, you first need to update the AndroidManifest.xml file to include the permission to access the network state:

    // android/app/src/main/AndroidManifest.xml
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

Next, at the top of your component class, import the NetInfo library:

    import { NetInfo } from "react-native";

Inside your component class, initialize the state for storing the user’s online status. We’re setting it to false to assume that the device isn’t online by default. Later, we’ll update its value to true using NetInfo:

    state = {
      isConnected: false 
    };

Next, we listen for the connectionChange event. This is triggered whenever there’s a change in the connectivity of the device. Examples of these changes include: turning wifi on or off, enabling airplane mode, switching to a different wifi hotspot, and connecting via a data plan:

    componentDidMount() {
      NetInfo.addEventListener("connectionChange", this.handleConnectionChange);
    }

When the connectionChange event is triggered, the following function is executed. From here, we log the response object so we know the data that we’re working with. After that, we determine if the device is actually connected or not. This returns a boolean value which tells you exactly that, so we can directly use that value to update the state:

    handleConnectionChange = connectionInfo => {
      console.log("connection info: ", connectionInfo);
      NetInfo.isConnected.fetch().then(isConnected => {
        this.setState({ isConnected: isConnected });
      });
    };

Here’s a sample response object contained in the connectionInfo when the device is connected via wifi:

    {
      type: "wifi", 
      effectiveType: "unknown"
    }

And here’s what it contains if the device is offline:

    {
      type: "none", 
      effectiveType: "unknown"
    }

Inside the render method, we simply use the value of the isConnected property in the state to show a different background color and text for the alert box:

    render() {
      const boxClass = this.state.isConnected ? "success" : "danger";
      const boxText = this.state.isConnected ? "online" : "offline";

      return (
        <ScrollView contentContainerStyle={styles.container}>
          <View style={[styles.alertBox, styles[boxClass]]}>
            <Text style={styles.statusText}>you're {boxText}</Text>
          </View>
        </ScrollView>
      );
    }

Lastly, don’t forget to remove the event listener once the component is unmounted. We don’t really want a listener to be hanging around when the user has already moved on to another screen. Especially one where determining the connectivity is no longer needed:

    componentWillUnmount() {
      NetInfo.isConnected.removeEventListener(
        "connectionChange",
        this.handleConnectionChange
      );
    }

NetInfo is great at determining network connectivity, but it doesn’t really determine whether that connection can actually access the internet or not. For example, in an airport, subway, restaurant or a coffee shop where a wi-fi hotspot is behind a paywall. In these cases, NetInfo will report that there’s a connection when in fact there isn’t because the user hasn’t gone through the authentication process, advertisements, or the paywall in order to get access to the internet.

To overcome the limitation mentioned above, you can use fetch to ping any server. If the request fails then you know you for sure that the device isn’t connected to the internet.

Alternatively, you can use the react-native-offline library to easily implement this functionality. First, install it with the command below:

    yarn add react-native-offline

Under the hood, it also uses NetInfo so you also need to have this on your AndroidManifest.xml file:

    // android/app/src/main/AndroidManifest.xml
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

Once installed, all you have to do is import it and export your component like so:

    import { withNetworkConnectivity } from "react-native-offline";

    export default withNetworkConnectivity()(OnlineStatusLib);

This gives you access to the isConnected prop inside your component:

    render() {
      const { isConnected } = this.props;
      const boxClass = isConnected ? "success" : "danger";
      const boxText = isConnected ? "online" : "offline";

      return (
        <View style={[styles.alertBox, styles[boxClass]]}>
          <Text style={styles.statusText}>you're {boxText}</Text>
        </View>
      );
    }

By default, it already pings Google. So even if the user is connected to a network without an internet connection, it will still fail, so isConnected becomes false.

Cache resources from the server

Informing the user that they’re offline is just the first step in creating an offline-friendly app. The second step is caching resources from the server whenever there’s an internet connection. This enables the user to still be able to view some of the content that has been previously loaded while they are offline.

For most apps, the only resource it needs to cache are the images, so we’ll limit our scope to that.

To cache images in React Native, you can use the react-native-cached-image package. You can install it with the following command:

    yarn add react-native-cached-image

Once installed, it gives you access to the following:

  • ImageCacheProvider
  • CachedImage
  • ImageCacheManager

Most of the time, you’ll only be working with ImageCacheProvider and CachedImage. ImageCacheProvider is the top-level component which exposes the caching functionality to the CachedImage component. This component replaces the Image component provided by React Native, it also exposes the same API as the Image component:

    import {
      CachedImage,
      ImageCacheProvider,
      ImageCacheManager
    } from "react-native-cached-image";

Here’s how to use it:

    const { images } = this.props; // images is an array of objects containing the image data
    var urls = images.map(img => { 
      return img.url; // URL of the image (example: https://i.imgur.com/aaJYTRw.jpg)
    });

    return (
      <ImageCacheProvider
        urlsToPreload={urls}
      >
        <View>{this.renderImages(images)}</View>
      </ImageCacheProvider>
    );

Here’s the renderImages function. Note that we’re using the CachedImage component instead of the Image component. In the example below, img.url still contains the original URL, but when the CachedImage component is used, it returns the cached version of the file instead:

    renderImages = images => {
      return images.map(img => {
        const uri = img.url; 
        return (
          <View style={styles.imageContainer} key={img.id}>
            <CachedImage source={{ uri: uri }} style={styles.image} />
          </View>
        );
      });
    };

If you want to manage what’s in the cache, you can use the ImageCacheManager. This allows you to manually download a specific URL into the cache, clear the cache, and get a list of files stored in the cache. Here’s an example of how to get the list of cached files:

    const ImageManager = ImageCacheManager({});

    ImageManager.getCacheInfo().then(res => {
      console.log("files: ", res.files);
    });

Here’s what a single file object looks like:

    {  
       "lastModified":1543325588000,
       "size":196551,
       "type":"file",
       "path":"/data/user/0/com.rnoffline/cache/imagesCacheDir/i_imgur_com_bfc5362b31e0f21ee11ac48c03a3d7e4c231bd27/02f23888f180ea23470dbafd4f87da2b3e80e5d0.jpg",
       "filename":"02f23888f180ea23470dbafd4f87da2b3e80e5d0.jpg"
    }

If you’re using Expo, the equivalent of the package we used above is react-native-expo-image-cache. You can install it with the following command:

    yarn add react-native-expo-image-cache

Once installed, you can use it through the Image component that it provides. This component automatically caches the image which you supply to it, then it uses the cached copy to render the image:

    import { Image, CacheManager } from "react-native-expo-image-cache";

    const uri = "https://i.imgur.com/Ru1qOAB.jpg";
    <Image {...{ uri }} style={styles.image} />

If you want to access the actual path to the cached image, you can use the following code:

    CacheManager.get(uri)
        .getPath()
        .then(path => {
          console.log(path); // sample path: file:///data/user/0/host.exp.exponent/cache/ExperienceData/%2540wernancheta%252FRNOfflineTips/expo-image-cache/edf3678403b4259059e28e4ff650eae40e9128a7.jpg
        });

Treat internet connection as a second-class citizen

The last step is to treat internet connection as a second-class citizen. For general purpose apps, being online should be considered optional. Examples include photo-sharing, project management, and note-taking apps. Users should still be able to use these kinds of apps for their intended purposes even when they’re offline. For example, in a photo-sharing app, users should still be able to select a photo to upload. And once they go online, the app will automatically upload the photo for them.

An exception to this is when your app deals with highly sensitive data, and you need to authenticate the user for most of the actions that they do within the app. If that’s the case, then being online should be a requirement. The same is also true for realtime apps like chat apps, video streaming apps, and multi-player games. The whole idea of those apps is being connected to the internet to perform their intended tasks.

To give you an example of how to implement this in React Native, we’ll be using the redux-persist library. We’ll add it to a pre-coded app so it locally persists data which was fetched from a server. We then display this persisted data when the user goes offline.

First, clone the project repo and switch to the starter branch:

    git clone https://github.com/anchetaWern/RNOffline.git
    cd RNOffline
    git checkout starter
    yarn install

Next, install redux-persist:

    yarn add redux-persist

The app that you just cloned makes a request to the PokeAPI to get the data of a random Pokemon. Currently, it simply returns an error if the user is offline when they trigger a fetch. We want to update this so that it lets the user load the data of the last Pokemon that was loaded instead.

Open the src/screens/RehydrateScreen.js file and import the redux-persist components:

    import { persistStore, persistReducer } from "redux-persist";
    import storage from "redux-persist/lib/storage";
    import { PersistGate } from "redux-persist/integration/react";

Here’s what each component does:

  • persistStore - for persisting the store.
  • persistReducer - for enhancing the reducer so it works with redux-persist. This allows you to specify options like the storage to use, state reconciler, and whitelist or blacklist specific data from being persisted.
  • storage - where redux-persist will store your Redux store. By default, this uses AsyncStorage.
  • PersistGate - delays UI rendering until the persisted data has been retrieved from local storage and loaded into the store.

To use redux-persist, call persistReducer and specify the options and the reducer that you want to use. The sample app that we’re working with already uses Redux, so all we have to do is supply the reducer that it already uses:

    // src/screens/RehydrateScreen.js
    const persistConfig = {
      key: "root", // name of the key for storing the data
      storage // storage to use. defaults to AsyncStorage
    };

    const persistedReducer = persistReducer(persistConfig, reducer);
    const store = createStore(persistedReducer, applyMiddleware(sagaMiddleware));
    let persistor = persistStore(store);

Next, in your render method, you can supply your store like usual. But this time, you can optionally use the PersistGate component. This allows you to specify the component to display while redux-persist is retrieving the data from the local storage. In this case, we simply specified null because we don’t really have much data to retrieve so it loads almost instantly:

    // src/screens/RehydrateScreen.js
    render() {
      return (
        <Provider store={store}>
          <PersistGate loading={null} persistor={persistor}>
            <PokemonLoader />
          </PersistGate>
        </Provider>
      );
    }

Next, update the PokemonLoader component to include an additional message and a button to allow the user to retrieve the last Pokemon that was loaded before they went offline:

    // src/components/PokemonLoader.js
    {error && (
      <View>
        <View style={styles.smallTextContainer}>
          <Text style={styles.errorText}>Something went wrong</Text>
          <Text>Would you like to view the last Pokemon instead?</Text>
        </View>
        <Button onPress={this.rehydrate} title="Yes" color="#841584" />
      </View>
    )}

When the button is clicked, we execute a method which dispatches the action for restoring the data of the previous Pokemon:

    rehydrate = () => {
      this.props.requestPersistedPokemon();
    };

Update mapDispatchToProps to dispatch the action:

    const mapDispatchToProps = dispatch => {
      return {
        // the rest of the action creators here... 
        requestPersistedPokemon: () => dispatch({ type: "API_CALL_RESTORE" }) // add this
      };
    };

Lastly, update the reducer to handle the new action type API_CALL_RESTORE. This removes the error from the state. This way, the last Pokemon that was loaded via API_CALL_SUCCESS is restored. The important thing to note here is that we’re not clearing out the pokemon data when API_CALL_FAILURE is called. This way, the last Pokemon is always persisted in local storage even if an error occurs:

    // src/redux/index.js

    // the rest of your action types here..
    const API_CALL_RESTORE = "API_CALL_RESTORE";

    export function reducer(state = initialState, action) {
      switch (action.type) {
        // the rest of the reducers here...

        case API_CALL_SUCCESS:
          return { ...state, fetching: false, pokemon: action.pokemon };
        case API_CALL_FAILURE:
          return { ...state, fetching: false, error: action.error };

        // add this  
        case API_CALL_RESTORE:
          return { ...state, fetching: false, error: null };
        default:
          return state;
      }
    }

Conclusion

That’s it! In this tutorial, you learned some tips on how you can make React Native apps offline-friendly. Specifically, you learned how to inform the user if they go offline, caching images from the server, and persisting data locally so it can be accessed when the user goes offline.

Stay tuned for part two where you will learn how to apply the concepts we’ve learned in a real-world scenario.

You can find the source code used in this tutorial on its GitHub repo.

Clone the project repository
  • Android
  • iOS
  • JavaScript
  • React Native
  • Chatkit

Products

  • Channels
  • Chatkit
  • Beams

© 2019 Pusher Ltd. All rights reserved.

Pusher Limited is a company registered in England and Wales (No. 07489873) whose registered office is at 160 Old Street, London, EC1V 9BW.