Expo File System gets a major upgrade in SDK 54
Product•Development••7 minutes read
Aleksander Mikucki
Engineering
A modern, object-based file system API is now stable in Expo SDK 54. It's simpler, faster, and built for the New Architecture.

Last year on this blog, we shared more information on how Shared Objects help both the Expo team and the wider community ship modules that are both performant and easy to use for developers.
In that article (worth a read if you haven’t had a chance yet) we briefly described an early release of the new file system module that came out in SDK 52 as expo-file-system/next.
Now, with SDK 54 we’re finally promoting expo-file-system/next to stable and dropping the next entrypoint. While the old filesystem API is still available under expo-file-system/legacy if you need more time to migrate, we encourage you to migrate in this SDK. As always, we appreciate it if you report issues, any features you’re missing, and share general feedback.
Motivation (on why we even do breaking changes)
Mobile apps don’t rely on direct access to files and directories as much as your typical desktop program, but it’s still a pretty common and important SDK module.
I personally remember working on react-native apps circa 2019-2020. While the general experience was great, sometimes things that seemed easy turned out to be rather painful, way more than they would be on the underlying native platform.
A lot of these problems originated from dealing with binary or other data that wasn’t serializable over the bridge.
That’s why, back then, we relied heavily on base64 for passing data between native modules (I sometimes sent user avatars as base64 to the backend just because it was the easiest option to set up).
Since then a lot has changed – new architecture dropped the bridge entirely, we heavily improved a lot of the Expo SDK, we introduced SharedObjects and SharedRefs that are essentially pointers to native instances, data structures, and way more.
We have so many new module capabilities and concepts, so it only makes sense to update the APIs to use them. We want Expo in general, and the Expo SDK, to lead users down a wide path towards success.
Important functions and use-cases should be easy, and making mistakes or using APIs in non-performant ways really shouldn’t be.
Key concepts (how the module works now)
There are a few assumptions we made quite early when rewriting the module:
It’s 100% more object based now
The previous API always accepted a uri parameter for each method.
While it was easy and convenient for us as library authors (back then the only things we could pass as a parameter to the native code were strings and numbers anyway), it doesn’t really make for an ergonomic API when you need to do more complicated operations on the filesystem.
Here’s what creating a new directory in your documents looked like (sure there are probably options you can use to simplify that, but this is an obvious solution):
const newPath = `${FileSystem.documentDirectory}/myNewFolderName`const info = await FileSystem.getInfoAsync(newPath)if(!info.exists) {FileSystem.makeDirectoryAsync(newPath)}
This complexity is why we introduced File and Directory classes to make these operations easier. They do store URIs under the hood, but they improve the DX in a couple of different ways:
- No need to manually concatenate path strings
- Code suggestions that promote methods that make sense in the current context (that’s also why we’ve split off files and directories into separate classes)
- Ability to chain some function calls
Here’s what that example looks like in the modern API:
const directory = Directory(Paths.document, "myNewFolderName")if (!directory.exists) {directory.create()}
Another benefit is that listing directory contents now returns actual File and Directory instances!
const directory = Directory(Paths.document, "myNewFolderName")for (const item of directory.list()) {if (item instanceof Directory) {console.log(`DIRECTORY: ${item.name}`)} else {console.log(`FILE: ${item.name}`)}}
No more serialization (unless you actually want to)
Different modules need different inputs (kinda like Factorio), so we’ve built plenty of functions to read and write data into a file in many different ways.
Strings and TypedArrays just work, both as synchronous and asynchronous functions:
const file = new File(Paths.document, "myFile.txt")// data is either a string or a Uint8Arrayfile.write(data)file.text() // Promise<string>file.textSync() // stringfile.bytes() // Promise<Uint8Array>file.bytesSync() // Uint8Arrayfile.base64() // Promise<string>file.base64Sync() // string
We provide readable and writable streams as well as a custom FileHandle class to support applications like loggers, reading large files, seeking, ect.
file.readableStream() // ReadableStream<Uint8Array>file.writableStream() // WritableStream<Uint8Array>// Read 10 bytes from the center of a fileconst fileHandle = file.open()fileHandle.offset = file.size / 2fileHandle.readBytes(10) // Uint8ArrayfileHandle.close()
In short, we want you to have flexibility when reading and writing files with any type of string, base64, or binary data.
It’s a blob and it works with expo/fetch
The File instances also implement the Blob interface, which not only makes them play nice with a lot of existing ecosystem packages, but also makes it really easy to use expo/fetch :
import { fetch } from 'expo/fetch';import { File } from 'expo-file-system';const src = new File(testDirectory, 'file.txt');file.write('Hello, world!');const response = await fetch('https://example.com', {method: 'POST',body: file,});
In fact, we’re thinking of expo/fetch as the default way to download and upload files from remote URLs.
SAF + security scoped resources built in
Android uses something called SAF for accessing files from cloud providers and when selected by the user using the system picker.
Our new API supports SAF URIs directly inside the File and Directory classes where possible, so you should be able to reuse code that deals with resources always available to your app (those that live in your cache or documents folder) and those that you are explicitly granted access to via SAF.
We’ve also added support for iOS’s Security Scoped Resources, again inside the core File and Directory classes, and added built-in pickers to make it easier to add file selection on both platforms. Still, we have full compatibility with expo-document-picker , so you’re free to keep your existing code.
File.pickFileAsync(): Promise<File>
What about Web?
The Web platform doesn’t really directly expose file access at arbitrary URIs, so most of the methods available on iOS and Android don’t really make as much sense on the Web.
On the Web, the only entry is from a user-presented picker for files (as a web File interface that is essentially a Blob or via the new FileSystemHandle API).
We’re exploring exposing one of these APIs under Expo FileSystem at some point, but we’d love to gauge community interest in a common File API covering all 3 platforms and use-cases for it first.
In the meantime for most applications that handle files on all three platforms you can rely on our File matching the Blob interface to build common code accepting either Expo’s File on native or the File from <input type="file"> elements on Web.
Feedback is always welcome!
We really hope the new API covers all your use-cases for file system access in Expo apps. As always, we’re open and extremely grateful for any feedback, Github issues, feature requests or tweets you want to send our way.
More examples, API specification and more is available in our docs.
And please join the livestream tomorrow if you have questions or would like to see live demos from SDK 54:




