Implementing Offline Support in React Native Apps

In this article, we'll explore how to implement offline support in React Native apps using Expo, ensuring a smooth user experience regardless of network conditions.

July 19, 20243 min read

In today's mobile-first world, users expect apps to work seamlessly, even offline or with a poor internet connection. In this article, we'll explore how to implement offline support in React Native apps using Expo with AsyncStorage and get to understand what Async Storage is and how we can work with Expo’s Network API

Implementing Offline Support

What is AsyncStorage

Async Storage is a key-value storage system supplied by React Native Expo that allows you to manage local storage in mobile apps. It provides a simple key-value storage system that enables developers to store and retrieve data asynchronously.

Unlike synchronous storage methods, Async Storage allows you to save and retrieve data without interrupting the main thread, resulting in a more seamless user experience.

Image

Try Kodaschool for free

Click below to sign up and get access to free web, android and iOs challenges.

Sign Up


Local Data Storage with AsyncStorage

AsyncStorage is a simple, unencrypted, asynchronous, persistent, key-value storage system that is global to the app.

To use AsyncStorage, you first need to install it using npm

npx expo install @react-native-async-storage/async-storage

After installing you are ready to use AsyncStorage in your React Native apps

Async Storage has different methods used for storing key data values

getItem

Gets a string value for given key. This function can either return a string value for existing key or return null otherwise.

In order to store object value, you need to deserialize it, e.g. using

Example:

getMyStringValue = async () => {
  try {
    return await AsyncStorage.getItem('@key')
  } catch(e) {
    // read error
  }

  console.log('Done.')
}

setItem

Sets a string value for given key. This operation can either modify an existing entry, if it did exist for given key, or add new one otherwise.

In order to store object value, you need to serialize it, e.g. using JSON.stringify().

Example:

setStringValue = async (value) => {
  try {
    await AsyncStorage.setItem('key', value)
  } catch(e) {
    // save error
  }

  console.log('Done.')
}

mergeItem

Merges an existing value stored under key, with new value, assuming both values are stringified JSON.

Example:

const USER_1 = {
  name: 'Tom',
  age: 20,
  traits: {
    hair: 'black',
    eyes: 'blue'
  }
}

const USER_2 = {
  name: 'Sarah',
  age: 21,
  hobby: 'cars',
  traits: {
    eyes: 'green',
  }
}


mergeUsers = async () => {
  try {
    //save first user
    await AsyncStorage.setItem('@MyApp_user', JSON.stringify(USER_1))

    // merge USER_2 into saved USER_1
    await AsyncStorage.mergeItem('@MyApp_user', JSON.stringify(USER_2))

    // read merged item
    const currentUser = await AsyncStorage.getItem('@MyApp_user')

    console.log(currentUser)

    // console.log result:
    // {
    //   name: 'Sarah',
    //   age: 21,
    //   hobby: 'cars',
    //   traits: {
    //     eyes: 'green',
    //     hair: 'black'
    //   }
    // }
  }
}

And many more which you can look at the AsynStorage website below https://docs.expo.dev/develop/user-interface/store-data/

In this article, we will look at getItem and setItem

Network Status Detection

We are going to use expo-network which provides useful information about the device's network such as its IP address, MAC address, and airplane mode status.

Image

To install expo network use:

npx expo install expo-network

After installing you import in your code like this

import * as Network from 'expo-network';

This is how we are using expo network on our demo project


const checkNetworkStatus = async () => {
    const status = await Network.getNetworkStateAsync();
    setYouAreConnected(status.isConnected);
    saveYouHaveLastChecked();
  };

Implementing a Basic Offline-First Architecture

Here's a simple example of how to implement offline-first data fetching:

const loadYouHaveLastChecked = async () => {
    try {
      const value = await AsyncStorage.getItem('@youHave_LastChecked');
      if (value !== null) {
        setYouHaveLastChecked(value);
      }
    } catch (e) {
      console.error("Failed to load youHaveLastChecked", e);
    }
  };

  const saveYouHaveLastChecked = async () => {
    try {
      const now = new Date().toISOString();
      await AsyncStorage.setItem('@youHave_LastChecked', now);
      setYouHaveLastChecked(now);
    } catch (e) {
      console.error("Failed to save youHaveLastChecked", e);
    }
  };

  const checkNetworkStatus = async () => {
    const status = await Network.getNetworkStateAsync();
    setYouAreConnected(status.isConnected);
    saveYouHaveLastChecked();
  };

In our code we use asynchronous functions that execute the last time you checked and saves the current time you last checked the connection

Syncing Data When Back Online

const saveYouHaveLastChecked = async () => {
    try {
      const now = new Date().toISOString();
      await AsyncStorage.setItem('@youHave_LastChecked', now);
      setYouHaveLastChecked(now);
    } catch (e) {
      console.error("Failed to save youHaveLastChecked", e);
    }
  };

In the code we use setItem() to get the current time and save it using AsyncStorage.

Full app code

import * as Network from 'expo-network';

import { Button, Text, View } from 'react-native';
import React, { useEffect, useState } from 'react';

import AsyncStorage from '@react-native-async-storage/async-storage';

const WorkingWithAsyncStorage: React.FC = () => {
  const [youAreConnected, setYouAreConnected] = useState<boolean |undefined| null>(null);
  const [youHaveLastChecked, setYouHaveLastChecked] = useState<string | null>(null);

  useEffect(() => {
    loadYouHaveLastChecked();
    checkNetworkStatus();
  }, []);

  const loadYouHaveLastChecked = async () => {
    try {
      const value = await AsyncStorage.getItem('@youHave_LastChecked');
      if (value !== null) {
        setYouHaveLastChecked(value);
      }
    } catch (e) {
      console.error("Failed to load youHaveLastChecked", e);
    }
  };

  const saveYouHaveLastChecked = async () => {
    try {
      const now = new Date().toISOString();
      await AsyncStorage.setItem('@youHave_LastChecked', now);
      setYouHaveLastChecked(now);
    } catch (e) {
      console.error("Failed to save youHaveLastChecked", e);
    }
  };

  const checkNetworkStatus = async () => {
    const status = await Network.getNetworkStateAsync();
    setYouAreConnected(status.isConnected);
    saveYouHaveLastChecked();
  };

  return (
    <View>
      <Text>Working with AsyncStorage</Text>
      <Text>
        Network Status: {youAreConnected === null ? 'Unknown' : youAreConnected ? 'Connected' : 'Disconnected'}
      </Text>
      {youHaveLastChecked && (
        <Text>Last Checked: {new Date(youHaveLastChecked).toLocaleString()}</Text>
      )}
      <Button title="Check Network Status" onPress={checkNetworkStatus} />
    </View>
  );
};

export default WorkingWithAsyncStorage;
Cliff Gor

About Cliff Gor

As a Fullstack Software Engineer, I design and develop user-centric and robust systems and applications using HTML, CSS, Bootstrap, React Js, React Native, and Kotlin(Android).

More articles like this

View all articles

Continue exploring React Native articles