
Caching API Data In Your React Native Android App Using AsyncStorage
If you’re planning an app that will consume data from an external API, chances are you’ll want to cache some of that data on the user’s device – at least temporarily. One of the main reasons to do this is to cut down on extra network requests to your API and to provide a degree of offline support for your users.
There are a few ways to go about implementing this kind of data caching, but the one I’m going to go over today will utilize React Native’s AsyncStorage system.
What is AsyncStorage?
AsyncStorage is simply a way to store data on the user’s device using key-value pairs. You can use the methods setItem()
, getItem()
, removeItem()
and so on, it’s a very simple and intuitive system to use.
As the name would suggest, AsyncStorage is just that — asynchronous. There are some caveats to this for those that aren’t familiar. In a nutshell, you may need to await the function calls (due to their asynchronous nature) or you may receive some unpredictable results.
Stay tuned for an article delving deeper into asynchronous programming. But for now, this Mozilla article on asynchronous programming concepts should provide a good introduction for those who are unfamiliar.
So what do you mean by “caching” data?
Lets take the example of a blog or news app:
Once the user opens up your app, you may pull down 40 of the most recent blog posts directly from your API service. That’s great. But when the user closes down your app only to return to it again 30 minutes later, there will be a subsequent request that will fetch those same posts once more.
We don’t necessarily want to download these 40 blog posts again in this case. They should be retained in the app as the data is relatively fresh and another network request is most likely not necessary.
This is what is meant by “caching your data”. In this case: just storing data (sensibly) on the user’s device to make your app more efficient and to avoid needless operations where appropriate.
How long do I need to cache the data for?
This really depends on the specific implementation and the type of app you are dealing with. How important is it for the user to receive to-the-minute content and updates? If it’s mission critical, then you’ll want to keep this caching threshold to the bare minimum.
If your API is only updated once or twice a week, you could cache the downloaded data for 24 hours. It’s really down to your own judgement and what you feel is best in each given scenario.
A practical example – caching API data in its most simplistic form
Here’s a simple example, where we simply store the results of an API request via AsyncStorage:
fetch(`${apiUrl}/blogPosts/recent`)
.then(async (response) => {
return await response.json()
})
.then(async (json) => {
if (!json || json.length == 0) {
throw new Error()
}
return await AsyncStorage.setItem('blogPosts', JSON.stringify(json))
})
.catch(error => {
console.error(error)
})
So as you can see, we are using the fetch API to grab our data (but any library will do). Upon success, we store this data on the user’s device using AsyncStorage.
Note: there are many other mechanisms outside of AsyncStorage to store data locally to the user’s device! Think of AsyncStorage as basically the most simplistic approach, hence why it’s a good reason to demonstrate this kind of primitive caching.
Now, next time we would ordinarily execute this network request, we can simply check if the data already exists on the user’s device first:
let blogPosts = await AsyncStorage.getItem('blogPosts')
if (blogPosts == null) {
// Fetch the blog posts
// blogPosts = …
}
If the data does exist, we don’t need to make the API call again, we can rely solely on the stored (cached) data.
If the data doesn’t exist yet, then it’s clear that we need to make our request in this case.
This is great, and it works fine, but as you can see — once the blog posts have been initially populated, we won’t make any subsequent requests and as such, our users won’t receive any new data! That’s obviously not what we want.
So what do we do about that?
Caching the data per each session
The first approach would be to simply cache the retrieved data for only one session at a time. By session, I’m simply referring to the time between the user opening your app and then closing it down again.
So whenever the user opens up your app — they’ll make a single network request to retrieve the API data and store it, as above. It’ll be retained on the device for the duration of the session — or until the app is closed.
Once the user reopens the app, the cached data would be replaced or overridden by a new batch of “fresh” data upon another (successful) call to the API.
This works fine, but what if you want to cache your data for more than one session at a time?
Caching the data for a specific period of time
We can adapt our approach to become a little more sophisticated.
We can take the same kind of approach – but also store a timestamp of when the last successful call to our API took place. Here’s an adapted version of the previous example:
const cacheIntervaInHours = 24
const cacheExpiryTime = new Date()
cacheExpiryTime.setHours(cacheExpiryTime.getHours() + cacheIntervalInHours)
const lastRequest = await AsyncStorage.getItem("lastRequest")
if (lastRequest == null || lastRequest > cacheExpiryTime) {
fetch(`${apiUrl}/blogPosts/recent`)
.then(async (response) => {
return await response.json()
})
.then(async (json) => {
if (!json || json.length == 0) {
throw new Error()
}
AsyncStorage.setItem("lastRequest", new Date());
return await AsyncStorage.setItem('blogPosts', JSON.stringify(json))
})
.catch(error => {
console.error(error)
})
}
Now, whenever the user opens the app up again, or whenever another network call such as this initial one is due to take place — we can compare the current timestamp with the stored timestamp. If we’ve surpassed the expected time (stored timestamp + threshold
), we know that we need to make another request and fetch some fresh data.
If the API data was received and stored on the user’s device fairly recently (so less than the threshold we have defined) – we can use the cached data directly, instead.
Simple, right?
That’s basically a simplified version of caching that you can implement yourself right away. If you need to also sync data with your backend service or only cache/retrieve partial data, you’ll need a more complex approach. We’ll briefly cover that at the end of the article.
Other approaches to caching data
I find this approach (using AsyncStorage) useful to handle this type of caching and data storage in a simplistic and predictable manner. If you’re working with a small data-set, and it’s not really relational by nature, I’d definitely recommend this approach.
Of course, there are more sophisticated means to go about caching in general (and many React Native Caching modules like this one) – but this rudimentary method may be all you require to get started.
If you’re handling relational data…
You could also consider using SQLite via a module such as react-native-sqlite-storage.
This is particularly suitable if your data contains relations, as you’ll be able to access and work with the cached data as if you were using a regular relational database.
If you’ve ever worked with a relational database before; you’ll understand that there are many benefits to this type of approach. As such, you’ll definitely want to opt for SQLite over AsyncStorage if you’re working with complex and/or larger data-sets!
More refined, complex approaches
The purpose of this article isn’t to cover all of the available caching mechanisms in an exhaustive fashion, but it’s worth pointing our the “next” stage of caching solutions for your consideration.
The two main proponents in this regard would be services like Google’s Firebase and Realm with MongoDB. These both offer facilities related to working with your app’s data store (amongst other things). This involves the caching/syncing functionalities that you may required, too.
In short, it’s well worth a read through the documentation in both cases to see if you’d benefit from either solution.