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.
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.
Try Kodaschool for free
Click below to sign up and get access to free web, android and iOs challenges.
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.
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;