expo-file-system
Sandboxed paths, downloads, and migration from legacy FS API - the expo-file-system cookbook for Expo SDK 57 apps that cache PDFs, photos, and offline payloads on device.
Search across all documentation pages
Sandboxed paths, downloads, and migration from legacy FS API - the expo-file-system cookbook for Expo SDK 57 apps that cache PDFs, photos, and offline payloads on device.
Quick-reference recipe card - copy-paste ready.
npx expo install expo-file-systemimport { Directory, File, Paths } from "expo-file-system";
export function ensureDownloadDir() {
const dir = new Directory(Paths.cache, "downloads");
if (!dir.exists) {
dir.create();
}
return dir;
}
export async function downloadPdf(url: string, filename: string) {
const dir = ensureDownloadDir();
const output = await File.downloadFileAsync(url, dir);
return output.uri;
}
export function writeJsonCache(name: string, data: unknown) {
const file = new File(Paths.cache, `${name}.json`);
if (!file.exists) {
file.create();
}
file.write(JSON.stringify(data));
}When to reach for this:
File.createUploadTask progress.When to avoid:
Download with progress, JSON sidecar metadata, and legacy interop during migration.
npx expo install expo-file-system// src/files/downloadWithProgress.ts
import { Directory, File, Paths } from "expo-file-system";
export async function downloadInspectionPdf(
url: string,
jobId: string,
onProgress: (ratio: number) => void
) {
const dir = new Directory(Paths.document, "inspections", jobId);
if (!dir.exists) {
dir.create({ intermediates: true });
}
const destination = new File(dir, "report.pdf");
const task = File.createDownloadTask(url, destination, {
onProgress: ({ bytesWritten, totalBytes }) => {
if (totalBytes > 0) {
onProgress(bytesWritten / totalBytes);
}
},
});
const file = await task.downloadAsync();
if (!file) throw new Error("Download paused or cancelled");
const meta = new File(dir, "meta.json");
meta.create();
meta.write(JSON.stringify({ url, downloadedAt: Date.now() }));
return file.uri;
}// src/files/listInspections.ts
import { Directory, Paths } from "expo-file-system";
export function listInspectionJobIds(): string[] {
const root = new Directory(Paths.document, "inspections");
if (!root.exists) return [];
return root
.list()
.filter((entry) => entry instanceof Directory)
.map((d) => d.name);
}// src/files/legacyRead.ts - temporary bridge
import * as FileSystem from "expo-file-system/legacy";
import { File, Paths } from "expo-file-system";
export async function readLegacyManifest() {
const file = new File(Paths.cache, "legacy-manifest.json");
if (!file.exists) return null;
const text = await FileSystem.readAsStringAsync(file.uri);
return JSON.parse(text) as { version: number };
}What this demonstrates:
Paths.document for user-meaningful PDFs - survives cache sweeps.createDownloadTask with progress callback for UI bars.Directory.list() for enumerating job folders.| Path | Persistence | Typical use |
|---|---|---|
Paths.document | Until app uninstall | User exports, inspection PDFs |
Paths.cache | OS may evict | Thumbnails, temp downloads |
Paths.bundle | Read-only bundled assets | Seed files at first launch |
import { Paths } from "expo-file-system";
console.log(Paths.document.uri);
console.log(Paths.cache.uri);file.exists.const dir = new Directory(Paths.cache, "avatars");
dir.create({ intermediates: true });
const avatar = dir.createFile("user-42.jpg", "image/jpeg");
avatar.write(bytes);
const copy = new File(Paths.cache, "avatars", "user-42-copy.jpg");
avatar.copy(copy);
avatar.move(new Directory(Paths.document, "archive"));file:// string concat bugs.exists property before read - throws are for wrong File/Directory type on existing paths.textSync / bytesSync for small files - async variants for large payloads.// Simple one-shot download
await File.downloadFileAsync(url, new Directory(Paths.cache, "pdfs"));
// Upload with progress
const file = new File(Paths.document, "photo.jpg");
const task = file.createUploadTask("https://api.example.com/upload", {
uploadType: File.UploadType?.MULTIPART ?? 1,
onProgress: ({ bytesSent, totalBytes }) => {
console.log(bytesSent, totalBytes);
},
});
await task.uploadAsync();expo/fetch with File body for multipart when upload task options are overkill.// Before (legacy)
import * as FileSystem from "expo-file-system/legacy";
await FileSystem.readAsStringAsync(uri);
// After (SDK 57)
import { File } from "expo-file-system";
const file = new File(uri);
const text = file.textSync();expo-file-system/legacy only at unmigrated call sites.File from legacy URI strings - new File(existingUri).npx expo doctor and grep show zero /legacy imports.{
"expo": {
"plugins": [
[
"expo-file-system",
{
"enableFileSharing": true,
"supportsOpeningDocumentsInPlace": true
}
]
]
}
}documentDirectory - useful for exported PDFs.Paths.document.new File(Paths.cache, "a", "b.txt").bytes() / streams.intermediates: true - Nested create() fails. Fix: { intermediates: true } on deep paths.| Alternative | Use When | Don't Use When |
|---|---|---|
File / Directory API | New SDK 57 code | Maintaining SDK 54 apps without migration bandwidth |
expo-file-system/legacy | Unmigrated modules | Greenfield features |
expo-sqlite | Relational rows | Large binary blobs |
AsyncStorage | Small JSON prefs | Files larger than a few KB |
npx expo install expo-file-systemImport { File, Directory, Paths } from expo-file-system - not the default namespace alone.
Paths.cache directories on app version upgrade or settings action.import { File, Paths } from "expo-file-system";
const seed = new File(Paths.bundle, "seed-data.json");const dir = new Directory(Paths.document, "inspections", jobId);
if (dir.exists) dir.delete(); // recursiveStack versions: This page was written for React 19.2.3, React Native 0.86.0, and Expo SDK 57 (
expo~57.0.4).
Reviewed by Chris St. John·Last updated Jul 16, 2026