
Reduce the Number of Calls to your Backend by doing this:
Or, how you can reduce computational costs and improve UX
We are building a React Native app. The primary data that we show in the app updates approximately once a day, but users log in more than once.
How can we reduce the calls to our backend without losing the latest updates?
Most apps would do the following:
- users open the app
- we fetch an endpoint to retrieve the data we need
- meanwhile, we display the loading state
- and after we get our data, we render it
- users close the app
- users open the app … go to step 2
Our approach has more steps in between:
- apart from the data, we receive the version of the data snapshot
- we save it and the data to our Local Store
- on the second open, we render the data saved in the store
- we fetch an endpoint that will tell if our snapshot version is the latest
- if yes, we are fine
- if not, it will retrieve a new dataset with a new snapshot version
Let’s visualize the flow

Let’s see code examples
On new updates in the database, let’s trigger a function that will create a snapshot version and aggregate data in JSON format:
import { sha1 } from "object-hash"
const writeToRedis = async () => {
const freshData = await getFreshData()
const snapshot = sha1(freshData)
await redis.setObject("data", freshData)
await redis.set("shapshot", snapshot)
}
Note: redis.setObject is a custom method of the extended Redis class, see here.
And our /get handler:
const get = async (req, res) => {
const snapshot = await redis.get("shapshot")
if (req.query.snapshot === shapshot) {
res.json({ hasUpdates: false })
return
}
const data = await redis.getObject("data")
res.json({
hasUpdates: true,
snapshot,
data
})
}
In the app, we can put all our logic into a custom hook:
import { storage } from "@/lib/storage"
import { useMMKVObject, useMMKVString } from "react-native-mmkv"
import { useDeepCompareEffect } from "use-deep-compare"
const useData = () => {
// read from the local storate
const [snapshot, setSnapshot] = useMMKVString("snapshot", storage)
const [storedData, setStoredData] = useMMKVObject("data", storage)
// fetch our /get endpoint
const { data, error, loading } = useApi(...)
// save to storage after successful fetch
useDeepCompareEffect(() => {
if (!loading && !error && data) {
// replace only if the version has changed
if (!data.hasUpdates) return
setStoredData(data.data)
setSnapshot(data.snapshot)
}
}, [data, error, loading])
// return data
const dataToReturn = (() => {
// if the response has updates, let's return the data from it
if (data?.hasUpdates && data?.data) return data.data
// if not, let's return the storedData or at least an empty array
return storedData ?? []
})()
return { data: dataToReturn, loading, error }
}
const MyList = () => {
const { data } = useData()
return <List options={data} />
}
Let me know if you want to see a working example, so I could prepare a GitHub repo.
Improvements you may want/need
- We have also separated the logic of the /get function into 2 functions. We redirect if we need to return the fresh data.
const get = async (req, res) => {
const snapshot = await redis.get("shapshot")
if (req.query.snapshot === shapshot) {
res.json({ hasUpdates: false })
return
}
res.redirect("/api/data/realGet")
}
- if you want to have a forced pull of the fresh data in your app/site, just add a button that will trigger the same endpoint but with &force=true query param.
const get = async (req, res) => {
const snapshot = await redis.get("shapshot")
if (req.query.force) {
res.redirect("/api/data/realGet")
return
}
if (req.query.snapshot === shapshot) {
res.json({ hasUpdates: false })
return
}
res.redirect("/api/data/realGet")
}
Note: this is a conceptual example. In the real one, the query values are parsed using zod library.