Caching with Expo FileSystem: A Practical Guide to Local Storage on iOS and Android
Learn how to cache data locally in Expo apps using the FileSystem API. The guide covers directory choices, async I/O, permissions, a full code example, and trade‑offs for iOS and Android.
18 Sept 2026, 23:46 UTC

Concrete Problem: Why Local Caching Matters
When building a mobile app with Expo, you often fetch data from a remote API and display it immediately. Users expect instant feedback, even when the network is slow or unavailable. A common solution is to cache the response on the device, so the app can read the data from disk on the next launch. The Expo FileSystem API gives you a cross‑platform way to write, read, and delete files, but there are subtle differences between iOS and Android that can trip up developers.
Thesis: Use Expo FileSystem’s Directory Constants and Async Methods for Reliable Caching
The key to robust caching is: choose the right directory, perform all I/O asynchronously, and handle OS‑specific permissions and purging rules. By following these guidelines you can avoid runtime crashes, keep the UI responsive, and respect platform storage policies.
1. Directory Constants: DocumentDirectory vs CacheDirectory
Expo exposes two predefined paths you can use:
FileSystem.documentDirectory– a persistent, user‑visible folder that is backed up by iCloud (iOS) or the app’s internal storage (Android). Data here is never deleted by the OS.FileSystem.cacheDirectory– a temporary folder meant for cache data. The system may delete files in this directory when storage is low.
Here’s a quick table that summarizes the differences:
| Directory | Typical Use | OS Purge Policy | Backup |
|---|---|---|---|
| DocumentDirectory | Config, user data, files you want to keep | Never purged by OS | Yes (iCloud, Google Drive) |
| CacheDirectory | Images, JSON, temp files | May be purged under storage pressure | No |
For caching API responses that can be regenerated, cacheDirectory is usually the right choice. If the data is user‑generated or essential, store it in documentDirectory instead.
2. Async I/O: Keep the UI Thread Free
FileSystem methods are asynchronous and return Promises. Blocking the JavaScript thread with synchronous reads can freeze the UI, especially on older devices. Always use the async versions:
await FileSystem.writeAsStringAsync(path, data, {encoding: FileSystem.EncodingType.UTF8});
const content = await FileSystem.readAsStringAsync(path, {encoding: FileSystem.EncodingType.UTF8});
When writing large blobs (>1 MB), consider FileSystem.createDownloadResumable or writing in chunks to avoid ANRs (Application Not Responding) on Android.
3. Permissions & OS‑Specific Quirks
From SDK 48 onward, Expo auto‑links the native module, so you only need to install the package:
expo install expo-file-system
Older projects (pre‑SDK 45) must manually link the module or run expo install expo-file-system and then npx pod-install for iOS.
Android API 30+ uses scoped storage. You no longer need WRITE_EXTERNAL_STORAGE to write to cacheDirectory. For API 29 and below, add the permission to app.json:
{
"expo": {
"android": {
"permissions": ["WRITE_EXTERNAL_STORAGE"]
}
}
}
Always request runtime permission on Android 6.0+ using expo-permissions if you target API 29 or lower.
4. Worked Example: Cache a JSON Response
Below is a minimal, self‑contained example that fetches a JSON payload, caches it, and reads it on subsequent launches. Replace <YOUR_API_URL> with your endpoint.
import React, { useEffect, useState } from 'react';
import { Text, View, Button } from 'react-native';
import * as FileSystem from 'expo-file-system';
const CACHE_PATH = FileSystem.cacheDirectory + 'api-cache.json';
export default function App() {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const fetchAndCache = async () => {
try {
const response = await fetch('<YOUR_API_URL>');
const json = await response.json();
await FileSystem.writeAsStringAsync(CACHE_PATH, JSON.stringify(json), {
encoding: FileSystem.EncodingType.UTF8,
});
setData(json);
} catch (e) {
console.warn('Network error, falling back to cache', e);
const cached = await FileSystem.readAsStringAsync(CACHE_PATH, {
encoding: FileSystem.EncodingType.UTF8,
});
setData(JSON.parse(cached));
} finally {
setLoading(false);
}
};
useEffect(() => {
// On mount, try to read cache first
const loadCache = async () => {
try {
const cached = await FileSystem.readAsStringAsync(CACHE_PATH, {
encoding: FileSystem.EncodingType.UTF8,
});
setData(JSON.parse(cached));
setLoading(false);
} catch {
// Cache miss – fetch from network
fetchAndCache();
}
};
loadCache();
}, []);
if (loading) return Loading…;
return (
Cached Data:
{JSON.stringify(data, null, 2)}
);
}
**What this does**:
- On first run, the cache is empty. The app fetches from the network and writes the JSON to
cacheDirectory. - On subsequent launches, the app reads the file immediately, providing instant data.
- Pressing “Refresh” re‑fetches and updates the cache.
Trade‑Off: Cache Purging and Data Freshness
Because cacheDirectory can be cleared by the OS, you must treat cached data as *potentially stale*. Your app should:
- Validate a timestamp or ETag before using the cache.
- Provide a manual “Refresh” button to force a network fetch.
- Use
FileSystem.getInfoAsyncto check file age before reading.
If you need guaranteed persistence, store the data in documentDirectory or use a database like expo-sqlite.
Actionable Checklist
- Run
expo install expo-file-systemand ensure you’re on SDK 48+. - Pick the correct directory constant based on data importance.
- Always perform I/O with async methods and handle errors gracefully.
- Add Android permissions only if targeting API 29 or lower.
- Test by inspecting the file path printed to the console and verifying the file exists in Android Studio’s Device File Explorer or Xcode’s Devices & Simulators.
- Implement a cache‑validation strategy to avoid stale data.
By following these steps, you’ll have a reliable, cross‑platform caching layer that respects both iOS and Android storage policies while keeping your UI snappy.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.